Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(symfony): Autoconfigure classes using #[ApiResource] attribute #6943

Open
wants to merge 3 commits into
base: 4.1
Choose a base branch
from

Conversation

GromNaN
Copy link
Contributor

@GromNaN GromNaN commented Feb 5, 2025

Q A
Branch? main
Tickets -
License MIT
Doc PR todo

Using the Symfony bundle, classes with the #[ApiResource] attribute must be in src/ApiResource, src/Entity (for Doctrine ORM) or src/Document (for MongoDB ODM). It's possible to add custom paths in the configuration, but that's one more little thing to take care of.

By using the Symfony autoconfiguration feature, we can detect classes with the #[ApiResource] attribute during the build of the container instead of scanning directories at runtime.

The same feature was implemented in Symfony for the #[JsonEncodable] attribute: symfony/symfony#59401

@GromNaN GromNaN force-pushed the autoconfigure branch 2 times, most recently from 24c6b04 to 939fe53 Compare February 5, 2025 10:36
@GromNaN GromNaN marked this pull request as ready for review February 5, 2025 12:33
@GromNaN GromNaN changed the title feat(symfony): Autoconfigure resource dirs using #[ApiResource] attribute feat(symfony): Autoconfigure resource classes using #[ApiResource] attribute Feb 5, 2025
@GromNaN GromNaN changed the title feat(symfony): Autoconfigure resource classes using #[ApiResource] attribute feat(symfony): Autoconfigure classes using #[ApiResource] attribute Feb 5, 2025
Copy link
Member

@dunglas dunglas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very smart! I like this.

@soyuka
Copy link
Member

soyuka commented Feb 6, 2025

Nice! It reminds me a discussion we had with @nicolas-grekas ^^.
I'm wondering a few things, do we still need:

private function getClassNameResources(): array
{
return [
Error::class,
ValidationException::class,
];
}

The way api_platform.class_name_resources works right now it's called by api_platform.metadata.resource.name_collection_factory.class_name, until this patch classes were discovered using api_platform.resource_class_directories inside api_platform.metadata.resource.name_collection_factory.attributes. Shouldn't we clean that part of the code to remove php files?

$paths = array_unique(array_merge($this->getBundlesResourcesPaths($container, $config), $config['mapping']['paths']));
if (!$config['mapping']['paths']) {
$projectDir = $container->getParameter('kernel.project_dir');
foreach (["$projectDir/config/api_platform", "$projectDir/src/ApiResource"] as $dir) {
if (is_dir($dir)) {
$paths[] = $dir;
}
}
if ($this->isConfigEnabled($container, $config['doctrine']) && is_dir($doctrinePath = "$projectDir/src/Entity")) {
$paths[] = $doctrinePath;
}
if ($this->isConfigEnabled($container, $config['doctrine_mongodb_odm']) && is_dir($documentPath = "$projectDir/src/Document")) {
$paths[] = $documentPath;
}
}
$resources = ['yml' => [], 'xml' => [], 'dir' => []];
foreach ($paths as $path) {
if (is_dir($path)) {
foreach (Finder::create()->followLinks()->files()->in($path)->name('/\.(xml|ya?ml)$/')->sortByName() as $file) {
$resources['yaml' === ($extension = $file->getExtension()) ? 'yml' : $extension][] = $file->getRealPath();
}
$resources['dir'][] = $path;
$container->addResource(new DirectoryResource($path, '/\.(xml|ya?ml|php)$/'));
continue;
}
if ($container->fileExists($path, false)) {
if (!preg_match('/\.(xml|ya?ml)$/', (string) $path, $matches)) {
throw new RuntimeException(\sprintf('Unsupported mapping type in "%s", supported types are XML & YAML.', $path));
}
$resources['yaml' === $matches[1] ? 'yml' : $matches[1]][] = $path;
continue;
}
throw new RuntimeException(\sprintf('Could not open file or directory "%s".', $path));
}
$container->setParameter('api_platform.resource_class_directories', $resources['dir']);
return [$resources['xml'], $resources['yml']];

This is quite heavy work for no real gain, especially that the resources discovered with this patch will already be discovered there right?

Last thing, inside the route loader we declare directory resources (so that the container is rebuilt when these file changes) at:

foreach ($this->resourceClassDirectories as $directory) {
$routeCollection->addResource(new DirectoryResource($directory, '/\.php$/'));
}

If we remove that api_platform.resource_class_directories for php files, we can also remove these lines but we may need to add something to handle these no?

@GromNaN
Copy link
Contributor Author

GromNaN commented Feb 6, 2025

Removing the directory scanning would be a breaking change as the Entity directory is excluded by default from the services config (see FrameworkBundle recipe).

Of-course, this is the target.

@soyuka
Copy link
Member

soyuka commented Feb 6, 2025

Mhh okay but this means that we only need this for the Entity directory, we can remove all the code handling our paths (for example ApiResource). What about the DirectoryResource to rebuild the container on file change? Are services tagged as container.excluded watched by Symfony?

@dunglas
Copy link
Member

dunglas commented Feb 6, 2025

If this doesn’t work for the Entity directory (that we won’t deprecate IMHO), then is this patch really useful?
We’ll have 2 systems to maintain basically forever instead of one 😅

@GromNaN
Copy link
Contributor Author

GromNaN commented Feb 6, 2025

Let's reintroduce the src/Entity dir in the DI config: doctrine/DoctrineBundle#1868

I'll try to move the directory scan to the compiler pass.

@GromNaN
Copy link
Contributor Author

GromNaN commented Feb 10, 2025

Well, I haven't found a solution yet to avoid scanning the configured folders twice (all the Entity, Document and ApiResource folders of the project and bundles).

If this doesn’t work for the Entity directory (that we won’t deprecate IMHO), then is this patch really useful?
This PR doesn't bring any performance gain, but at least it enables a feature: the ability to put ApiResource classes in any directory or the project without additional configuration.

nicolas-grekas added a commit to symfony/symfony that referenced this pull request Feb 11, 2025
…)` and `ContainerBuilder::findExcludedServiceIds()` for auto-discovering value-objects (GromNaN)

This PR was squashed before being merged into the 7.3 branch.

Discussion
----------

[DependencyInjection] Add `Definition::addExcludedTag()` and `ContainerBuilder::findExcludedServiceIds()` for auto-discovering value-objects

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

We could **not** use the method `findTaggedServiceIds` in #59401 (comment), same for api-platform/core#6943.

As "using the container loading tools to do resource discovery quite seamlessly" [seems to be a good idea](#59401 (review)), this changes make it easier.

I'm not closed to alternative ideas if we want to go further with this use-case.

### Usage

Let's create a `AppModel` attribute class and use it on any class of the project.

In the extension class:
```php
$this->registerAttributeForAutoconfiguration(AppModel::class, static function (ChildDefinition $definition) {
    $definition->addExcludedTag('app.model');
});
```

In a compiler pass:
```php
$classes = [];
foreach($containerBuilder->findExcludedServiceIds('app.model') as $id => $tags) {
    $classes[] = $containerBuilder->getDefinition($id)->getClass();
}

$containerBuilder->setParameter('.app.model_classes', $classes);
```

And this parameter can be injected into a service, or directly update a service definition to inject this list of classes. The attribute parameters can be injected into the tag, and retrieved in the compiler pass, for more advanced configuration.

Commits
-------

7a0443b [DependencyInjection] Add `Definition::addExcludedTag()` and `ContainerBuilder::findExcludedServiceIds()` for auto-discovering value-objects
@GromNaN
Copy link
Contributor Author

GromNaN commented Feb 11, 2025

So I think we need to move the extraction of the classes names into the compiler pass and inject them into a ClassNameResourceNameCollectionFactory. The CachedResourceNameCollectionFactory can then be removed.

@GromNaN
Copy link
Contributor Author

GromNaN commented Mar 13, 2025

Here's how it should be designed to take full advantage of the container builder.

Goals

  • Enable auto-discovery of #[ApiResource] classes anywhere
  • Build the list of "resource" classes during container compilation

Advantage: no need for additional cache unless explicitly required by a custom implementation CachedResourceNameCollectionFactory.

Backward-compatibility constraints:

  • src/ApiResource, src/Entity, src/Document directories must be scanned even if they are not in the DIC autoconfigure paths. This can be deprecated.
  • Keep support for custom ResourceNameCollectionFactoryInterface services
  • Keep support for XML/Yaml files: Detection should be similar to any other config files (doctrine.yaml, validation.yaml with registerMappingFilesFromConfig

Attributes

Detection: Use Symfony ContainerBuilder::registerAttributeForAutoconfiguration() to list all the classes that use the #[ApiResource] attribute and inject them into a ClassNameResourceNameCollectionFactory.

Cache invalidation: The Symfony cache is automatically invalidated and regenerated when one of this classes is modified, removed or added in the autoconfigured directories.

XML/Yaml files

Detection:: List files during container compilation.

Cache invalidation: Adding this files and directories as "resources"; even if they doesn't exist, the container cache is automatically invalidated and regenerated when on of the files is modified, removed or added in the configured directories.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants