Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.

Once you understand services and dependency injection, the natural question is: how does Symfony know which object to pass when a class asks for one? That is the job of the container and autowiring. The container is the registry of every service in your app, and autowiring is how it matches what you ask for to what it has, usually with no configuration at all.

Autowiring resolves by type

Autowiring is the feature that makes modern Symfony feel effortless. You type-hint a class or interface in a constructor, and the container finds the matching service and injects it. No YAML, no registration:

use Psr\Log\LoggerInterface;

class ReportBuilder
{
    public function __construct(private LoggerInterface $logger) {}
}

The container sees the LoggerInterface type-hint, knows which service implements it, and passes it in. The same works for your own classes: type-hint InvoiceGenerator and you get the InvoiceGenerator service. For the vast majority of your code, this is all you ever do.

How everything gets registered

This works because of the default config/services.yaml, which does three things for you:

services:
  _defaults:
    autowire: true       # resolve dependencies by type-hint
    autoconfigure: true  # auto-tag services by interface

  App\:
    resource: '../src/'  # register every class in src/ as a service

Every class in src/ is automatically a service, autowired and autoconfigured. You write a class, type-hint it where you need it, and it just works. This convention is why most Symfony projects barely touch services.yaml.

When autowiring needs help

Autowiring resolves by type, so it gets stuck when a type is ambiguous, most often when an interface has two implementations. The container cannot guess which one you mean. You resolve it with the #[Autowire] attribute, pointing at the specific service:

public function __construct(
    #[Autowire(service: 'app.s3_storage')]
    private StorageInterface $storage,
) {}

The same attribute injects parameters and environment values, which type-hints alone cannot supply:

public function __construct(
    #[Autowire('%env(STRIPE_SECRET)%')]
    private string $stripeSecret,
) {}

So the rule is: let autowiring handle the common case by type, and reach for #[Autowire] only for the exceptions, a chosen implementation or a scalar value.

Seeing what the container knows

Two console commands save you from guessing. php bin/console debug:autowiring lists every type you can type-hint and what it resolves to, which is the fastest way to find the right interface for, say, the cache or the HTTP client. And php bin/console debug:container shows every registered service. When a dependency will not wire, these tell you why.

Wrapping up

The container is the registry of your services, and autowiring is how it injects them by matching constructor type-hints to registered services, with no configuration for the common case thanks to the default services.yaml. When a type is ambiguous or you need a parameter, the #[Autowire] attribute points the container at exactly what you mean. Lean on autowiring for almost everything, use the attribute for the exceptions, and reach for debug:autowiring whenever you are unsure what to type-hint.