With PHP 8 and having attributes support, configuring beans can be even more simpler. I don't expect to have a complete picture of what's needed, since I have only experience with discolight where I did this proposal (sort of). Anyway here's my take:
There are some options to walk this path:
- Add support to the 0.x major versioning line
- Replace configuration by annotation with attributes which of course would require a major version release: 1.x
I can't think of a good reason to go for option 2. Adding it to 0.x allows for a gradual upgrade path allowing to deprecate features which can eventually be removed in 1.x.
Adding this feature can be done by:
- add support to current Annotation classes, or
- creating new Attribute classes.
While new Attribute classes allows you to start with a clean slate, current Annotation classes can quite easily be adapted in order to be used as Attribute classes as well.
As option 1 is in my opinion the most valuable approach I'll go a bit further on that:
Add support to current Annotation classes
Example of how that can be achieved in case of the Alias class:
/**
* @Annotation
* @Target({"ANNOTATION"})
* @Attributes({
* @Attribute("name", type = "string"),
* @Attribute("type", type = "bool"),
* })
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class Alias
{
/**
* @var string
*/
private $name;
/**
* @var bool
*/
private $type;
/**
* Creates a new {@link \bitExpert\Disco\Annotations\Bean\Alias}.
*
* @param array $attributes
* @throws AnnotationException
*/
public function __construct(array $attributes = [], ?string $name = null, bool $type = false)
{
// When configured by annotations the $attributes['value'] is not empty, so
// in that case override the $name and $type variables from the $attributes['value'] array.
if (!empty($attributes['value'])) {
$attributes = $attributes['value'];
// Assign local variables the value from $attributes meanwhile making sure the keys exist
// with at least the default value as defined in the constructor.
['name' => $name, 'type' => $type] = $attributes + ['name' => $name, 'type' => $type];
}
if (!is_bool($type)) {
$type = AnnotationAttributeParser::parseBooleanValue($type);
}
if ($type && $name) {
throw new AnnotationException('Type alias should not have a name!');
}
if (!$type && !$name) {
throw new AnnotationException('Alias should either be a named alias or a type alias!');
}
$this->type = $type;
$this->name = $name;
}
public function getName(): ?string
{
return $this->name;
}
public function isTypeAlias(): bool
{
return $this->type;
}
}
class Config
{
/**
* Using named arguments
*/
#[Bean]
#[Alias(type: true)]
#[Alias(name: 'my.service.id')]
public function myService(): Service
{}
/**
* Using ordered arguments
*/
#[Bean]
#[Alias([], null, true)]
#[Alias([], 'my.service.id')]
public function myOtherService(): OtherService
{}
}
As you can see named arguments allow for a clean usage of existing Annotation classes.
What about nested attributes?
In PHP's current implementation of attributes nesting is not supported, so explicitly configuring needs to be done on the same level, or instances of nested attributes/annotations should be able to be configured with scalars and instantiated in the "parent" attribute instance.
class Config {
#[Bean(aliases: [
['type' => true],
['name' => 'my.service.id'],
])]
public function myService(): Service
{}
}
What does this means for each type of annotation?
Alias
Allow configuration of
Bean
Allow configuration of
- scope
- singleton
- lazy
- aliases discouraged, use (multiple) Alias attributes instead
- parameters discouraged, use (multiple) Parameter attributes instead
BeanPostProcessor
Allow configuration of
- parameters discouraged, use (multiple) Parameter attributes instead
Configuration
Just an identifier attribute
Parameter
The ordering of parameters is important since the method is called with the parameters in the order they are configured. Nesting the parameters inside the Bean annotation provides a natural way of respecting the ordering of the arguments in the methods signature.
But requiring attributes to be listed in a specific order feels a bit like a smell to me.
class Config {
#[Bean(parameters: [
['name' => 'config.key1', 'default' => 'default value'],
['name' => 'config.key2', 'default' => 'other default value'],
])]
public function myService($key1, $key2): Service
{}
// Correct ordering of the parameter attributes required, NOT IDEAL
#[Bean]
#[Parameter(name: 'config.key1', default => 'default value')]
#[Parameter(name: 'config.key2', default => 'other default value')]
public function myOtherService($key1, $key2): OtherService
{}
}
Possible solution: When using the Parameter attribute, require a config key with the name of the argument so it can be used for calling the method with named arguments. The nicest would be something like this:
$parameters = [
'config' => [
'key1' => 'value of key1',
'key2' => 'value of key2',
],
];
class Config {
#[Bean]
#[Parameter(name: 'arg2', key: 'config.key2', default => 'other default value')]
#[Parameter(name: 'arg1', key: 'config.key1', default => 'default value')]
public function myService($arg1, $arg2): Service
{
// $arg1 = 'value of key1;
// $arg2 = 'value of key2;
}
}
Unfortunately name is already in use as the name of the parameter key, but i.e. argname can perhaps be a good alternative.
How to incorporate this with the ConfigurationGenerator?
The doctrine AnnotationReader can be swapped out for a FallbackAttributeReader. The FallbackAttributeReader would prioritize attributes over Annotations.
I would not recommend mixing annotations and attributes on the same level.
Possible guidelines:
- When the Bean attribute is found on a method ignore any annotations.
- When the Bean attribute is found on a method look for Alias and Parameter attributes.
- When the Bean attribute has nested aliases prioritize these over configured Alias attributes.
- When the Bean attribute has nested parameters prioritize these over configured Parameter attributes.
- When the BeanPostProcessor attribute is found on a method ignore any annotations.
- When the Configuration attribute is found on a method ignore any annotations.