# Inyección de dependencias de Symfony: contenedor de servicios, autowiring y tags en 2026 > Análisis a fondo de la inyección de dependencias de Symfony: contenedor de servicios, autowiring, atributo #[Autowire], enlace de interfaces, servicios etiquetados con #[AutowireIterator], compiler passes y servicios perezosos en Symfony 7.4 y 8.0. - Published: 2026-07-06 - Updated: 2026-07-07 - Author: SharpSkill - Tags: symfony, dependency injection, service container, autowiring, php, di - Reading time: 11 min --- La inyección de dependencias de Symfony es la columna vertebral de toda aplicación Symfony: el contenedor de servicios instancia, configura y conecta los objetos entre sí, de modo que las clases declaran lo que necesitan en lugar de construirlo por su cuenta. En Symfony 7.4 y 8.0, el autowiring, los atributos de PHP y los servicios etiquetados redujeron la configuración explícita a casi nada, sin dejar de mantener el contenedor totalmente resuelto y predecible en tiempo de compilación. Entender cómo funciona esa resolución separa a los desarrolladores que pelean contra el contenedor de aquellos que lo dejan hacer su trabajo. > **¿Qué es la inyección de dependencias en Symfony?** > > La inyección de dependencias en Symfony es un patrón en el que el contenedor de servicios pasa las dependencias de un objeto a través de su constructor en lugar de dejar que el objeto las cree. El autowiring resuelve automáticamente cada argumento tipado hacia un servicio correspondiente, por lo que la mayoría de las clases no requieren ninguna configuración manual. ## Cómo el contenedor de servicios de Symfony resuelve las dependencias El contenedor de servicios es una clase PHP compilada que sabe construir cada servicio de la aplicación. Durante la fase de compilación del contenedor, Symfony analiza las definiciones de clases, lee las firmas de los constructores y produce una fábrica en caché. En tiempo de ejecución, solicitar un servicio devuelve una instancia compartida y construida de forma perezosa. Como el cableado ocurre en tiempo de compilación, una dependencia mal configurada falla cuando se construye la caché, no cuando una petición llega a producción. Un servicio es simplemente una clase que realiza una tarea y que el contenedor gestiona. El archivo `config/services.yaml` por defecto registra todo lo que hay bajo `src/` como servicio, con el autowiring y la autoconfiguración habilitados, y por eso una clase corriente funciona sin ningún ajuste adicional. ```yaml # config/services.yaml services: _defaults: autowire: true # inject arguments by type-hint autoconfigure: true # apply tags based on implemented interfaces App\: resource: '../src/' exclude: - '../src/Entity/' - '../src/Kernel.php' ``` Con esos valores por defecto en su sitio, una clase pide sus colaboradores a través del constructor y el contenedor los provee. Sin fábrica, sin `new`, sin lista de argumentos escrita a mano. ```php // src/Notification/SlackNotifier.php namespace App\Notification; use Psr\Log\LoggerInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; final class SlackNotifier { // The container reads each type-hint and injects a matching service public function __construct( private readonly HttpClientInterface $httpClient, private readonly LoggerInterface $logger, ) {} public function send(string $channel, string $message): void { $this->httpClient->request('POST', 'https://slack.example/api/post', [ 'json' => ['channel' => $channel, 'text' => $message], ]); $this->logger->info('Slack message dispatched', ['channel' => $channel]); } } ``` El contenedor instancia `SlackNotifier` una sola vez, le inyecta el cliente HTTP y el logger compartidos, y reutiliza esa instancia en todo lugar donde se la solicita. Ese comportamiento compartido por defecto es lo que hace al contenedor rápido y eficiente en memoria a la vez. El modelo completo está documentado en la [guía oficial del contenedor de servicios de Symfony](https://symfony.com/doc/current/service_container.html), que se apoya en el estándar de la [interfaz de contenedor PSR-11](https://www.php-fig.org/psr/psr-11/) compartido en todo el ecosistema PHP. El compartir puede desactivarse cuando se requiere una instancia nueva por cada petición, por ejemplo un builder con estado que acumula datos. Definir `shared: false` en una definición la convierte en una fábrica que devuelve un objeto nuevo cada vez que se inyecta. Esto es poco común en la práctica, porque la mayoría de los servicios son colaboradores sin estado, pero saber que ese valor por defecto existe y cómo sobrescribirlo importa cuando un bug se rastrea hasta dos consumidores que mutan por error el mismo objeto. ## Autowiring de Symfony: type hints y el atributo Autowire El autowiring de Symfony resuelve los argumentos del constructor según su declaración de tipo. Cuando un argumento está tipado con una clase o interfaz que corresponde a exactamente un servicio, el contenedor lo inyecta sin ninguna configuración. El autowiring solo se rinde ante valores que no son servicios: escalares, variables de entorno y parámetros. El atributo `#[Autowire]` cierra esa brecha directamente sobre el argumento, manteniendo el cableado junto al código que lo usa. ```php // src/Service/PaymentGateway.php namespace App\Service; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; final class PaymentGateway { public function __construct( // Inject a container parameter with the %...% syntax #[Autowire('%kernel.environment%')] private readonly string $environment, // Inject an environment variable, resolved at runtime #[Autowire(env: 'STRIPE_SECRET')] private readonly string $apiKey, // Bind a specific service when the type alone is ambiguous #[Autowire(service: 'monolog.logger.payment')] private readonly LoggerInterface $logger, ) {} } ``` Cada argumento declara su propia fuente: un parámetro, una variable de entorno o un servicio nombrado. El atributo también acepta un argumento `expression:` para valores de Expression Language, útil cuando una dependencia proviene de un cálculo en tiempo de ejecución en vez de una definición estática. Mantener estos metadatos en el constructor permite que quien lee vea con exactitud de dónde nace cada valor sin abrir un archivo YAML aparte. La referencia del atributo vive en la [documentación del autowiring de Symfony](https://symfony.com/doc/current/service_container/autowiring.html). > **Tipa interfaces, no clases concretas** > > Hacer autowiring de una clase concreta como `S3Storage` acopla a cada consumidor con esa implementación y vuelve doloroso reemplazarla en las pruebas. Tipa la interfaz en su lugar y deja que `#[AsAlias]` o `#[Autowire(service: ...)]` decidan el servicio concreto. Inyectar el contenedor completo para buscar servicios a mano anula las verificaciones en tiempo de compilación y es justamente el antipatrón que el autowiring existe para eliminar. ## Enlazar interfaces cuando existen varias implementaciones El autowiring contra una interfaz funciona limpiamente hasta que dos clases la implementan. En ese punto el contenedor no puede adivinar cuál inyectar y lanza un error de ambigüedad en tiempo de compilación. Dos atributos resuelven esto: `#[AsAlias]` marca una implementación como la predeterminada para la interfaz, y `#[Autowire(service: ...)]` fija una implementación específica en un argumento concreto. ```php // src/Storage/StorageInterface.php namespace App\Storage; interface StorageInterface { public function put(string $key, string $contents): void; } ``` ```php // src/Storage/S3Storage.php namespace App\Storage; use Symfony\Component\DependencyInjection\Attribute\AsAlias; // Becomes the default service returned for StorageInterface #[AsAlias(StorageInterface::class)] final class S3Storage implements StorageInterface { public function put(string $key, string $contents): void { /* upload to S3 */ } } ``` ```php // src/Service/BackupService.php namespace App\Service; use App\Storage\LocalStorage; use App\Storage\StorageInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; final class BackupService { public function __construct( // Resolves to the #[AsAlias] default (S3Storage) private readonly StorageInterface $primary, // Pins the concrete implementation explicitly by service id #[Autowire(service: LocalStorage::class)] private readonly StorageInterface $fallback, ) {} } ``` El atributo `#[Target]` ofrece una tercera opción: referencia una implementación por su nombre de alias de autowiring, desacoplando el nombre de la variable del argumento de la resolución. Elegir entre estos mecanismos depende de la intención. Usa `#[AsAlias]` cuando una implementación es realmente la predeterminada en toda la base de código, y `#[Autowire(service: ...)]` o `#[Target]` cuando un único consumidor necesita una variante no predeterminada. Este patrón aparece constantemente en aplicaciones de producción, y es un tema favorito del [módulo de entrevista sobre inyección de dependencias de Symfony](/technologies/symfony/interview-questions/dependency-injection). ## Etiquetas de servicios de Symfony y el colector AutowireIterator Las etiquetas son la forma en que Symfony agrupa servicios relacionados para que otro servicio los consuma todos de una vez. Esto impulsa los patrones strategy, cadena de responsabilidad y plugin que atraviesan al propio framework: las extensiones de tipo de formulario, las extensiones de Twig y los event subscribers se basan todos en etiquetas. En lugar de etiquetar manualmente, `#[AutoconfigureTag]` colocado sobre una interfaz le indica al contenedor que etiquete automáticamente cada clase que la implemente. ```php // src/Export/ExporterInterface.php namespace App\Export; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; // Every class implementing this interface is tagged automatically #[AutoconfigureTag('app.exporter')] interface ExporterInterface { public function supports(string $format): bool; public function export(array $rows): string; } ``` Cada implementación controla su posición en la colección con `#[AsTaggedItem]`, que fija una prioridad (la más alta se ejecuta primero) y un índice de cadena opcional para el acceso por clave. ```php // src/Export/CsvExporter.php namespace App\Export; use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem; // Higher priority services appear earlier in the injected iterable #[AsTaggedItem(priority: 10)] final class CsvExporter implements ExporterInterface { public function supports(string $format): bool { return $format === 'csv'; } public function export(array $rows): string { return implode("\n", array_map(fn ($r) => implode(',', $r), $rows)); } } ``` El servicio consumidor reúne cada exporter etiquetado en un `iterable` ordenado usando `#[AutowireIterator]`. Luego recorre la colección y delega en la primera estrategia que admite el formato solicitado. ```php // src/Export/ExportManager.php namespace App\Export; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; final class ExportManager { // Collects all services tagged 'app.exporter', ordered by priority public function __construct( #[AutowireIterator('app.exporter')] private readonly iterable $exporters, ) {} public function export(string $format, array $rows): string { foreach ($this->exporters as $exporter) { if ($exporter->supports($format)) { return $exporter->export($rows); } } throw new \InvalidArgumentException("No exporter for format: $format"); } } ``` Cuando la colección es grande o costosa de construir, `#[AutowireLocator]` es la alternativa perezosa: inyecta un contenedor PSR-11 que instancia un servicio etiquetado solo cuando se lo recupera realmente por su clave. Esa distinción entre iteración ávida y búsqueda perezosa importa para los servicios que mantienen conexiones o estado pesado. ## Autoconfiguración, compiler passes y servicios perezosos La autoconfiguración es el mecanismo que lee atributos como `#[AutoconfigureTag]` y `#[AsTaggedItem]` y los aplica durante la compilación. Es la razón por la que implementar `EventSubscriberInterface` basta para registrar un event subscriber sin ninguna configuración. Para las transformaciones que los atributos no pueden expresar, un compiler pass se engancha directamente a la construcción del contenedor y muta las definiciones antes de escribir la caché. El código fuente del componente `DependencyInjection` en [GitHub](https://github.com/symfony/dependency-injection) muestra cómo el propio framework usa internamente docenas de estas passes. Los grafos sensibles al rendimiento se benefician de los servicios perezosos. Marcar un argumento con `#[Autowire(lazy: true)]` difiere la instanciación hasta la primera llamada a la dependencia, respaldado por los objetos perezosos nativos de PHP 8.4 en lugar de una biblioteca de proxies generados. ```php // src/Service/ReportBuilder.php namespace App\Service; use App\Pdf\PdfRenderer; use Symfony\Component\DependencyInjection\Attribute\Autowire; final class ReportBuilder { public function __construct( // PdfRenderer is only instantiated on first actual use #[Autowire(lazy: true)] private readonly PdfRenderer $renderer, ) {} public function buildMonthly(): string { // If no method touches $this->renderer, it is never constructed return $this->renderer->render('monthly-report'); } } ``` Los objetos perezosos nativos llegaron con PHP 8.4 y se tratan en profundidad en la guía sobre [Symfony 8 y los objetos perezosos de PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects). La recompensa es un contenedor que se mantiene ligero: colaboradores pesados como generadores de PDF, clientes de SDK de terceros o generadores de reportes no cuestan nada hasta que se ejecuta el camino de código que los necesita. ## Preguntas y respuestas de entrevista sobre DI en Symfony Una pregunta de entrevista común sobre DI en Symfony pide la diferencia entre autowiring y autoconfiguración. El autowiring resuelve los argumentos del constructor de un servicio dado por su tipo, mientras que la autoconfiguración aplica etiquetas, llamadas a métodos y ajustes a un servicio según las interfaces que implementa. Son dos interruptores independientes: un servicio puede tener autowiring sin estar autoconfigurado, y a la inversa. Otra pregunta frecuente es por qué el contenedor distingue entre servicios públicos y privados. Los servicios son privados por defecto, lo que significa que no pueden recuperarse directamente del contenedor con `$container->get()` y solo pueden inyectarse. Los servicios privados pueden inlinearse y eliminarse por optimización durante la compilación, lo que es más rápido; los servicios públicos deben seguir siendo recuperables, así que el optimizador los deja intactos. Recuperar servicios manualmente del contenedor se considera un antipatrón precisamente porque anula esta optimización y oculta las dependencias. Las dependencias circulares son una trampa clásica y una repregunta frecuente. Si el servicio A necesita a B en su constructor y B necesita a A, el contenedor no puede construir ninguno de los dos y reporta una referencia circular en tiempo de compilación. Las correcciones estándar son romper el ciclo extrayendo el comportamiento compartido a un tercer servicio, cambiar un lado a inyección perezosa con `#[Autowire(lazy: true)]` para que la dependencia se resuelva solo en la primera llamada, o recurrir a la inyección por setter para que el objeto exista antes de que su colaborador se cablee. Reconocer por qué el ciclo es un defecto de diseño, y no solo cómo silenciarlo, es lo que los entrevistadores realmente escuchan. Los entrevistadores también sondean el momento de la resolución del contenedor. Como Symfony compila el contenedor una vez y lo guarda en caché, una dependencia irresoluble sale a la luz durante el calentamiento de la caché en lugar de en el momento de la petición. Esa garantía es una consecuencia directa del modelo en tiempo de compilación y una de las razones por las que las grandes aplicaciones Symfony se mantienen predecibles bajo carga. Para un conjunto más amplio de estas preguntas, la [ruta de la tecnología Symfony](/technologies/symfony) las agrupa por componente y dificultad. ## Conclusión - Deja trabajar al contenedor: registra las clases bajo `src/`, tipa las dependencias en el constructor y evita por completo el cableado manual en YAML. - Recurre a `#[Autowire]` solo para no-servicios, parámetros, variables de entorno e identificadores de servicio específicos. - Resuelve la ambigüedad de interfaces con `#[AsAlias]` para el predeterminado y `#[Autowire(service: ...)]` o `#[Target]` para elecciones explícitas. - Impulsa los patrones strategy y plugin con `#[AutoconfigureTag]`, las prioridades de `#[AsTaggedItem]` y `#[AutowireIterator]`; cambia a `#[AutowireLocator]` cuando la búsqueda perezosa sea preferible a la iteración ávida. - Difiere los colaboradores costosos con `#[Autowire(lazy: true)]`, que usa los objetos perezosos nativos de PHP 8.4. - Mantén los servicios privados e inyectados en lugar de recuperados, para que el compilador pueda inlinearlos y optimizar el grafo. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/symfony/symfony-dependency-injection-service-container-autowiring-tags