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 paqueteacme/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.
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.
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
Ensrc/ReportsServiceProvider.php, registra las implementaciones estándar y el servicio agregador. La factory del servicio agregador pasa tal cual el iterable obtenido.
DailyReport que implemente Report y lo añaden a la misma etiqueta dentro del register() de su propio provider.
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.
$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.
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 unbind() 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 entests/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.
Fuentes primarias consultadas
- Documentación oficial de Laravel: Tagging
- Documentación oficial de Laravel: Binding primitives y Variadic tag dependencies
- Documentación oficial de Laravel: Service providers
- Documentación oficial de Laravel: Desarrollo de paquetes
- Container: tag() y tagged()
- ContextualBindingBuilder: giveTagged()
- RewindableGenerator: nuevo recorrido y número de elementos
- Atributo Tag: resolve()
- Test oficial: ContainerTaggingTest
- Test oficial: ContextualBindingTest