Skip to content
Snippets Groups Projects
Commit 1e090476 authored by Tobias Zimmermann's avatar Tobias Zimmermann
Browse files

by tstoeckler: Implement version detection for libraries

parent b1a99e1c
No related branches found
No related tags found
No related merge requests found
Showing
with 447 additions and 50 deletions
......@@ -15,6 +15,9 @@ services:
plugin.manager.libraries.locator:
class: Drupal\libraries\ExternalLibrary\Local\LocatorManager
parent: default_plugin_manager
plugin.manager.libraries.version_detector:
class: Drupal\libraries\ExternalLibrary\Version\VersionDetectorManager
parent: default_plugin_manager
libraries.extension_handler:
class: Drupal\libraries\Extension\ExtensionHandler
......
<?php
namespace Drupal\libraries\Annotation;
use Drupal\Component\Annotation\PluginID;
/**
* Provides an annotation class for version detector plugins.
*
* @Annotation
*/
class VersionDetector extends PluginID {
}
......@@ -7,23 +7,27 @@
namespace Drupal\libraries\ExternalLibrary\Asset;
use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
use Drupal\libraries\ExternalLibrary\LibraryTrait;
use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
use Drupal\libraries\ExternalLibrary\Local\LocalLibraryTrait;
use Drupal\libraries\ExternalLibrary\Remote\RemoteLibraryInterface;
use Drupal\libraries\ExternalLibrary\Remote\RemoteLibraryTrait;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryTrait;
/**
* Provides a base asset library implementation.
*/
class AssetLibrary implements AssetLibraryInterface, LocalLibraryInterface, RemoteLibraryInterface {
class AssetLibrary implements AssetLibraryInterface, VersionedLibraryInterface, LocalLibraryInterface, RemoteLibraryInterface {
use
LibraryTrait,
LocalLibraryTrait,
RemoteLibraryTrait,
SingleAssetLibraryTrait,
LocalRemoteAssetTrait
LocalRemoteAssetTrait,
VersionedLibraryTrait
;
/**
......@@ -36,7 +40,11 @@ class AssetLibrary implements AssetLibraryInterface, LocalLibraryInterface, Remo
*/
public function __construct($id, array $definition) {
$this->id = (string) $id;
// @todo Split this into proper properties.
// @todo Split this into a generic trait.
if (isset($definition['version_detector'])) {
// @todo Validate the sub-keys.
$this->versionDetector = $definition['version_detector'];
}
if (isset($definition['remote_url'])) {
$this->remoteUrl = $definition['remote_url'];
}
......
......@@ -42,10 +42,6 @@ trait LocalRemoteAssetTrait {
/**
* Gets the locator of this library using the locator factory.
*
* Because determining the installation status and library path of a library
* is not specific to any library or even any library type, this logic is
* offloaded to separate locator objects.
*
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
*
* @return \Drupal\libraries\ExternalLibrary\Local\LocatorInterface
......
......@@ -17,6 +17,7 @@ use Drupal\libraries\ExternalLibrary\Exception\InvalidLibraryDependencyException
*
* @see \Drupal\libraries\ExternalLibrary\Asset\AssetLibraryInterface
* @see \Drupal\libraries\ExternalLibrary\ExternalLibraryInterface
* @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface
*/
trait SingleAssetLibraryTrait {
......
......@@ -12,7 +12,7 @@ use Drupal\libraries\ExternalLibrary\Utility\LibraryAccessorTrait;
use Exception;
/**
* Provides an exception for a library definition that cannot be found.
* Provides an exception for a library that is not installed.
*/
class LibraryNotInstalledException extends \RuntimeException {
......
<?php
namespace Drupal\libraries\ExternalLibrary\Exception;
use Drupal\libraries\ExternalLibrary\Utility\LibraryAccessorTrait;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
/**
* Provides an exception for libraries whose version has not been detected.
*/
class UnknownLibraryVersionException extends \RuntimeException {
use LibraryAccessorTrait;
/**
* Constructs a library exception.
*
* @param \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface $library
* The library.
* @param string $message
* (optional) The exception message.
* @param int $code
* (optional) The error code.
* @param \Exception $previous
* (optional) The previous exception.
*/
public function __construct(
VersionedLibraryInterface $library,
$message = '',
$code = 0,
\Exception $previous = NULL
) {
$this->library = $library;
$message = $message ?: "The version of library '{$this->library->getId()}' could not be detected.";
parent::__construct($message, $code, $previous);
}
}
......@@ -23,14 +23,6 @@ interface LibraryInterface {
*/
public function getId();
/**
* Returns the currently installed version of the library.
*
* @return string
* The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
*/
public function getVersion();
/**
* Returns the libraries dependencies, if any.
*
......
......@@ -16,19 +16,6 @@ trait LibraryTrait {
use IdAccessorTrait;
/**
* Returns the currently installed version of the library.
*
* @return string
* The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
*
* @see \Drupal\libraries\ExternalLibrary\LibraryInterface::getVersion()
*/
public function getVersion() {
// @todo Turn into something useful and split into some other trait.
return '1.0';
}
/**
* Returns the libraries dependencies, if any.
*
......
<?php
namespace Drupal\libraries\ExternalLibrary\Version;
/**
* Provides an interface for version detectors.
*/
interface VersionDetectorInterface {
/**
* Detects the version of a library.
*
* @param \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface $library
* The library whose version to detect.
*
* @throws \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
*
* @todo Provide a mechanism for version detectors to provide a reason for
* failing.
*/
public function detectVersion(VersionedLibraryInterface $library);
}
<?php
namespace Drupal\libraries\ExternalLibrary\Version;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\libraries\Annotation\VersionDetector;
use Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface;
/**
* Provides a plugin manager for library version detector plugins.
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
*/
class VersionDetectorManager extends DefaultPluginManager {
/**
* Constructs a version detector manager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/libraries/VersionDetector', $namespaces, $module_handler, VersionDetectorInterface::class, VersionDetector::class);
// @todo Document this hook.
$this->alterInfo('libraries_version_detector_info');
$this->setCacheBackend($cache_backend, 'libraries_version_detector_info');
}
}
<?php
namespace Drupal\libraries\ExternalLibrary\Version;
use Drupal\Component\Plugin\Factory\FactoryInterface;
use Drupal\libraries\ExternalLibrary\LibraryInterface;
/**
* Provides an interface for versioned libraries.
*
* Version detection and negotiation is a key aspect of Libraries API's
* functionality so every type of library should implement this interface.
*/
interface VersionedLibraryInterface extends LibraryInterface {
/**
* Gets the version of the library.
*
* @return string
* The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
*
* @throws \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::setVersion()
*/
public function getVersion();
/**
* Sets the version of the library.
*
* @param string $version
* The version of the library.
*
* @reutrn $this
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::getVersion()
*/
public function setVersion($version);
/**
* Gets the version detector of this library using the detector factory.
*
* Because determining the installation version of a library is not specific
* to any library or even any library type, this logic is offloaded to
* separate detector objects.
*
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
*
* @return \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
*/
public function getVersionDetector(FactoryInterface $detector_factory);
}
<?php
namespace Drupal\libraries\ExternalLibrary\Version;
use Drupal\Component\Plugin\Factory\FactoryInterface;
use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
/**
* Provides a trait for versioned libraries.
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface
*/
trait VersionedLibraryTrait {
/**
* The library version.
*
* @var string
*/
protected $version;
/**
* Information about the version detector to use fo rthis library.
*
* Contains the following keys:
* id: The plugin ID of the version detector.
* configuration: The plugin configuration of the version detector.
*
* @var array
*/
protected $versionDetector = [
'id' => NULL,
'configuration' => [],
];
/**
* Gets the version of the library.
*
* @return string
* The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
*
* @throws \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::getVersion()
*/
public function getVersion() {
if (!isset($this->version)) {
throw new UnknownLibraryVersionException($this);
}
return $this->version;
}
/**
* Sets the version of the library.
*
* @param string $version
* The version of the library.
*
* @return $this
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::setVersion()
*/
public function setVersion($version) {
$this->version = (string) $version;
return $this;
}
/**
* Gets the version detector of this library using the detector factory.
*
* Because determining the installation version of a library is not specific
* to any library or even any library type, this logic is offloaded to
* separate detector objects.
*
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
*
* @return \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
*
* @see \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
*/
public function getVersionDetector(FactoryInterface $detector_factory) {
return $detector_factory->createInstance($this->versionDetector['id'], $this->versionDetector['configuration']);
}
}
......@@ -15,6 +15,7 @@ use Drupal\libraries\ExternalLibrary\LibraryType\LibraryCreationListenerInterfac
use Drupal\libraries\ExternalLibrary\LibraryType\LibraryTypeInterface;
use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
use Drupal\libraries\ExternalLibrary\Utility\IdAccessorTrait;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -35,6 +36,13 @@ class AssetLibraryType implements
*/
protected $locatorFactory;
/**
* The version detector factory.
*
* @var \Drupal\Component\Plugin\Factory\FactoryInterface
*/
protected $detectorFactory;
/**
* Constructs the asset library type.
*
......@@ -42,17 +50,24 @@ class AssetLibraryType implements
* The plugin ID taken from the class annotation.
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
* The locator factory.
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
* The version detector factory.
*/
public function __construct($plugin_id, FactoryInterface $locator_factory) {
public function __construct($plugin_id, FactoryInterface $locator_factory, FactoryInterface $detector_factory) {
$this->id = $plugin_id;
$this->locatorFactory = $locator_factory;
$this->detectorFactory = $detector_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($plugin_id, $container->get('plugin.manager.libraries.locator'));
return new static(
$plugin_id,
$container->get('plugin.manager.libraries.locator'),
$container->get('plugin.manager.libraries.version_detector')
);
}
/**
......@@ -71,6 +86,9 @@ class AssetLibraryType implements
if ($library instanceof LocalLibraryInterface) {
$library->getLocator($this->locatorFactory)->locate($library);
}
if ($library instanceof VersionedLibraryInterface) {
$library->getVersionDetector($this->detectorFactory)->detectVersion($library);
}
}
}
......@@ -16,6 +16,7 @@ use Drupal\libraries\ExternalLibrary\LibraryType\LibraryTypeInterface;
use Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLibrary;
use Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLoaderInterface;
use Drupal\libraries\ExternalLibrary\Utility\IdAccessorTrait;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -37,6 +38,13 @@ class PhpFileLibraryType implements
*/
protected $locatorFactory;
/**
* The version detector factory.
*
* @var \Drupal\Component\Plugin\Factory\FactoryInterface
*/
protected $detectorFactory;
/**
* The PHP file loader.
*
......@@ -51,12 +59,15 @@ class PhpFileLibraryType implements
* The plugin ID taken from the class annotation.
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
* The locator factory.
* @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
* The version detector factory.
* @param \Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLoaderInterface $php_file_loader
* The PHP file loader.
*/
public function __construct($plugin_id, FactoryInterface $locator_factory, PhpFileLoaderInterface $php_file_loader) {
public function __construct($plugin_id, FactoryInterface $locator_factory, FactoryInterface $detector_factory, PhpFileLoaderInterface $php_file_loader) {
$this->id = $plugin_id;
$this->locatorFactory = $locator_factory;
$this->detectorFactory = $detector_factory;
$this->phpFileLoader = $php_file_loader;
}
......@@ -67,6 +78,7 @@ class PhpFileLibraryType implements
return new static(
$plugin_id,
$container->get('plugin.manager.libraries.locator'),
$container->get('plugin.manager.libraries.version_detector'),
$container->get('libraries.php_file_loader')
);
}
......@@ -82,8 +94,11 @@ class PhpFileLibraryType implements
* {@inheritdoc}
*/
public function onLibraryCreate(LibraryInterface $library) {
/** @var \Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLibraryInterface $library */
/** @var \Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLibraryInterface|\Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface $library */
$library->getLocator($this->locatorFactory)->locate($library);
if ($library instanceof VersionedLibraryInterface) {
$library->getVersionDetector($this->detectorFactory)->detectVersion($library);
}
}
/**
......@@ -94,6 +109,7 @@ class PhpFileLibraryType implements
foreach ($library->getPhpFiles() as $file) {
$this->phpFileLoader->load($file);
}
}
}
<?php
namespace Drupal\libraries\Plugin\libraries\VersionDetector;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
use Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Detects the version by matching lines in a file against a specified pattern.
*
* This version detector can be used if the library version is denoted in a
* particular format in a changelog or readme file, for example.
*
* @VersionDetector("line_pattern")
*/
class LinePatternDetector extends PluginBase implements VersionDetectorInterface, ContainerFactoryPluginInterface {
/**
* The app root.
*
* @var string
*/
protected $appRoot;
/**
* Constructs a line pattern version detector.
*
* @param array $configuration
* @param string $plugin_id
* @param array $plugin_definition
* @param string $app_root
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, $app_root) {
$configuration += [
'file' => '',
'pattern' => '',
'lines' => 20,
'columns' => 200,
];
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->appRoot = $app_root;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('app.root')
);
}
/**
* {@inheritdoc}
*/
public function detectVersion(VersionedLibraryInterface $library) {
if (!($library instanceof LocalLibraryInterface)) {
throw new UnknownLibraryVersionException($library);
}
$filepath = $this->appRoot . '/' . $library->getLocalPath() . '/' . $this->configuration['file'];
if (!file_exists($filepath)) {
throw new UnknownLibraryVersionException($library);
}
$file = fopen($filepath, 'r');
$lines = $this->configuration['lines'];
while ($lines && $line = fgets($file, $this->configuration['columns'])) {
if (preg_match($this->configuration['pattern'], $line, $version)) {
fclose($file);
$library->setVersion($version[1]);
return;
}
$lines--;
}
fclose($file);
}
}
<?php
namespace Drupal\libraries\Plugin\libraries\VersionDetector;
use Drupal\Core\Plugin\PluginBase;
use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
use Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface;
use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
/**
* Detects the version by returning a static string.
*
* As this does not perform any actual detection and, thus, circumvents any
* negotiation of versions by Libraries API it should only be used for testing
* or when the version of a library cannot be determined from the source code
* itself.
*
* @VersionDetector("static")
*/
class StaticDetector extends PluginBase implements VersionDetectorInterface {
/**
* Constructs a static version detector.
*
* @param array $configuration
* @param string $plugin_id
* @param array $plugin_definition
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
$configuration += [
'version' => NULL,
];
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function detectVersion(VersionedLibraryInterface $library) {
if (!isset($this->configuration['version'])) {
throw new UnknownLibraryVersionException($library);
}
$library->setVersion($this->configuration['version']);
}
}
type: asset
version_detector:
id: static
configuration:
version: '1.0.0'
remote_url: http://example.com
css:
base:
......
<?php
/**
* @file
* Contains \Drupal\Tests\libraries\Kernel\ExternalLibrary\Asset\AssetLibraryTest.
*/
namespace Drupal\Tests\libraries\Kernel\ExternalLibrary\Asset;
use Drupal\libraries\ExternalLibrary\Asset\AssetLibrary;
......@@ -55,7 +50,7 @@ class AssetLibraryTest extends LibraryKernelTestBase {
$this->assertEquals('test_asset_library', $library->getId());
$expected = ['test_asset_library' => [
'version' => 1.0,
'version' => '1.0.0',
'css' => ['base' => ['http://example.com/example.css' => []]],
'js' => ['http://example.com/example.js' => []],
'dependencies' => [],
......@@ -84,19 +79,19 @@ class AssetLibraryTest extends LibraryKernelTestBase {
public function testAssetLibraryRemote() {
$library = $this->libraryDiscovery->getLibraryByName('libraries', 'test_asset_library');
$expected = [
'version' => '1.0',
'version' => '1.0.0',
'css' => [[
'weight' => -200,
'group' => 0,
'type' => 'external',
'data' => 'http://example.com/example.css',
'version' => '1.0',
'version' => '1.0.0',
]],
'js' => [[
'group' => -100,
'type' => 'external',
'data' => 'http://example.com/example.js',
'version' => '1.0',
'version' => '1.0.0',
]],
'dependencies' => [],
'license' => [
......@@ -120,19 +115,19 @@ class AssetLibraryTest extends LibraryKernelTestBase {
$this->libraryDiscovery->clearCachedDefinitions();
$library = $this->libraryDiscovery->getLibraryByName('libraries', 'test_asset_library');
$expected = [
'version' => '1.0',
'version' => '1.0.0',
'css' => [[
'weight' => -200,
'group' => 0,
'type' => 'file',
'data' => $this->modulePath . '/tests/assets/vendor/test_asset_library/example.css',
'version' => '1.0',
'version' => '1.0.0',
]],
'js' => [[
'group' => -100,
'type' => 'file',
'data' => $this->modulePath . '/tests/assets/vendor/test_asset_library/example.js',
'version' => '1.0',
'version' => '1.0.0',
'minified' => FALSE,
]],
'dependencies' => [],
......
<?php
/**
* @file
* Contains \Drupal\Tests\libraries\Kernel\ExternalLibrary\PhpFile\PhpFileLibraryTest.
*/
namespace Drupal\Tests\libraries\Kernel\ExternalLibrary\PhpFile;
use Drupal\libraries\ExternalLibrary\Exception\LibraryClassNotFoundException;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment