Saltar al contenido principal
Si quieres que varios paquetes o la aplicación que los usa puedan añadir funcionalidades a un mismo servicio agregador, puedes usar tag() y tagged() del contenedor. El servicio que agrega no necesita conocer todas las clases de implementación. Esta página está dirigida a Laravel 13 y se basa en la revisión de la implementación y los tests oficiales de Framework v13.38.0. Para los fundamentos de los bindings, consulta el contenedor de servicios, y para la distribución de providers, Desarrollo de paquetes para Laravel.

Las etiquetas agrupan claves de registro de servicios

tag() añade claves de registro del contenedor, como nombres de clase, a la lista de una etiqueta. Etiquetar un servicio no lo crea. tagged() devuelve un iterable para recorrer los servicios registrados con esa etiqueta. No es una función que busque automáticamente las clases que implementan una interfaz concreta, sino un mecanismo en el que cada provider participa de forma explícita. Tampoco tiene relación con las etiquetas de recursos publicables de vendor:publish --tag.

Crear un paquete de informes extensible

Preparar el contrato que comparten las implementaciones

Como ejemplo, crearemos el paquete acme/reports, que reúne varios informes. Se asume una configuración PSR-4 de Composer en la que Acme\Reports\ corresponde a src/. En src/Contracts/Report.php, define el contrato que implementarán los plugins.
Como implementaciones mínimas, prepara src/RuntimeReport.php y src/MemoryReport.php.

El servicio agregador recibe un iterable

src/ReportCollector.php llama al proceso de recopilación al recorrer cada implementación. Si defines como constante el nombre de la etiqueta pública, quienes usen el paquete pueden referirse al mismo nombre.
El tipo iterable de PHP por sí solo no permite validar el tipo de los elementos, y tag() tampoco comprueba si se trata de una implementación de Report. En este ejemplo, el servicio agregador también verifica el contrato.

En register() solo se registran bindings y etiquetas

En src/ReportsServiceProvider.php, registra las implementaciones estándar y el servicio agregador. La factory del servicio agregador pasa tal cual el iterable obtenido.
Otros paquetes o la aplicación que los usa preparan, por ejemplo, un DailyReport que implemente Report y lo añaden a la misma etiqueta dentro del register() de su propio provider.
Después de registrar todos los providers, al llamar a app(ReportCollector::class)->collect() también se agregan las implementaciones añadidas. Aunque aumente el número de implementaciones participantes, no hace falta modificar la creación de ReportCollector.

tagged() y giveTagged() crean los servicios en momentos distintos

En la implementación revisada, tagged() sobre una etiqueta registrada devuelve un RewindableGenerator. El make() de cada servicio se llama durante el recorrido y, si se detiene a mitad, los servicios restantes no se crean. Además, RewindableGenerator crea un Generator nuevo en cada recorrido. No es una función que almacene en caché los resultados de los servicios: con un bind() normal, las instancias se vuelven a crear al recorrer de nuevo. Los servicios singleton() o scoped() siguen sus propias reglas de compartición. En cambio, giveTagged() del contextual binding convierte el resultado de tagged() en un array con iterator_to_array() en el momento de la resolución. Si quieres resolver las dependencias de una sola vez como array, puedes sustituir el bind() con factory anterior por el siguiente registro.
Aunque el tipo de $reports sea iterable, el valor que se guarda con este método es un array. El tipo del argumento por sí solo no garantiza la resolución diferida. Además, lo que hace giveTagged() es resolver las instancias, no llamar al collect() de cada implementación. El resolve() del atributo Tag devuelve directamente tagged(). Si usas el atributo, indícalo en un argumento del constructor como en los atributos contextuales y deja la construcción de la clase en manos del contenedor. El atributo no se aplica automáticamente a un new dentro de una factory propia.

No romper el contrato de registro de plugins

Explicitar el registro duplicado y el orden

tag() es una operación de añadido a la lista y no elimina duplicados aunque se vuelva a añadir la misma clave de registro. Si la misma implementación se recorre dos veces, collect() se llama dos veces aunque un singleton devuelva el mismo objeto. Concentra el registro de la etiqueta en un único lugar: el provider responsable de cada implementación. bindIf() es una API para evitar sobrescribir bindings, no para impedir un tag() duplicado posterior. El orden de recorrido es el orden en que se añadieron los elementos a la lista de la etiqueta. Las etiquetas no ordenan por prioridad, así que no hagas depender el orden de procesamiento entre paquetes distintos solo del orden de carga de los providers. Si el orden forma parte de la especificación pública, incluye una prioridad o un identificador en el contrato y diseña el servicio agregador para que ordene de forma explícita.

Registrar primero los providers que añaden etiquetas

tagged() sobre una etiqueta no registrada devuelve un array vacío en lugar de lanzar una excepción. Los registros de etiqueta posteriores no se reflejan en un array vacío recibido antes ni en un array ya creado con giveTagged(). Tampoco des por hecho que el recorrido diferido de una etiqueta registrada reflejará cambios en la lista durante la ejecución. El tagged() revisado pasa el número de elementos en el momento de obtenerlo, pero consulta la lista de la etiqueta al recorrerla. Si diseñas el uso para después de que todos los providers hayan terminado de registrarse, también evitas discrepancias entre el número de elementos y los elementos recorridos.
Recorrer una etiqueta no busca, a partir del nombre de la etiqueta, providers que aún no se hayan cargado. Si el provider que añade la etiqueta es un DeferrableProvider, el registro de la etiqueta no se ejecuta hasta que ese servicio se resuelva por otro camino, y puede quedar fuera de la lista. Para registrar en una etiqueta implementaciones que deban participar siempre, usa un provider normal y diséñalo para que no cree las implementaciones durante el registro.
Puedes consultar las condiciones de arranque de los providers diferidos en Service providers diferidos, y la carga durante la instalación en la detección automática de paquetes.

Separar las etiquetas del ciclo de vida de los servicios

Etiquetar un servicio no lo convierte automáticamente en singleton ni en scoped. Este ejemplo usa un bind() normal y no guarda los resultados de la recopilación en el objeto agregador. En particular, si guardas en un servicio agregador singleton un array resuelto con giveTagged(), los servicios scoped del array también pueden quedar retenidos. Restablecer el scope no sustituye las referencias ya guardadas en otros objetos. Para diseños que se usen con Octane o workers de colas, consulta también el Diseño del ciclo de vida de los servicios de paquetes.

Probar la resolución diferida y la conversión a array

En un entorno con el autoload de Composer y PHPUnit del paquete, puedes añadir el siguiente test en tests/ReportCollectorTest.php. Llama al registro del provider solo con el contenedor y verifica el número de creaciones. Los tests que incluyan la integración con la aplicación y la detección automática se complementan con Orchestra Testbench.
Además, al actualizar el paquete, comprueba las siguientes combinaciones.

Fuentes primarias consultadas

Última modificación el 26 de octubre de 2026