Commit e559eaa9 authored by catch's avatar catch

Issue #2671034 by alexpott, damiankloip: Create plugin per file YAML discovery

parent f7c02dfe
......@@ -17,6 +17,9 @@ interface DiscoverableInterface {
*
* @return array
* An array of discovered data keyed by provider.
*
* @throws \Drupal\Component\Discovery\DiscoveryException
* Exception thrown if there is a problem during discovery.
*/
public function findAll();
......
<?php
/**
* @file
* Contains \Drupal\Component\Discovery\DiscoveryException.
*/
namespace Drupal\Component\Discovery;
/**
* Exception thrown during discovery if the data is invalid.
*/
class DiscoveryException extends \RuntimeException {
}
<?php
/**
* @file
* Contains \Drupal\Component\Discovery\YamlDirectoryDiscovery.
*/
namespace Drupal\Component\Discovery;
use Drupal\Component\FileSystem\RegexDirectoryIterator;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\FileCache\FileCacheFactory;
/**
* Discovers multiple YAML files in a set of directories.
*/
class YamlDirectoryDiscovery implements DiscoverableInterface {
/**
* Defines the key in the discovered data where the file path is stored.
*/
const FILE_KEY = '_discovered_file_path';
/**
* An array of directories to scan, keyed by the provider.
*
* The value can either be a string or an array of strings. The string values
* should be the path of a directory to scan.
*
* @var array
*/
protected $directories = [];
/**
* The suffix for the file cache key.
*
* @var string
*/
protected $fileCacheKeySuffix;
/**
* The key contained in the discovered data that identifies it.
*
* @var string
*/
protected $idKey;
/**
* Constructs a YamlDirectoryDiscovery object.
*
* @param array $directories
* An array of directories to scan, keyed by the provider. The value can
* either be a string or an array of strings. The string values should be
* the path of a directory to scan.
* @param string $file_cache_key_suffix
* The file cache key suffix. This should be unique for each type of
* discovery.
* @param string $key
* (optional) The key contained in the discovered data that identifies it.
* Defaults to 'id'.
*/
public function __construct(array $directories, $file_cache_key_suffix, $key = 'id') {
$this->directories = $directories;
$this->fileCacheKeySuffix = $file_cache_key_suffix;
$this->idKey = $key;
}
/**
* {@inheritdoc}
*/
public function findAll() {
$all = array();
$files = $this->findFiles();
$file_cache = FileCacheFactory::get('yaml_discovery:' . $this->fileCacheKeySuffix);
// Try to load from the file cache first.
foreach ($file_cache->getMultiple(array_keys($files)) as $file => $data) {
$all[$files[$file]][$this->getIdentifier($file, $data)] = $data;
unset($files[$file]);
}
// If there are files left that were not returned from the cache, load and
// parse them now. This list was flipped above and is keyed by filename.
if ($files) {
foreach ($files as $file => $provider) {
// If a file is empty or its contents are commented out, return an empty
// array instead of NULL for type consistency.
try {
$data = Yaml::decode(file_get_contents($file)) ?: [];
}
catch (InvalidDataTypeException $e) {
throw new DiscoveryException("The $file contains invalid YAML", 0, $e);
}
$data[static::FILE_KEY] = $file;
$all[$provider][$this->getIdentifier($file, $data)] = $data;
$file_cache->set($file, $data);
}
}
return $all;
}
/**
* Gets the identifier from the data.
*
* @param string $file
* The filename.
* @param array $data
* The data from the YAML file.
*
* @return string
* The identifier from the data.
*/
protected function getIdentifier($file, array $data) {
if (!isset($data[$this->idKey])) {
throw new DiscoveryException("The $file contains no data in the identifier key '{$this->idKey}'");
}
return $data[$this->idKey];
}
/**
* Returns an array of providers keyed by file path.
*
* @return array
* An array of providers keyed by file path.
*/
protected function findFiles() {
$file_list = [];
foreach ($this->directories as $provider => $directories) {
$directories = (array) $directories;
foreach ($directories as $directory) {
if (is_dir($directory)) {
/** @var \SplFileInfo $fileInfo */
foreach ($this->getDirectoryIterator($directory) as $fileInfo) {
$file_list[$fileInfo->getPathname()] = $provider;
}
}
}
}
return $file_list;
}
/**
* Gets an iterator to loop over the files in the provided directory.
*
* This method exists so that it is easy to replace this functionality in a
* class that extends this one. For example, it could be used to make the scan
* recursive.
*
* @param string $directory
* The directory to scan.
*
* @return \Traversable
* An \Traversable object or array where the values are \SplFileInfo
* objects.
*/
protected function getDirectoryIterator($directory) {
return new RegexDirectoryIterator($directory, '/\.yml$/i');
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Discovery\YamlDiscovery.
*/
namespace Drupal\Core\Plugin\Discovery;
use Drupal\Component\Discovery\YamlDirectoryDiscovery as ComponentYamlDirectoryDiscovery;
/**
* Allows multiple YAML files per directory to define plugin definitions.
*/
class YamlDirectoryDiscovery extends YamlDiscovery {
/**
* Constructs a YamlDirectoryDiscovery object.
*
* @param array $directories
* An array of directories to scan, keyed by the provider. The value can
* either be a string or an array of strings. The string values should be
* the path of a directory to scan.
* @param string $file_cache_key_suffix
* The file cache key suffix. This should be unique for each type of
* discovery.
* @param string $key
* (optional) The key contained in the discovered data that identifies it.
* Defaults to 'id'.
*/
public function __construct(array $directories, $file_cache_key_suffix, $key = 'id') {
// Intentionally does not call parent constructor as this class uses a
// different YAML discovery.
$this->discovery = new ComponentYamlDirectoryDiscovery($directories, $file_cache_key_suffix, $key);
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Component\Discovery\YamlDiscoveryTest.
*/
namespace Drupal\Tests\Component\Discovery;
use Drupal\Component\Discovery\DiscoveryException;
use Drupal\Component\Discovery\YamlDirectoryDiscovery;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
/**
* YamlDirectoryDiscoveryTest component unit tests.
*
* @coversDefaultClass \Drupal\Component\Discovery\YamlDirectoryDiscovery
*
* @group Discovery
*/
class YamlDirectoryDiscoveryTest extends UnitTestCase {
/**
* Tests YAML directory discovery.
*
* @covers ::findAll
*/
public function testDiscovery() {
vfsStream::setup('modules', NULL, [
'test_1' => [
'subdir1' => [
'item_1.test.yml' => "id: item1\nname: 'test1 item 1'",
],
'subdir2' => [
'item_2.test.yml' => "id: item2\nname: 'test1 item 2'",
],
],
'test_2' => [
'subdir1' => [
'item_3.test.yml' => "id: item3\nname: 'test2 item 3'",
],
'subdir2' => [],
],
'test_3' => [],
'test_4' => [
'subdir1' => [
'item_4.test.yml' => "id: item4\nname: 'test4 item 4'",
'item_5.test.yml' => "id: item5\nname: 'test4 item 5'",
'item_6.test.yml' => "id: item6\nname: 'test4 item 6'",
],
],
]);
// Set up the directories to search.
$directories = [
// Multiple directories both with valid items.
'test_1' => [
vfsStream::url('modules/test_1/subdir1'),
vfsStream::url('modules/test_1/subdir2'),
],
// The subdir2 directory is empty.
'test_2' => [
vfsStream::url('modules/test_2/subdir1'),
vfsStream::url('modules/test_2/subdir2'),
],
// Directories that do not exist.
'test_3' => [
vfsStream::url('modules/test_3/subdir1'),
vfsStream::url('modules/test_3/subdir2'),
],
// A single directory.
'test_4' => vfsStream::url('modules/test_4/subdir1'),
];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$data = $discovery->findAll();
$this->assertSame(['id' => 'item1', 'name' => 'test1 item 1', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/subdir1/item_1.test.yml'], $data['test_1']['item1']);
$this->assertSame(['id' => 'item2', 'name' => 'test1 item 2', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/subdir2/item_2.test.yml'], $data['test_1']['item2']);
$this->assertCount(2, $data['test_1']);
$this->assertSame(['id' => 'item3', 'name' => 'test2 item 3', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_2/subdir1/item_3.test.yml'], $data['test_2']['item3']);
$this->assertCount(1, $data['test_2']);
$this->assertTrue(empty($data['test_3']), 'test_3 provides 0 items');
$this->assertSame(['id' => 'item4', 'name' => 'test4 item 4', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1/item_4.test.yml'], $data['test_4']['item4']);
$this->assertSame(['id' => 'item5', 'name' => 'test4 item 5', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1/item_5.test.yml'], $data['test_4']['item5']);
$this->assertSame(['id' => 'item6', 'name' => 'test4 item 6', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_4/subdir1/item_6.test.yml'], $data['test_4']['item6']);
$this->assertCount(3, $data['test_4']);
}
/**
* Tests YAML directory discovery with an alternate ID key.
*
* @covers ::findAll
*/
public function testDiscoveryAlternateId() {
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "alt_id: item1\nid: ignored",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test', 'alt_id');
$data = $discovery->findAll();
$this->assertSame(['alt_id' => 'item1', 'id' => 'ignored', YamlDirectoryDiscovery::FILE_KEY => 'vfs://modules/test_1/item_1.test.yml'], $data['test_1']['item1']);
$this->assertCount(1, $data['test_1']);
}
/**
* Tests YAML directory discovery with a missing ID key.
*
* @covers ::findAll
* @covers ::getIdentifier
*/
public function testDiscoveryNoIdException() {
$this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains no data in the identifier key \'id\'');
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->findAll();
}
/**
* Tests YAML directory discovery with invalid YAML.
*
* @covers ::findAll
*/
public function testDiscoveryInvalidYamlException() {
$this->setExpectedException(DiscoveryException::class, 'The vfs://modules/test_1/item_1.test.yml contains invalid YAML');
vfsStream::setup('modules', NULL, [
'test_1' => [
'item_1.test.yml' => "id: invalid\nfoo : [bar}",
],
]);
// Set up the directories to search.
$directories = ['test_1' => vfsStream::url('modules/test_1')];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->findAll();
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Plugin\Discovery\YamlDiscoveryTest.
*/
namespace Drupal\Tests\Core\Plugin\Discovery;
use Drupal\Component\Discovery\YamlDirectoryDiscovery as ComponentYamlDirectoryDiscovery;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery;
use org\bovigo\vfs\vfsStream;
/**
* @coversDefaultClass \Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery
*
* @group Plugin
*/
class YamlDirectoryDiscoveryTest extends UnitTestCase {
/**
* @covers ::getDefinitions
*/
public function testGetDefinitions() {
vfsStream::setup('modules', NULL, [
'module_a' => [
'subdir1' => [
'plugin1.yml' => "id: plugin1\ntest_provider: module_a",
'plugin2.yml' => "id: plugin2\ntest_provider: module_a",
],
'subdir2' => [
'plugin3.yml' => "id: plugin3\ntest_provider: module_a",
],
'subdir3' => [
]
],
'module_b' => [
'subdir1' => [
'plugin4.yml' => "id: plugin4\ntest_provider: module_b",
],
],
]);
$directories = [
'module_a' => [
vfsStream::url('modules/module_a/subdir1'),
vfsStream::url('modules/module_a/subdir2'),
// Empty directory.
vfsStream::url('modules/module_a/subdir3'),
// Directory does not exist.
vfsStream::url('modules/module_a/subdir4'),
],
'module_b' => vfsStream::url('modules/module_b/subdir1'),
];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$definitions = $discovery->getDefinitions();
$this->assertInternalType('array', $definitions);
$this->assertCount(4, $definitions);
foreach ($definitions as $id => $definition) {
foreach (array('id', 'provider', ComponentYamlDirectoryDiscovery::FILE_KEY) as $key) {
$this->assertArrayHasKey($key, $definition);
}
$this->assertEquals($id, $definition['id']);
$this->assertEquals($definition['test_provider'], $definition['provider']);
}
}
/**
* @covers ::getDefinitions
*/
public function testGetDefinitionsWithTranslatableDefinitions() {
vfsStream::setup('modules', NULL, [
'module_a' => [
'subdir1' => [
'plugin1.yml' => "id: plugin1\ntest_provider: module_a\ntitle: 'test title'",
'plugin2.yml' => "id: plugin2\ntest_provider: module_a\ntitle: 'test title'\ntitle_context: test-context",
],
],
]);
$directories = [
'module_a' => vfsStream::url('modules/module_a/subdir1'),
];
$discovery = new YamlDirectoryDiscovery($directories, 'test');
$discovery->addTranslatableProperty('title', 'title_context');
$definitions = $discovery->getDefinitions();
$this->assertCount(2, $definitions);
$plugin_1 = $definitions['plugin1'];
$plugin_2 = $definitions['plugin2'];
$this->assertInstanceOf(TranslatableMarkup::class, $plugin_1['title']);
$this->assertEquals([], $plugin_1['title']->getOptions());
$this->assertInstanceOf(TranslatableMarkup::class, $plugin_2['title']);
$this->assertEquals(['context' => 'test-context'], $plugin_2['title']->getOptions());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment