Skip to content

[DI][DX] Optional class for named services DX issue #21380

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
ogizanagi opened this issue Jan 23, 2017 · 9 comments
Closed

[DI][DX] Optional class for named services DX issue #21380

ogizanagi opened this issue Jan 23, 2017 · 9 comments

Comments

@ogizanagi
Copy link
Contributor

ogizanagi commented Jan 23, 2017

Q A
Bug report? yesish
Feature request? no
BC Break report? not really
RFC? no
Symfony version 3.3

Since #21133, the class attribute isn't required anymore when using a classname as service id:

services:
    Vendor\Namespace\Class: ~

However, due to the current implementation, the following is considered valid too:

services:
    bar:
      arguments: ['foo']

The bar key is considered valid, and will be used as the Definition class, despite there is no bar class in the project.
Which means no compile-time exception. You'll only get:

Fatal error: Uncaught Symfony\Component\Debug\Exception\ClassNotFoundException: Attempted to load class "bar" from the global namespace.
Did you forget a "use" statement?

at runtime, when trying to use the service.
Previously, you got an error at compilation time:

Fatal error: Uncaught Symfony\Component\DependencyInjection\Exception\RuntimeException: The definition for "bar" has no class.

I think it's a DX regression, especially for someone not being aware of this behavior.

But the fact is that, looking at the tests, it seems expected.
In #20943 (comment), I suggested to enforce a leading backslash for global namespace classes, ie:

services:
    \bar: ~

but that's not something to consider, given one of the main benefits of declaring a service this way, is to call it using $container->get(\bar::class). Which doesn't resolves to a FQCN with a leading backslash.
So, unless we move the service id as definition class logic inside each loaders, it's probably a no-go...

First question: Should we care?

Then, what are the possible solutions ?

  1. Enforce a leading backslash from file loaders but keeping the ability to do
    $container->register(\ExistingClassFromTheGlobalNamespace::class)
    which means moving the logic to yaml and xml file loaders. Which creates an inconsistent behavior between formats and probably gives wrong responsibilities to the loaders compared to the compiler pass.
  2. Do not accept classes from the global namespace, or at least only accept classes from the global namespace starting with an uppercase char.
    In my experience, most services ids are lowercased (despite the fact case doesn't matter until [DI] Deprecate case insentivity of service identifiers #21193), whereas classnames are mainly capitalized (despite there is no recommendation about it in PSR-0/PSR-4 AFAIK), so it'll prevent WTF moments for a majority and looks sensible to me.
  3. Anything else?
@nicolas-grekas
Copy link
Member

IMHO, there are two no-gos:

  • using a convention to "sniff" what "looks enough like" a class or not;
  • using class_exists, because that would slow down the creation of the compiler, and more importantly make it context-sensitive.

Which means I'm leaning towards answering "no, we can't" to your "Should we care?" question.

That being said, what about catching the Error that comes up when a class is missing, and turn it into some friendlier error message when we find that the missing class has exactly the same name than the service id that we were asking for?

@ogizanagi
Copy link
Contributor Author

what about catching the Error that comes up when a class is missing, and turn it into some friendlier error message when we find that the missing class has exactly the same name than the service id that we were asking for?

It'll indeed be a step forward, but how would you proceed in order to achieve that? 😕

The two possibilities I see are:

  1. Catch the \Error from Container::get() and parse the message to see if it's a class not found. This also requires updating the PhpDumper to wrap any method instantiating a service in the same try/catch block (can be done through a proxy method though to avoid cluttering the generated class).
  2. Analyze the backtrace from ClassNotFoundFatalErrorHandler to find out if we were trying to call a service with this id when the class not found error occurred.

Both solutions looks complex for this very specific DX regression, and moreover are still runtime exceptions, while it used to throw at compil time before :/

@theofidry
Copy link
Contributor

@ogizanagi what about adding a leading backslash and removing it when registering the service? i.e. forcing:

services:
    \bar: ~

but behind the scene remove the leading backslash so that $container->get(\bar::class) still works.

Alternatively we could make the class mandatory if no backslash if found at all in the service name:

services:
    bar: ~ # throw an error as not no `class` property has been found an `bar` is ambigus`

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Jan 29, 2017

Thanks @theofidry for these suggestions.

what about adding a leading backslash and removing it when registering the service

Could be a solution. However, as mentioned earlier, I think we should keep the ability to do:

$container->register(\ExistingClassFromTheGlobalNamespace::class)

using the PHP format. So either we don't support the above, either that'll require moving the logic from passes to file loaders :/
Also, I don't really like transforming the service id behind the scene.

Alternatively we could make the class mandatory if no backslash if found at all in the service name

Yes, I think it's better to consider that registering a class from the global namespace this way is a limitation and throw. But I'm afraid everyone will disagree. 😄

@theofidry
Copy link
Contributor

I also prefer the second option, less magics and I think it's an reasonable limitation.

@nicolas-grekas
Copy link
Member

I fact, why not, with one more step: let's not map id to class when no namespace is found.
@ogizanagi I think you can try a PR doing that - enforcing the "class" attribute for global classes.
WDYT?

@ogizanagi
Copy link
Contributor Author

Sure, I'll do it.

@theofidry
Copy link
Contributor

theofidry commented Jan 29, 2017 via email

@ogizanagi
Copy link
Contributor Author

@theofidry : It's already opened 😄 See #21453.
Thanks for your involvement. :)

fabpot added a commit that referenced this issue Jan 29, 2017
…anagi)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI][DX] Do not map id to class for global classes

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yesish
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #21380
| License       | MIT
| Doc PR        | N/A

Using a global classname as service id without specifying the definition class attribute won't work anymore after this, in the benefit of properly throwing an exception at compilation time for a misconfigured service. Service ids could previously be wrongly interpreted as a class name.

So:
```yml
services:
    app_bar:
        arguments: ['foo']
```

will now properly result into:
> Fatal error: Uncaught Symfony\Component\DependencyInjection\Exception\RuntimeException: The definition for "app_bar" has no class.

at compilation time.

Commits
-------

bb87030 [DI][DX] Do not map id to class for global classes
@fabpot fabpot closed this as completed Jan 29, 2017
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

5 participants