Skip to content

Service fetched by interface has type of its concrete implementation alias, which is against LSP #401

Closed
@Wirone

Description

@Wirone

We encountered issue today (PHPStan 1.11.6 with bleeding edge + Symfony extension 1.4.5) that I consider violating the Liskov Substitution Principle. Consider simplified code:

services:
  # Interface
  Foo\ClientFactory: '@Foo\ApplicationClientFactory'

  # Implementation
  Foo\ApplicationClientFactory: ~

then, when we do something like:

/** @var \Foo\ClientFactory $factory */
$factory = $container->get(\Foo\ClientFactory::class);

we get an error:

PHPDoc tag @var with type Foo\ClientFactory is not subtype of type Foo\ApplicationClientFactory.

which does not make sense. We fetch ClientFactory from DI container, it's aliased to ApplicationClientFactory implementation that satisfies the interface, so of course interface is not a subtype, it's super type.

I don't want more specific type here, as the whole idea of interface is interchangeability. In the code, when I fetch ClientFactory (interface) I don't care what implementation comes from DI container, I only want this particular contract, because I don't want to encounter problems when DI alias is changed to other implementation. In our case ApplicationClientFactory has more methods that implemented ClientFactory interface, I don't want developers to be able to use these methods because PHPStan allows it (as it knows the aliased service), rather the other way around - I would expect that static analysis reports usage of methods not defined in the interface.

I believe it's a bug and PHPStan should not narrow the type here. Similar here, I believe I should be allowed to restrict the contract to interface instead of relying on extended implementation.

FYI: we don't use @var when we don't need, in this case it's only for IDE which does not have autocompletion for ClientFactory fetched from the container (since get()'s return type is ?object). We use dependency injection mostly, but unfortunately in legacy part of the code there is a lot of direct interaction with DI container.

Originally posted by @Wirone in #350 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions