Skip to content

Commit

Permalink
[DependencyInjection] Explicitly add service definitions with a new `…
Browse files Browse the repository at this point in the history
…Service` attribute

Sometimes its better to explicitly mark classes that should be service definitions instead of including everything and then working with either the `exclude` path from the resource loader, or the `Exclude` attribute.
To achieve this, the following things were done:

* Add a new `Service` attribute which can be used on classes
* Add a new `onlyWithServiceAttribute` configuration to the resource loader
  If this option (along with `autoconfigure=true`) is set to `true`, only classes that have a `Service` attribute will be added as a service definition in the container.
  All other classes/files from the resource path will still be added, but marked as excluded with the `container.excluded` tag.

Example PHP configuration:

```php
return function (ContainerConfigurator $c) {
    $services = $c->services();
    $services
        ->load('YourNamespace\\', '/path/to/namespace')
        ->onlyWithServiceAttribute()
        ->autoconfigure();
};
```

Example XML configuration:

```xml
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
  <services>
      <prototype namespace="YourNamespace\" resource="/path/to/namespace" autoconfigure="true" onlyWithServiceAttribute="true" />
  </services>
</container>
```

Example YML configuration:

```yml
services:
    YourNamespace\:
        resource: /path/to/namespace
        onlyWithServiceAttribute: true
        autoconfigure: true
```
  • Loading branch information
Spea committed Jul 26, 2024
1 parent ab9d2f2 commit 87a026a
Show file tree
Hide file tree
Showing 23 changed files with 283 additions and 14 deletions.
20 changes: 20 additions & 0 deletions src/Symfony/Component/DependencyInjection/Attribute/Service.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Attribute;

/**
* An attribute to explicitly mark a class as a service definition.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class Service
{
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.2
---

* Explicitly add service definitions with a new `Service` attribute

7.1
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class PrototypeConfigurator extends AbstractServiceConfigurator

private ?array $excludes = null;

private bool $onlyWithServiceAttribute = false;

public function __construct(
ServicesConfigurator $parent,
private PhpFileLoader $loader,
Expand Down Expand Up @@ -67,11 +69,18 @@ public function __destruct()
parent::__destruct();

if (isset($this->loader)) {
$this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->excludes, $this->path);
$this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->excludes, $this->path, $this->onlyWithServiceAttribute);
}
unset($this->loader);
}

public function onlyWithServiceAttribute(bool $onlyWithServiceAttribute = true): static
{
$this->onlyWithServiceAttribute = $onlyWithServiceAttribute;

return $this;
}

/**
* Excludes files from registration using glob patterns.
*
Expand Down
21 changes: 14 additions & 7 deletions src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
use Symfony\Component\DependencyInjection\Attribute\Service;
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\DependencyInjection\Attribute\WhenNot;
use Symfony\Component\DependencyInjection\ChildDefinition;
Expand Down Expand Up @@ -100,13 +101,14 @@ public function import(mixed $resource, ?string $type = null, bool|string $ignor
/**
* Registers a set of classes as services using PSR-4 for discovery.
*
* @param Definition $prototype A definition to use as template
* @param string $namespace The namespace prefix of classes in the scanned directory
* @param string $resource The directory to look for classes, glob-patterns allowed
* @param string|string[]|null $exclude A globbed path of files to exclude or an array of globbed paths of files to exclude
* @param string|null $source The path to the file that defines the auto-discovery rule
* @param Definition $prototype A definition to use as template
* @param string $namespace The namespace prefix of classes in the scanned directory
* @param string $resource The directory to look for classes, glob-patterns allowed
* @param string|string[]|null $exclude A globbed path of files to exclude or an array of globbed paths of files to exclude
* @param string|null $source The path to the file that defines the auto-discovery rule
* @param bool|null $onlyWithServiceAttribute Whether to include only classes with the service attribute
*/
public function registerClasses(Definition $prototype, string $namespace, string $resource, string|array|null $exclude = null, ?string $source = null): void
public function registerClasses(Definition $prototype, string $namespace, string $resource, string|array|null $exclude = null, ?string $source = null, ?bool $onlyWithServiceAttribute = null): void
{
if (!str_ends_with($namespace, '\\')) {
throw new InvalidArgumentException(\sprintf('Namespace prefix must end with a "\\": "%s".', $namespace));
Expand Down Expand Up @@ -154,13 +156,18 @@ public function registerClasses(Definition $prototype, string $namespace, string
$this->addContainerExcludedTag($class, $source);
continue;
}
if ($onlyWithServiceAttribute && !($r->getAttributes(Service::class)[0] ?? null)) {
$this->addContainerExcludedTag($class, $source);
continue;
}

if ($this->env) {
$excluded = true;
$whenAttributes = $r->getAttributes(When::class, \ReflectionAttribute::IS_INSTANCEOF);
$notWhenAttributes = $r->getAttributes(WhenNot::class, \ReflectionAttribute::IS_INSTANCEOF);

if ($whenAttributes && $notWhenAttributes) {
throw new LogicException(sprintf('The "%s" class cannot have both #[When] and #[WhenNot] attributes.', $class));
throw new LogicException(\sprintf('The "%s" class cannot have both #[When] and #[WhenNot] attributes.', $class));
}

if (!$whenAttributes && !$notWhenAttributes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ private function parseDefinitions(\DOMDocument $xml, string $file, Definition $d
}
$excludes = [$service->getAttribute('exclude')];
}
$this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes, $file);
$onlyWithServiceAttribute = XmlUtils::phpize($service->getAttribute('onlyWithServiceAttribute'));
$this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes, $file, $onlyWithServiceAttribute);
} else {
$this->setDefinition((string) $service->getAttribute('id'), $definition);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class YamlFileLoader extends FileLoader
'autoconfigure' => 'autoconfigure',
'bind' => 'bind',
'constructor' => 'constructor',
'onlyWithServiceAttribute' => 'onlyWithServiceAttribute',
];

private const INSTANCEOF_KEYWORDS = [
Expand Down Expand Up @@ -707,7 +708,8 @@ private function parseDefinition(string $id, array|string|null $service, string
}
$exclude = $service['exclude'] ?? null;
$namespace = $service['namespace'] ?? $id;
$this->registerClasses($definition, $namespace, $service['resource'], $exclude, $file);
$onlyWithServiceAttribute = $service['onlyWithServiceAttribute'] ?? null;
$this->registerClasses($definition, $namespace, $service['resource'], $exclude, $file, $onlyWithServiceAttribute);
} else {
$this->setDefinition($id, $definition);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
<xsd:attribute name="autowire" type="boolean" />
<xsd:attribute name="autoconfigure" type="boolean" />
<xsd:attribute name="constructor" type="xsd:string" />
<xsd:attribute name="onlyWithServiceAttribute" type="boolean" />
</xsd:complexType>

<xsd:complexType name="stack">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes;

use Symfony\Component\DependencyInjection\Attribute\Service;

#[Service]
class BarWithService
{
public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null)
{
}

public function setFoo(self $foo)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes;

use Symfony\Component\DependencyInjection\Attribute\Service;

#[Service]
class FooWithService
{
public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null)
{
}

public function setFoo(self $foo)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes;

class FooWithoutService
{
public function __construct($bar = null, ?iterable $foo = null, ?object $baz = null)
{
}

public function setFoo(self $foo)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
$di->load(Prototype::class.'\\', '../Prototype')
->public()
->autoconfigure()
->exclude('../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}')
->exclude('../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor,ServiceAttributes}')
->factory('f')
->deprecate('vendor/package', '1.1', '%service_id%')
->args([0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
$di->load(Prototype::class.'\\', '../Prototype')
->public()
->autoconfigure()
->exclude(['../Prototype/OtherDir', '../Prototype/BadClasses', '../Prototype/BadAttributes', '../Prototype/SinglyImplementedInterface', '../Prototype/StaticConstructor'])
->exclude(['../Prototype/OtherDir', '../Prototype/BadClasses', '../Prototype/BadAttributes', '../Prototype/SinglyImplementedInterface', '../Prototype/StaticConstructor', '../Prototype/ServiceAttributes'])
->factory('f')
->deprecate('vendor/package', '1.1', '%service_id%')
->args([0])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

services:
service_container:
class: Symfony\Component\DependencyInjection\ContainerInterface
public: true
synthetic: true
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService
public: true
tags:
- foo
- baz
deprecated:
package: vendor/package
version: '1.1'
message: '%service_id%'
autoconfigure: true
lazy: true
arguments: [1]
factory: f
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService
public: true
tags:
- foo
- baz
deprecated:
package: vendor/package
version: '1.1'
message: '%service_id%'
autoconfigure: true
lazy: true
arguments: [1]
factory: f
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;

return function (ContainerConfigurator $c) {
$di = $c->services()->defaults()
->tag('baz');

$di->load(Prototype::class.'\\', '../Prototype')
->onlyWithServiceAttribute()
->public()
->autoconfigure()
->exclude('../Prototype/{BadClasses,BadAttributes}')
->factory('f')
->deprecate('vendor/package', '1.1', '%service_id%')
->args([0])
->args([1])
->tag('foo')
->parent('foo');

$di->set('foo')->lazy()->abstract()->public();
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" exclude="../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}" />
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" exclude="../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor,ServiceAttributes}" />
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<exclude>../Prototype/BadAttributes</exclude>
<exclude>../Prototype/SinglyImplementedInterface</exclude>
<exclude>../Prototype/StaticConstructor</exclude>
<exclude>../Prototype/ServiceAttributes</exclude>
</prototype>
</services>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<exclude>../Prototype/BadAttributes</exclude>
<exclude>../Prototype/SinglyImplementedInterface</exclude>
<exclude>../Prototype/StaticConstructor</exclude>
<exclude>../Prototype/ServiceAttributes</exclude>
<exclude> </exclude>
</prototype>
</services>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<prototype namespace="Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\" resource="../Prototype/*" exclude="../Prototype/{BadClasses,BadAttributes}" autoconfigure="true" onlyWithServiceAttribute="true" />
</services>
</container>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
services:
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\:
resource: ../Prototype
exclude: '../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor}'
exclude: '../Prototype/{OtherDir,BadClasses,BadAttributes,SinglyImplementedInterface,StaticConstructor,ServiceAttributes}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\:
resource: ../Prototype
exclude: '../Prototype/{BadClasses,BadAttributes}'
onlyWithServiceAttribute: true
autoconfigure: true
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\BarWithService;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\ServiceAttributes\FooWithService;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\BarInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasBarInterface;
Expand Down Expand Up @@ -411,6 +413,33 @@ public function testRegisterClassesWithAsAliasAndImplementingMultipleInterfaces(
'PrototypeAsAlias/{WithAsAliasMultipleInterface,AliasBarInterface,AliasFooInterface}.php'
);
}

public function testRegisterWithOnlyServiceAttribute()
{
$container = new ContainerBuilder();
$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));

$loader->registerClasses(
(new Definition())->setAutoconfigured(true),
'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\',
'Prototype/*',
'Prototype/{BadAttributes,BadClasses}',
null,
true
);

$ids = array_keys(array_filter($container->getDefinitions(), fn ($def) => !$def->hasTag('container.excluded')));
sort($ids);

$this->assertSame([
BarWithService::class,
FooWithService::class,
'service_container',
], $ids);

$this->assertTrue($container->has(FooWithService::class));
$this->assertTrue($container->has(BarWithService::class));
}
}

class TestFileLoader extends FileLoader
Expand Down
Loading

0 comments on commit 87a026a

Please sign in to comment.