Description
Description
Dependency injection component implements a method for recursively find classes in a certain folder, which serves as the basis for autowiring capability based upon a trimmed PSR-4 namespace.
During discussions around the messenger component in #33912, a particular need was mentioned by many people: the need to able to propagate a type
information throught the bus, which is not the PHP class name.
In order to solve this, I proposed in #57506, following the main issue, to add an $alias
(name does not matter and shall be discussed later) property on the #[AsMessage]
attribute which serves the purpose of registering the external class alias.
Main problem with this is that you can read the attribute when you know the class name, but you can't find the class name from the raw alias, since you can't read the attribute from an unknown class name. For this, having a discovery phase is required.
Ideally, this discovery phase should happen when the container is built.
Now, we have one major problem: messages are not and will not be services, they are data transport objects. It is clearly stated in #49143 that core contributors don't want the container builder to be abused for such discovery phase, since it would temporarily at least falsly consider messages as services.
Now the Symfony\Component\DependencyInjection\Loader\FileLoader
class has a well functionning discovery methods, originaly wrote for services in the findClasses()
method. Internally it uses the finder
component, but adds a few processing especially for services that don't really matter for me, but also namespace validation and class introspection, that we need.
It would be nice to have a generic attribute-based class discovery mecanism originally for the message alias purpose but which could be used for other use cases.
My opinion is that the container function could be duplicated in another less complex and smaller component, which could be used anywhere: rationale here is that I oftenly had this use case outside of Symfony framework, this would be a useful tool to give to people using standalone components.
Also, the container needs to have some kind of glue code to be able to use it easily during a compilation phase, or within an extension, so that the discovery mechanism can be used during compilation. Here is what I'm expecting for the specific message alias use case:
- Let the user write an arbitrary alias to PHP class name map, for example in
framework.messenger.message_aliases
(a class name can have more than one alias, an alias can have a single class name). - Let the user write the
#[AsMessage(alias: 'foo', otherAliases: ['bar', 'baz"])]
and be discovered during compilation. - Merge the two still during compilation, and place them into a registry (cached or simply hardcoded in container) service.
User YAML configuration could be:
framework:
messenger:
# First occurence of a class name is always the primary alias.
message_aliases:
add_product: App\Message\ProductAdd
deprecated_add_product: App\Message\ProductAdd
# ...
# Those are some sensible defaults for projects created using the symfony CLI.
discovery:
namespace: App\\Message\\
resource: "%kernel.project_dir%/src/Message"
The registry itself would have the following signature:
interface ClassAliasRegistry
{
public function hasAliasFor(string $className): bool;
// Returns the class name if nothing found, always returns primary alias.
public function getAlias(string $className): string;
// Returns as list of ordered aliases.
public function getAllAliases(string $className): string;
public function hasClassNameFor(string $alias): bool;
// Raise exception if no alias, return $alias if is a valid class name.
public function getClassName(string $alias): string;
}
And finally, let the components that require it use it.
Future might be that the serializer
could internally use such maps directly instead of having it decored, which leads to another one feature: having a generic #[Aliased(name: 'foo', otherAliases: ['bar', 'baz"], group: ['a', 'b'])]
(where $group
is string|array
but this doesn't matter here).
So, in this future, the AsMessage
attribute class could actually extend the Aliased
class, by hardcoding the $group
parameter to be message
or messenger_message
or anything else.
This means that:
- Generic way of retrieving aliased class names.
- Generic central map of aliases, which are tagged using groups.
Serializer
knows the map, and decorates theserialize()
andunserialize()
methods with a retrieval of the targgeted class name. It can be either hardcoded in the default implementation or done via the decorator pattern (no strong opinion here).AsMessage
hardcode themessage
group (or not, we also may allow the user to change it, but it be the default).- The messenger itself must know the map, in order to expose the
type
header in sent enveloppes.
Following this future plan, registry interface would evolve to:
interface ClassAliasRegistry
{
public function hasAliasFor(string $className, string $group = 'default'): bool;
// Returns the class name if nothing found, always returns primary alias.
public function getAlias(string $className, string $group = 'default'): string;
// Returns as list of ordered aliases.
public function getAllAliases(string $className, string $group = 'default'): string;
public function hasClassNameFor(string $alias, string $group = 'default'): bool;
// Raise exception if no alias, return $alias if is a valid class name.
public function getClassName(string $alias, string $group = 'default'): string;
}
A new trait could be added in the config component for registering a static map:
trait ClassAliasMapConfigurationTrait
{
public function defineClassAliasMap(TreeBuilder $node): void
{
// ...create the class map definition
}
}
A new trait would be added in the dependency injection namespace for using discovery:
trait ClassAliasMapExtensionTrait
{
// $namespace is the PSR-4 prefix
// $pattern is the directory with glob patterns
private function discoverClassAliasMap(string $namespace, string $pattern): array
{
// run the generic finder method
}
// variadic $maps so it could merge from config and from discovery
private function registerClassAliasMap(string $group, array ...$maps): void
{
// 1. does validation (class exists, etc...)
// 2. add the definitions into the registry service argument
}
// another variant, do it all at once
private function discoverAndRegisterClassAliasMap(string $group, string $namespace, string $pattern, array ...$maps): void
{
// all of the above in one single function
}
}
And thus, in the messenger extension, use all of this.
And the circle is complete:
- Messenger feature is solved.
- Generic type alias can be re-used for other future use cases in Symfony.
- Even more generic discovery tooling is publicly exposed for standalone component users.
Remaining questions are:
- The class discovery algorithm should be a new component, or should live in an existing one (finder is for files mostly, but I can't see where it would better belong to).
- Are you OK with plugging this per default in serializer?
- All your questions.
What are your opinions of this?
Example
No response