feature #31587 [Routing][Config] Allow patterns of resources to be excluded from config loading (tristanbes)

This PR was merged into the 4.4 branch.

Discussion
----------

[Routing][Config] Allow patterns of resources to be excluded from config loading

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #31516
| License       | MIT
| Doc PR        | not yet

The PR will fix the following RFC: #31516

Like resource loading for services, this PR offers a way to exclude patterns of resources like:

```yml
// config/routes/annotations.yaml
controllers:
    resource: ../../src/Controller/*
    type: annotation
    exclude: '../src/Controller/{DebugEmailController}.php'
```

All the annotation routes inside `Controller/` will be loaded in this example except all the one present inside the `Controller/DebugEmailController.php`

Commits
-------

332ff8811c [Routing][Config] Allow patterns of resources to be excluded from config loading
This commit is contained in:
Nicolas Grekas 2019-11-04 11:53:20 +01:00
commit 13dd18c8a6
15 changed files with 68 additions and 13 deletions

View File

@ -19,6 +19,11 @@ Debug
* Deprecated the `FlattenException` class, use the one from the `ErrorRenderer` component instead
* Deprecated the component in favor of the `ErrorHandler` component
Config
------
* Deprecated overriding the `FilerLoader::import()` method without declaring the optional `$exclude` argument
DependencyInjection
-------------------

View File

@ -27,6 +27,7 @@ Config
* Removed `FileLoaderLoadException`, use `LoaderLoadException` instead.
* Using environment variables with `cannotBeEmpty()` if the value is validated with `validate()` will throw an exception.
* Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead
* The `FilerLoader::import()` method has a new `$exclude` argument.
Console
-------
@ -390,6 +391,7 @@ Routing
with the new serialization methods in PHP 7.4.
* Removed `ServiceRouterLoader` and `ObjectRouteLoader`.
* Service route loaders must be tagged with `routing.route_loader`.
* The `RoutingConfigurator::import()` method has a new optional `$exclude` argument.
Security
--------

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.4.0
-----
* added a way to exclude patterns of resources from being imported by the `import()` method
4.3.0
-----

View File

@ -59,10 +59,11 @@ abstract class FileLoader extends Loader
/**
* Imports a resource.
*
* @param mixed $resource A Resource
* @param string|null $type The resource type or null if unknown
* @param bool $ignoreErrors Whether to ignore import errors or not
* @param string|null $sourceResource The original resource importing the new resource
* @param mixed $resource A Resource
* @param string|null $type The resource type or null if unknown
* @param bool $ignoreErrors Whether to ignore import errors or not
* @param string|null $sourceResource The original resource importing the new resource
* @param string|string[]|null $exclude Glob patterns to exclude from the import
*
* @return mixed
*
@ -70,12 +71,25 @@ abstract class FileLoader extends Loader
* @throws FileLoaderImportCircularReferenceException
* @throws FileLocatorFileNotFoundException
*/
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null)
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null/*, $exclude = null*/)
{
if (\func_num_args() < 5 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) {
@trigger_error(sprintf('The "%s()" method will have a new "$exclude = null" argument in version 5.0, not defining it is deprecated since Symfony 4.4.', __METHOD__), E_USER_DEPRECATED);
}
$exclude = \func_num_args() >= 5 ? func_get_arg(4) : null;
if (\is_string($resource) && \strlen($resource) !== $i = strcspn($resource, '*?{[')) {
$excluded = [];
foreach ((array) $exclude as $pattern) {
foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) {
// normalize Windows slashes
$excluded[str_replace('\\', '/', $path)] = true;
}
}
$ret = [];
$isSubpath = 0 !== $i && false !== strpos(substr($resource, 0, $i), '/');
foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath) as $path => $info) {
foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath, false, $excluded) as $path => $info) {
if (null !== $res = $this->doImport($path, $type, $ignoreErrors, $sourceResource)) {
$ret[] = $res;
}

View File

@ -92,6 +92,14 @@ class FileLoaderTest extends TestCase
$this->assertSame(__FILE__, strtr($loader->import('FileLoaderTest.*'), '/', \DIRECTORY_SEPARATOR));
}
public function testImportWithExclude()
{
$loader = new TestFileLoader(new FileLocator(__DIR__.'/../Fixtures'));
$loadedFiles = $loader->import('Include/*', null, false, null, __DIR__.'/../Fixtures/Include/{ExcludeFile.txt}');
$this->assertCount(2, $loadedFiles);
$this->assertNotContains('ExcludeFile.txt', $loadedFiles);
}
}
class TestFileLoader extends FileLoader

View File

@ -38,7 +38,7 @@ class GlobFileLoaderTest extends TestCase
class GlobFileLoaderWithoutImport extends GlobFileLoader
{
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null)
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null, $exclude = null)
{
}
}

View File

@ -6,6 +6,7 @@ CHANGELOG
* Deprecated `ServiceRouterLoader` in favor of `ContainerLoader`.
* Deprecated `ObjectRouteLoader` in favor of `ObjectLoader`.
* Added a way to exclude patterns of resources from being imported by the `import()` method
4.3.0
-----

View File

@ -33,11 +33,14 @@ class RoutingConfigurator
$this->file = $file;
}
final public function import($resource, string $type = null, bool $ignoreErrors = false): ImportConfigurator
/**
* @param string|string[]|null $exclude Glob patterns to exclude from the import
*/
final public function import($resource, string $type = null, bool $ignoreErrors = false, $exclude = null): ImportConfigurator
{
$this->loader->setCurrentDir(\dirname($this->path));
$imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file) ?: [];
$imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file, $exclude) ?: [];
if (!\is_array($imported)) {
return new ImportConfigurator($this->collection, $imported);
}

View File

@ -163,10 +163,24 @@ class XmlFileLoader extends FileLoader
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "prefix" attribute and <prefix> child nodes.', $path));
}
$exclude = [];
foreach ($node->childNodes as $child) {
if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) {
$exclude[] = $child->nodeValue;
}
}
if ($node->hasAttribute('exclude')) {
if ($exclude) {
throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and <exclude> tags at the same time.');
}
$exclude = [$node->getAttribute('exclude')];
}
$this->setCurrentDir(\dirname($path));
/** @var RouteCollection[] $imported */
$imported = $this->import($resource, ('' !== $type ? $type : null), false, $file) ?: [];
$imported = $this->import($resource, ('' !== $type ? $type : null), false, $file, $exclude) ?: [];
if (!\is_array($imported)) {
$imported = [$imported];

View File

@ -28,7 +28,7 @@ use Symfony\Component\Yaml\Yaml;
class YamlFileLoader extends FileLoader
{
private static $availableKeys = [
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8',
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude',
];
private $yamlParser;
@ -169,6 +169,7 @@ class YamlFileLoader extends FileLoader
$schemes = isset($config['schemes']) ? $config['schemes'] : null;
$methods = isset($config['methods']) ? $config['methods'] : null;
$trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true;
$exclude = $config['exclude'] ?? null;
if (isset($config['controller'])) {
$defaults['_controller'] = $config['controller'];
@ -185,7 +186,7 @@ class YamlFileLoader extends FileLoader
$this->setCurrentDir(\dirname($path));
$imported = $this->import($config['resource'], $type, false, $file) ?: [];
$imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: [];
if (!\is_array($imported)) {
$imported = [$imported];

View File

@ -61,9 +61,11 @@
<xsd:sequence maxOccurs="unbounded" minOccurs="0">
<xsd:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="prefix" type="localized-path" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="exclude" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="resource" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="exclude" type="xsd:string" />
<xsd:attribute name="prefix" type="xsd:string" />
<xsd:attribute name="name-prefix" type="xsd:string" />
<xsd:attribute name="host" type="xsd:string" />

View File

@ -38,7 +38,7 @@ class GlobFileLoaderTest extends TestCase
class GlobFileLoaderWithoutImport extends GlobFileLoader
{
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null)
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null, $exclude = null)
{
return new RouteCollection();
}