disco icon indicating copy to clipboard operation
disco copied to clipboard

Rewrite using native Attributes instead of annotations.

Open zluiten opened this issue 3 years ago • 14 comments

This is an implementation of #131

Changes

Bean

#[Configuration]
class X
{
    #[Bean(
        singleton: true, 
        lazy: true, 
        scope: Bean::SCOPE_SESSION
    )]
    public function serviceDefinition(): Service {}
}

Use the Bean attribute to define a bean and set their singleton, lazy or scope option. Use the Parameter and Alias attribute to define parameters and aliases.

Alias

#[Configuration]
class X
{
    #[Bean]
    #[Alias(name: 'my.service.id')]
    #[TypeAlias]
    public function serviceDefinition(): SomeService { /* .... */ }
}

The purpose of the Alias attribute is to define a named alias. The name must be at least 1 character of length. To define the return type as an alias use #[TypeAlias]. Multiple Alias attribute are allowed, only 1 TypeAlias. Both Alias and TypeAlias only affect Bean definitions.

BeanPostProcessor

#[Configuration]
class X
{
    #[BeanPostProcessor]
    public function beanPostProcessor(): MyPostProcessor { /* .... */ }
}

To define a bean post processor apply #[BeanPostProcessor] attribute. Add #[Parameter] attributes to define parameters.

Parameter

#[Configuration]
class X
{
    #[BeanPostProcessor]
    #[Parameter(
        key: 'configKey',
        name: 'argName',
        default: 'some default value',
        required: false,
    )]
    public function beanPostProcessor(string $argName): MyPostProcessor { /* .... */ }

    #[Bean]
    #[Parameter(
        key: 'configKey',
        name: 'secondArg',
        default: 'some default value',
        required: false,
    )]
    #[Parameter(key: 'otherConfigKey', name: 'secondArg')]
    public function serviceDefinition(string $configValue, int $secondArg): MyPostProcessor { /* .... */ }
}

Attribute a Bean or BeanPostProcessor with 1 or multiple Parameter attributes. key used to look up the value in the configuration array. :warning: This is the equivalent of name in 0.x version. name has now a different purpose: name corresponds with the name of argument of the method definition. default and required act the same as in 0.x.

Disco will use the feature named arguments of PHP 8, to pass the arguments to the method call.

About positional parameters

At this moment the name option is not mandatory. When omitted the Parameter becomes a positional argument. Disco will pass positional arguments to the method function followed by the named arguments as required by PHP, see Example #16.

The mutual order of the positional parameters is of course important.

The base line is that the same rules and restrictions apply as when using and mixing named and positional arguments when calling the method yourself. Read more about it the PHP docs.

To be discussed

Builtin types as return type alias

In 0.x it was possible to define the return type alias when the actual return type was a builtin type like string, int, float, bool, array, object, callable or iterable. This is now not possible anymore. An exception will be thrown. An example of an invalid configuration:

#[Configuration]
class X
{
    #[Bean]
    #[TypeAlias]
    public function serviceDefinition(): callable { /* .... */ }
}

Parameter validation

At compile time, should we validate configured parameters with the method signature? Possible validations:

  • name option van the Parameter should correspond with the name of one of the arguments
  • multiple Parameters with the same name should not be allowed
  • a bit more advanced: named Parameter should not correspond with an argument that's covered by a positional Parameter
  • when argument in the method signature has no default value, the parameter should have a default value or be required...

Anything else?

zluiten avatar Aug 16 '21 18:08 zluiten

Cool!

shochdoerfer avatar Aug 16 '21 19:08 shochdoerfer

@netiul if it makes things easier, I'd be fine supporting only named parameters.

shochdoerfer avatar Aug 17 '21 14:08 shochdoerfer

Coverage Status

Coverage decreased (-2.3%) to 97.739% when pulling 48d452baa7d80c613dac2b431249a73371d1a843 on netiul:php8-rewrite into c3659ff55bb610cffb3a41fd5146d2d64fdf658f on bitExpert:master.

coveralls avatar Aug 17 '21 14:08 coveralls

@shochdoerfer It's actually not very hard to support both. This is basically all that's needed, but it might be a bit confusing in userland to have both ways availabe...

zluiten avatar Aug 17 '21 14:08 zluiten

@shochdoerfer I've have updated the top post with a summary of the main changes and some points up for discussing.

zluiten avatar Aug 18 '21 08:08 zluiten

@netiul awesome work! Thanks a lot for tackling this.

Not allowing built-in types as return type alias makes sense to me. That seems to be a "bug" in the previous versions that I missed.

Parameter validation also makes sense to me. I would even go that far and get rid of the positional logic and "just" keep the named parameter logic to reduce potential problems when parameters are re-ordered any types don't match anymore. Since PHP 8 gives us the ability to use named parameters it makes sense to make use of them. Not that I like them in code but for the configuration that sounds fine. Given that, checking that parameter names exist and are unique in the attribute configuration makes absolute sense. I also like the idea of the no default value check.

@heiglandreas any other validations, you would like to see? I hear you are big Disco fan, so may have opinions :)

shochdoerfer avatar Aug 21 '21 17:08 shochdoerfer

For sure butwon't be able to voice them for another week. So if you don't want to wait that's fine with me 😉

heiglandreas avatar Aug 21 '21 18:08 heiglandreas

@heiglandreas have you had a chance to think about this?

shochdoerfer avatar Sep 05 '21 19:09 shochdoerfer

For me the whole Parameter declaration is not 💯% clear as most of it seems to duplicate internal functionality.

default and require are already declarable via the function parameter declaration (OK: Default only via native types) and can be retrieved via Reflection. I would not require people to declare things in duplicate, once in the parameter-definition and once in the function signature, as that will lead to discrepancies. And I can't see why one would declare a different default value via the function signature and the parameter declaration. Same applies for whether a parameter is required or not.

So for me the only declaration that introduces something, that can not be declared via the function signature, would be the key parameter, that defines a configuration key whose value will be passed as parameter. Using named parameters here makes absolutely sense to me. Validating that all required parameters are set via a Parameter-annotation would then be the only thing left to do.

I would also only go for named parameters only. The key name has to be mandatory IMO.

When there are multiple parameter declarations for the same name I would use the "last" one to win and raise a warning.

Question: Can we use aliases from the DI within the configuration?

$testConfig = [
  'db' => '@sqlite',
];
$prodConfig = [
  'db' => '@postgresql',
];
#[Configuration]
class MyConfiguration
{
    #[Parameter(
        name: 'db',
        key: 'db',
    )]
    public function mySampleService(PDO $db) : SampleService
    {
        $service = new SampleService();
        $service->setDatabase($test);
        return $service;
    }

    #[Bean]
    #[Alias(name: 'postgresql')]
    public function productionDatabase(): PDO
    {
        // Define Postgresql service
    }

    #[Bean]
    #[Alias(name: 'sqlite')]
    public function testDatabase(): PDO
    {
        // Define SQLite-Service
    }
}

heiglandreas avatar Sep 06 '21 05:09 heiglandreas

And I'm absolutely in favour of not allowing native types as type-aliases.

heiglandreas avatar Sep 06 '21 05:09 heiglandreas

@netiul does the feedback make sense to you? :)

shochdoerfer avatar Sep 12 '21 16:09 shochdoerfer

Yes! Sorry for the late reply :)

Thank for the feedback @heiglandreas. It makes totally sense to use the functions signature to declare defaults and what parameter is required. Funny how one can be easily fixated on an existing path :laughing:

About "last parameter with the same name wins", shouldn't we just hard fail at compile time? It's not really something that can only be detected at runtime.

zluiten avatar Sep 13 '21 17:09 zluiten

Yeah. failing at compile time makes more sense. There can only be one name entry. If there are more, that needs to be fixed.

heiglandreas avatar Sep 13 '21 20:09 heiglandreas

I will process the feedback shortly. I am currently stuck in work.

zluiten avatar Sep 30 '21 09:09 zluiten