skip to content

Why does PSR-11 advise against injecting ContainerInterface into a class so it can fetch its own dependencies, and when is injecting the container legitimate?

level: seniorimportance: must knowfreq 50%

answer

  1. PSR-11 section 1.3, recommended usage
  2. hidden dependencies, harder tests
  3. forces entry names like 'db'
  4. router computes the entry name
  5. factory behind its own interface

basics

~20 s

Injecting ContainerInterface so a class pulls its own dependencies makes the container a service locator: dependencies are hidden, tests need a container, entry names are hard-coded. It is legitimate when a class computes which entry it needs, like a router.

solid answer

~50 s

PSR-11's recommended-usage section says users SHOULD NOT pass a container into an object so the object can retrieve its own dependencies, because that is the service-locator pattern, which is generally discouraged. The meta document lists the costs: the class only works with PSR-11 containers instead of any wiring; it forces a naming convention such as an entry called `db` that may clash with another package; it is harder to test; and its dependencies are no longer visible in its constructor. The rule of thumb is whether the fetched objects are dependencies of the class or data it resolves. A router that turns a URL into a controller entry name and fetches it is fine, because the controller is not the router's dependency. Factories whose only job is to create instances may also use the container, provided they implement an interface so they can be replaced.

code

php · 20 lines
php
<?php
declare(strict_types=1);

use Psr\Container\ContainerInterface;

// Legitimate: the dispatcher COMPUTES which entry it needs; handlers are not its dependencies.
final class JobDispatcher
{
    /** @param array<string, string> $handlerIds job class => container entry id */
    public function __construct(
        private ContainerInterface $container,
        private array $handlerIds,
    ) {}

    public function dispatch(object $job): void
    {
        $handler = $this->container->get($this->handlerIds[$job::class]);
        $handler->handle($job);
    }
}

go deeper

for a junior

Know that PSR-11 says not to hand the container to a class so it fetches its own collaborators; pass the collaborators themselves.

for a middle

List the costs PSR-11's meta document gives: interoperability, forced entry names, testing and hidden dependencies, and show the constructor-injection fix.

for a senior

Distinguish legitimate container use, routers, dispatchers and factories that compute entry names, from locator misuse in services, and fix it in review.

for a principal

Set boundaries for where container access is allowed in a codebase and how lazy or dynamic dependencies are modelled without spreading service location.

## What PSR-11 says Section 1.3 of **PSR-11**, "Recommended usage", states that users **should not** pass a container into an object so that the object can retrieve *its own dependencies*. Doing so uses the container as a **service locator**, a pattern the specification calls generally discouraged. The meta document spends a whole section on it, which is why interviewers ask about it. ## The anti-pattern ```php final class ReminderScheduler { private Mailer $mailer; public function __construct(private ContainerInterface $container) { $this->mailer = $container->get('mailer'); // pulls its own dependency } } ``` The class needs a mailer, but its constructor says it needs "a container". ## The four costs the meta document names | Cost | What it means for `ReminderScheduler` | |---|---| | **Less interoperable** | it only works where a PSR-11 container is available, instead of with any wiring, including plain `new` | | **Forced naming** | the application must register an entry called exactly `mailer`, which may clash with another package's expectations | | **Harder to test** | a test must build or fake a container and register the right names, instead of passing a fake mailer | | **Hidden dependencies** | nothing in the signature says a mailer is needed; readers and static analysers must inspect the body | A fifth, practical cost follows: a missing or misnamed entry surfaces only when the class performs its lookup, not when the container's definitions are built or validated. ## The fix Declare the real collaborator and let whatever wires the application, a container or a hand-written bootstrap, pass it in: ```php final class ReminderScheduler { public function __construct(private Mailer $mailer, private ClockInterface $clock) {} } ``` The container is still used; it just stays at the edge, building objects, instead of being carried into them. ## When injecting the container is legitimate The meta document gives a test: are the objects you retrieve **dependencies** of the class holding the container, or not? 1. **A router or dispatcher** turns a URL into a controller entry name and fetches that entry. The controller is not the router's dependency; the router *computes* which of many entries it needs. If a class computes the entry name among a varying set, the use is legitimate. 2. **A factory** whose only purpose is to create new instances may use the container, but it should implement a factory interface so that it can itself be replaced by another factory. 3. The same reasoning covers middleware pipelines that resolve middleware by name, and command buses that resolve a handler for a command class. ## Spotting it in review - A constructor parameter typed `ContainerInterface` in a domain or application service. - `get()` called with a **literal** string or `Foo::class` inside business logic. - Tests that register entries in a container just to construct one class. - A class whose real dependencies can only be found by reading its body. Each of these usually means the class should declare its collaborators in its constructor. ## Why the rule sits in the standard PSR-11 exists so that **infrastructure**, frameworks, routers and middleware dispatchers, can fetch entries from whichever container the application uses. The meta document even notes that an end-user developer working in a framework will rarely need to type-hint `ContainerInterface` directly. The recommended-usage section keeps the standard from being read as an invitation to pass the container everywhere. ## Lazy dependencies without a locator Sometimes a class holds a collaborator that is expensive to build and rarely used. Injecting the container to fetch it lazily is still service location. Better options are injecting a small factory interface, a `Closure` that returns the collaborator, or a lazy proxy the container generates, so the class's signature still names what it needs.

  • How can a class get a collaborator lazily without becoming a service locator?
    Inject something that names the need: a small factory interface, a `Closure` returning the collaborator, or a lazy proxy generated by the container. The class's constructor still states what it depends on, tests can pass a trivial implementation, and only construction is deferred.
  • According to PSR-11's meta document, when may a factory use the container?
    When the factory's only purpose is to create and return new instances. It may then fetch collaborators from the container, but it should implement a factory interface so that it can be replaced by another factory using the same interface, which keeps its consumers independent of the container.

saying these in an interview costs you the question

  • Injects ContainerInterface into services because it is more flexible.
  • Says PSR-11 forbids ever type-hinting ContainerInterface.
  • Believes a class using the container has no hidden dependencies because the container is visible.
  • Thinks the service-locator concern is only about performance.
  • Treats a router resolving controllers by name as service-locator misuse.