Skip to content

ServiceSubscriberTrait doesn't fully support PHP8 union types #43913

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

Closed
gmichard opened this issue Nov 3, 2021 · 7 comments
Closed

ServiceSubscriberTrait doesn't fully support PHP8 union types #43913

gmichard opened this issue Nov 3, 2021 · 7 comments

Comments

@gmichard
Copy link

gmichard commented Nov 3, 2021

Symfony version(s) affected

5.3.10

Description

An error occurs when using the Service Subscriber Trait on a service class that have a method with more than one type (i.e. union type) for return type.

Call to undefined method ReflectionUnionType::isBuiltin() at /vendor/symfony/service-contracts/ServiceSubscriberTrait.php:45

How to reproduce

  1. Create a brand new Symfony Application (download documentation)
    symfony new --full my_project
  2. Run the application
  3. Add a new service that use the ServiceSubscriberTrait and that have a method with more that one type for return type. See the following ExampleHandler class.
  4. Clear your cache, run the symfony bin/console
<?php
// File: src/Service/ExampleHandler.php

namespace App\Service;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;

class ExampleHandler implements ServiceSubscriberInterface
{
    use ServiceSubscriberTrait;

    protected function getSomethingOrOther(): UserAuthenticatorInterface|UserInterface|null
    {
        // No body. Return null for example. Only the return type is important for this issue.
        return null;
    }
}

With this example, if I add a xDebug breakpoint on the line 45 of ServiceSubscriberTrait
with the condition of suspension:

self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !method_exists($returnType, 'isBuiltin')

(Note the !method_exists($returnType, 'isBuiltin'))

I can check the values of $method and $returnType

// xDebug evaluation of two variables on line 45:

$method = ReflectionMethod::__set_state(array(
   'name' => 'getSomethingOrOther',
   'class' => 'App\\Service\\ExampleHandler',
));
// The method "getSomethingOrOther" is the cause of the error.

$returnType = ReflectionUnionType::__set_state(array(
))
// The return type is an instance of `ReflectionUnionType`
// which have no method called "isBuiltin".

Possible Solution

  • We can see in the PHP ReflectionType class "Changelog" section that:

    (Version 8.0.0) ReflectionType has become abstract and ReflectionType::isBuiltin() has been moved to ReflectionNamedType::isBuiltin().

  • We can see that the issue [3.4] Fix support for PHP8 union types #37340 by @nicolas-grekas mentions the "PHP8 Union types" and the isBuilin method.
    But the modifications doesn't seem to fix this use case.

    The reason is that these might return a ReflectionUnionType now, which has no getName() method (same for `isBuiltin() btw.)

Maybe a test is missing before calling the isBuiltin (like $returnType instanceof ReflectionNamedType), but I'm not sure about the expections here and the eventual side-effects.

Additional Context

Error trace in console.

Symfony\Component\ErrorHandler\Error\UndefinedMethodError^ {#25
  #message: "Attempted to call an undefined method named "isBuiltin" of class "ReflectionUnionType"."
  #code: 0
  #file: "./vendor/symfony/contracts/Service/ServiceSubscriberTrait.php"
  #line: 45
  trace: {
    ./vendor/symfony/contracts/Service/ServiceSubscriberTrait.php:45 { …}
    ./vendor/symfony/dependency-injection/Compiler/RegisterServiceSubscribersPass.php:74 { …}
    ./vendor/symfony/dependency-injection/Compiler/AbstractRecursivePass.php:81 { …}
    ./vendor/symfony/dependency-injection/Compiler/RegisterServiceSubscribersPass.php:36 { …}
    ./vendor/symfony/dependency-injection/Compiler/AbstractRecursivePass.php:46 { …}
    ./vendor/symfony/dependency-injection/Compiler/Compiler.php:91 { …}
    ./vendor/symfony/dependency-injection/ContainerBuilder.php:749 { …}
    ./vendor/symfony/http-kernel/Kernel.php:545 { …}
    ./vendor/symfony/http-kernel/Kernel.php:786 { …}
    ./vendor/symfony/http-kernel/Kernel.php:125 { …}
    ./src/Kernel.php:19 {
      App\Kernel->boot()^
      › {
      ›     parent::boot();
      › }
    }
    ./vendor/symfony/framework-bundle/Console/Application.php:168 { …}
    ./vendor/symfony/framework-bundle/Console/Application.php:74 { …}
    ./vendor/symfony/console/Application.php:167 { …}
    ./bin/console:43 { …}
  }
}
@nicolas-grekas
Copy link
Member

/cc @kbond :)
Note that wiring union|intersection types might very well belong to AutowirePass (reordering and filtering what TypeReference::getType() returns to resolve them there.

@kbond
Copy link
Member

kbond commented Nov 3, 2021

@gmichard, to clarify, are you trying to "autowire" getSomethingOrOther?

@nicolas-grekas can you confirm that union types are not supported by ServiceSubscriberInterface? I never got a clear answer here: #42238 (comment) (quick test in 5.4 shows me no)

I'm thinking to skip methods without \ReflectionNamedType in 4.4+ and in 5.4+, when using the SubscribedService trait, throw an exception. At least until a time that ServiceSubscriberInterface supports union/intersection services.

@nicolas-grekas
Copy link
Member

can you confirm that union types are not supported by ServiceSubscriberInterface

it's never been specified, but it's trivial to define them so I think we should consider them as defined actually:
?Foo|Bar / ?Foo&Bar.

the leading ? is needed as a flag to express that a key is optional.
Then AutowirePass could parse those, remove built-in types, order, and relying the existing logic to autowire

@kbond
Copy link
Member

kbond commented Nov 3, 2021

Ok, this will be in 5.4+ only, correct?

In 4.4+ and 5.4's deprecation layer I'm thinking we should ignore union/intersection types.

@gmichard
Copy link
Author

gmichard commented Nov 3, 2021

to clarify, are you trying to "autowire" getSomethingOrOther?

@kbond

  • autowire is enabled (autoconfigure is enabled too, for what it's worth)
  • But this method is not a really a service location. It's a regular method for internal usage, with no argument and with many return types.

A more complete example could be:

<?php
// File: src/Service/ExampleHandler.php

// use [...]

class ExampleHandler implements ServiceSubscriberInterface
{
    use ServiceSubscriberTrait;

    /**
     * A regular method that return a object type or another.
     * 
     * This method doesn't need to use the service locator.
     */
    private function getSomethingOrOther(): SomethingInterface|Other
    {
        /** @var $something SomethingInterface|null */
        $something = $this->somethingProvider()->load();

        if (null !== $something) {
            return $something;
        }

        return new Other();
    }

    /**
     * A Service Locator (thanks to the autowiring and the ServiceSubscriberTrait)
     */
    private function somethingProvider(): SomethingProviderInterface
    {
        return $this->container->get(__METHOD__);
    }
}

The both methods matches the criteria to add it as subscribed services (non-static, non-internal, etc.).
Perhaps there is a way to indicate that a method is not a service locator (and that its return type doesn't need to be added to the subscribed services array)?

I hope this words are clarifying and not confusing.
(And really really thank you for all your work and your reactivity!)

@nicolas-grekas
Copy link
Member

For 5.4+ yes @kbond

@kbond
Copy link
Member

kbond commented Nov 3, 2021

@gmichard Ok, that's what I thought. Keep in mind that you'll also have trouble with non-union return types (ie getSomethingOrOther(): UserInterface). #42238 fixes but only on 5.4 and when using the new attribute. Really, this issue would be fixed by #42238 as well - you wouldn't put the attribute on getSomethingOrOther().

@nicolas-grekas, #43922 provides a fix for this specific issue on 4.4 - do you not want to fix on that branch?

nicolas-grekas added a commit that referenced this issue Nov 4, 2021
… `ServiceSubscriberTrait` (kbond)

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

Discussion
----------

[DependencyInjection] only allow `ReflectionNamedType` for `ServiceSubscriberTrait`

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #43913
| License       | MIT
| Doc PR        | n/a

I'll follow this up with a PR on 5.4 to allow union/intersections when using the `SubscribedService` attribute (once `ServiceSubscriberInterface` [supports this](#43913 (comment))).

Commits
-------

b616042 [DependencyInjection] only allow `ReflectionNamedType` for `ServiceSubscriberTrait`
nicolas-grekas added a commit that referenced this issue Nov 6, 2021
… together with `ServiceSubscriberInterface` (kbond)

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

Discussion
----------

[DependencyInjection] Fix support for unions/intersections together with `ServiceSubscriberInterface`

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Continuation of #43479 per [discussion](#43913 (comment)) with `@nicolas`-grekas.

Todo:
- [x] intersection type support/tests
- [x] `ServiceSubscriberTrait` support

Commits
-------

123842a [DependencyInjection] Fix support for unions/intersections together with `ServiceSubscriberInterface`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants