-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Add better return type on ContainerInterface::get()
#60096
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
Conversation
* @psalm-return (B is self::EXCEPTION_ON_INVALID_REFERENCE|self::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE ? object : object|null) | ||
* @psalm-return ($id is class-string<A> ? (B is self::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE|self::EXCEPTION_ON_INVALID_REFERENCE ? A : A|null) : (B is self::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE|self::EXCEPTION_ON_INVALID_REFERENCE ? object : object|null)) | ||
* | ||
* @return ($id is class-string<A> ? (B is 0|1 ? A : A|null) : (B is 0|1 ? object : object|null)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you try with phpstan-return?
src/Symfony/Component/DependencyInjection/ContainerInterface.php
Outdated
Show resolved
Hide resolved
There is strictly no guarantee in the ContainerInterface that a service class is a subtype of the object id, even when the object id is a class-string. This PR adds this in the contract of the interface, which is a BC break to me (btw, it might force us to forbid some kind of service decoration to ensure this is true) |
src/Symfony/Component/DependencyInjection/ContainerInterface.php
Outdated
Show resolved
Hide resolved
I agree with @stof here. We decided against using PHPdoc types that do not represent the 100% use-case, even if it improves the experience for 95% There is no guarantee that there is a direct relation between service ID and object type, even though it's a common convention that there is. So 👎 from me. |
Indeed, but I thought it would be okay since it's only PHPDoc. But since it increase the DX a lot in test env, I thought it would be okay. If we add a sentence, to explain is not 100% accurate, would it be okay? |
Our phpdoc in our interfaces is considered as part of the defined contract (it is taken into account when deciding whether the change we do is a BC break or no)
still -1 to me, because of the previous point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sympathetic to the change.
The DX improvement is way more important to me than covering a meta programming edge case. Better than that: I'm fine if ppl that use a FQCN identifier without returning a compatible instance get a SA warning. They'd rely on confusing conventions.
No BC break here, this has no runtime effect.
okay, let's close it then |
Oh, there was a race condition here. I close the PR at the same time of last comment from @nicolas-grekas. Let's re-open |
They would not. they might get a runtime error because the runtime type does not match what the SA thinks the type was. Static analysis won't report errors on the service definition file if not respecting that rule (as it does not even analyze those XML/Yaml files) and the original service definition might even respect the rule without the final one respecting it. |
I don't think your interpretation of the BC policy is the one we're really sticking to. At least I don't agree with this argument when considering the current proposal. To me its totally fine and desired I'd say.
This is reverse thinking: the current declaration doesn't provide anything like that either, so the new one won't provide more guarantees. The DX improvement is obvious, the SA downside is none in practice. |
The new phpdoc explicitly says "if the id looks like a class-string, the method MUST return an object of that type (or null)". That's definitely a stricter contract for the interface than saying "the method MUST return an object (or null)". And phpdoc types have always been part of our BC policy (otherwise, adding native types would have been a BC break for callers of our methods due to narrowing the parameter types) |
The BC policy is designed to allow people to upgrade seamlessly. Seamlessly "means without us breaking their app". Yes we consider types in phpdocs, as guides toward what should be supported. We also change them at will when needed, provided this doesn't create any execution-time side effect - which is the core criteria when BC is concerned. So to me, DX clearly wins here. |
@nicolas-grekas interface implementations have to respect the contract defined in the interface. That's where the BC break is when making the interface stricter (and with this PR, we would end up allowing to instantiate a Container that does not respect ContainerInterface depending on how services are configured) |
The "allowing" part is not our business, same as one can wire a logger into a service argument that needs a serializer: "PHP" will throw, not "us". People are free to run whatever works for them. Declaring the intent is enough on our side, and will always be. Then no, no BC break here, because no exec-time side effect... |
Implementations of this interface will get static analysis errors if they don't guarantee a correct return object type if a class FQCN is passed. That's a regression from the current state. Still, I don't think we consider static analysis errors a BC break (otherwise, any additional PHPdoc type is a BC break as it could lead to new static analysis errors). However, this does change how we treat PHPdoc types. If we think this is OK, there are more types that we welcome from the phpstan/psalm stubs. We rejected these in the past, as they didn't represent the 100% truth (#47016). E.g. the stubbed form types which are a bigger elephpant in the room. |
@nicolas-grekas the big difference is that in this case, the class that won't respect its interface is our class. So the faulty code would be ours. |
We're not responsible for how people configure their containers, really. |
Adding a decorator on a service that was registered using its class name as id (the default convention) is enough to make the service invalid regarding this new rule (the dev intent is probably to decorate based on the interface of the class rather than the class itself). And this does not require weird wiring of the container, just using one of its flagship features. |
Sure, but still: the decorator has to act like the real objects, so the API will still be the same. SA tools won't be able to notice anyway, too many indirections. And if they do, ppl can decorate the common interface that the decorated and decorator must share. |
@nicolas-grekas if the code is written against the common interface, things indeed work. But SA will tell you that the type you get is When installing the phpstan-symfony extension, phpstan will infer the type of |
That's a very good point. All static analysers are using our XML dump of the container, which we make specifically for this use-case: mapping magic service ID strings to PHP object information. |
I don't know about the extension, but is the extension able to cope with scoped containers? On the other end, scoped containers are a thing ppl can use at will. Yet how are they supported by the extension, DX wise? @lyrixx maybe you'd have some hints on these questions? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 on my side. Thanks for the discussion, it's insightful.
My reasoning:
- this improves DX for sure
- the potential inaccuracy when decorators are used happens only when people don't use IoC, so that's their problem and there is a clear path forward: SOLID principles FTW
- the potential inaccuracy when decorators are used doesn't impact either DX nor static analyzers since one should still use the returned object as if it were an instance of FQCN
- for static analyzers that can properly understand how a container is configured (I've yet to see this happen), they might report issues. Yet the answer is the same as above: use IoC to fix the warning.
DX should be first class here IMHO.
@nicolas-grekas you seem to say that using a concrete class name as service id breaks IOC. but that's the default behavior of Symfony. |
When using AutowireLocator you mean, sure. Still, it's a matter of vision. Your reasoning makes sense in a world where types are reified. Mine works in an erased types world. I prefer erased types: they're good enough to solve the purpose of types in a language like PHP. |
src/Symfony/Component/DependencyInjection/ContainerInterface.php
Outdated
Show resolved
Hide resolved
I'm 👎 on this for the same reasons as stof and wouter (3 downvotes). This takes a too big shortcut for a too small benefit as this only matters in tests. Not worth the hack/wrong assumption IMHO |
What do you think about changing phpdoc to rely on interfaces then? https://psalm.dev/r/2c3a6cac5a I don't know whether IDE will handle this properly, and why for now Psalm resolves class-string as interface-string too, but this will be the most safe approarch in my opinion <?php
interface Container
{
/**
* @template T
* @template V
*
* @param T $id
*
* @return T is interface-string<V> ? V : mixed
*/
public function get(string $id): mixed;
}
$container = container();
/** @psalm-trace $interfaceInstance */
$interfaceInstance = $container->get(\ArrayAccess::class); // expected - `\ArrayAccess`
/** @psalm-trace $classInstance */
$classInstance = $container->get(\stdClass::class); // expected - `mixed`
function container(): Container
{
throw new \InvalidArgumentException('Not necessary');
} |
You missed Service Locator |
@lyrixx I rather use the lowest possible abstraction which is the PSR interface, as in the docs and every locator of our codebase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this? Would that improve the DX? From a type PoV, it's accurate.
@return ($id is class-string<C> ? (B is 0|1 ? C|object : C|object|null) : (B is 0|1 ? object : object|null))
I don't think |
I think C|object is the answer to @lyrixx previous comment:
C|object does |
I updated the PR with @nicolas-grekas proposal. It seems good. Now I have autocomplete, and we don't mess with SA |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
Thank you all for the discussion! |
Thank you @lyrixx. |
See https://phpstan.org/r/a58736e6-f181-4bea-ad14-134f55229664