tag()와 tagged()를 사용할 수 있습니다. 집약하는 쪽이 모든 구현 클래스를 알 필요는 없습니다.
이 페이지는 Laravel 13을 대상으로 하며, Framework v13.38.0의 구현과 공식 테스트를 확인한 내용입니다. 바인딩의 기본은 서비스 컨테이너를, 프로바이더 배포 방법은 패키지 개발을 참조하세요.
태그는 서비스의 등록 키를 묶는다
tag()는 클래스 이름 등의 컨테이너 등록 키를 태그 목록에 추가합니다. 태그를 붙이는 것만으로는 해당 서비스가 생성되지 않습니다.
tagged()는 해당 태그에 등록된 서비스를 열거하기 위한 iterable을 반환합니다. 특정 인터페이스를 구현한 클래스를 자동으로 검색하는 기능이 아니라, 각 프로바이더가 명시적으로 참여하는 구조입니다. vendor:publish --tag의 공개 리소스용 태그와도 다릅니다.
확장 가능한 리포트 패키지 만들기
구현이 공유하는 컨트랙트 준비하기
예로 여러 리포트를 모으는acme/reports 패키지를 만듭니다. Acme\Reports\가 src/에 대응하는 Composer PSR-4 설정을 전제로 합니다.
src/Contracts/Report.php에 플러그인이 구현할 컨트랙트를 정의합니다.
src/RuntimeReport.php와 src/MemoryReport.php를 준비합니다.
집약 서비스는 iterable을 받는다
src/ReportCollector.php는 각 구현을 열거할 때 수집 처리를 호출합니다. 공개할 태그 이름을 상수로 두면 사용하는 쪽도 같은 이름을 참조할 수 있습니다.
iterable 타입만으로는 요소의 타입을 검증할 수 없으며, tag() 자체도 Report의 구현인지 확인하지 않습니다. 이 예에서는 집약하는 쪽에서도 컨트랙트를 확인합니다.
register()에서는 바인딩과 태그만 등록한다
src/ReportsServiceProvider.php에서 기본 구현과 집약 서비스를 등록합니다. 집약 서비스의 팩토리는 가져온 iterable을 그대로 전달합니다.
Report를 구현한 DailyReport 등을 준비하고, 자신의 프로바이더의 register() 안에서 같은 태그에 추가합니다.
app(ReportCollector::class)->collect()를 호출하면 추가한 구현도 집약됩니다. 참여하는 구현이 늘어나도 ReportCollector의 생성 처리를 변경할 필요가 없습니다.
tagged()와 giveTagged()는 생성 시점이 다르다
확인한 구현에서는 등록된 태그의tagged()가 RewindableGenerator를 반환합니다. 각 서비스의 make()는 열거할 때 호출되며, 도중에 열거를 멈추면 나머지 서비스는 생성되지 않습니다.
또한 RewindableGenerator는 열거할 때마다 새로운 Generator를 만듭니다. 서비스의 결과를 캐시하는 기능이 아니므로, 일반 bind()라면 다시 열거할 때 인스턴스도 다시 생성됩니다. singleton()이나 scoped() 서비스는 각각의 공유 규칙을 따릅니다.
반면 컨텍스트 바인딩의 giveTagged()는 해석 시점에 tagged()의 결과를 iterator_to_array()로 배열로 변환합니다.
배열로 한 번에 의존성을 해석하고 싶다면 앞의 팩토리를 사용한
bind()를 다음 등록으로 바꿀 수 있습니다.
$reports의 타입이 iterable이라도 이 방법으로 보관되는 값은 배열입니다. 인수의 타입만으로는 지연 해석을 보장할 수 없습니다. 또한 giveTagged()가 수행하는 것은 인스턴스 해석이며, 각 구현의 collect() 호출이 아닙니다.
Tag 어트리뷰트의 resolve()는 tagged()를 그대로 반환합니다. 어트리뷰트를 사용할 때는 컨텍스트 어트리뷰트처럼 생성자 인수에 지정하고, 클래스 생성을 컨테이너에 맡깁니다. 직접 만든 팩토리 안의 new에 어트리뷰트가 자동으로 적용되지는 않습니다.
플러그인 등록 계약을 깨뜨리지 않기
중복 등록과 순서를 명시하기
tag()는 목록에 추가하는 처리이므로, 같은 등록 키를 다시 추가해도 중복을 제거하지 않습니다. 같은 구현이 두 번 열거되면, singleton이라 같은 객체가 반환되더라도 collect()는 두 번 호출됩니다.
태그 등록은 각 구현을 담당하는 프로바이더의 한 곳에 모읍니다. bindIf()는 바인딩 덮어쓰기를 피하기 위한 API이며, 그 이후의 tag() 중복을 막아 주지는 않습니다.
열거 순서는 태그 목록에 추가된 순서입니다. 태그에는 우선순위 정렬 처리가 없으므로, 서로 다른 패키지 간의 처리 순서를 프로바이더 로드 순서에만 의존하지 마세요. 순서가 공개 사양으로 필요하다면 우선순위나 식별자를 컨트랙트에 포함하고, 집약하는 쪽에서 명시적으로 정렬하도록 설계합니다.
태그 추가를 담당하는 프로바이더를 먼저 등록하기
등록되지 않은 태그의tagged()는 예외가 아니라 빈 배열을 반환합니다. 먼저 받아 둔 빈 배열이나 giveTagged()로 이미 생성된 배열에 나중에 등록한 태그가 반영되지는 않습니다.
등록된 태그의 지연 열거에 대해서도 실행 도중의 목록 변경을 전제로 하지 마세요. 확인한 tagged()는 개수를 가져올 때 전달하는 한편, 열거할 때는 태그 목록을 참조합니다. 모든 프로바이더의 등록을 마친 후에 사용하도록 설계하면 개수와 열거 대상의 불일치도 피할 수 있습니다.
지연 프로바이더의 시작 조건은 DeferrableProvider에서, 설치 시의 로드는 패키지 자동 검출에서 확인할 수 있습니다.
태그와 서비스의 라이프사이클을 분리하기
태그를 붙여도 서비스가 자동으로 singleton이나 scoped가 되지는 않습니다. 이 예는 일반bind()를 사용하며, 집약 객체에 수집 결과를 저장하지 않습니다.
특히 giveTagged()로 해석한 배열을 singleton 집약 서비스에 보관하면, 배열 안의 scoped 서비스도 계속 보관될 수 있습니다. 스코프 초기화는 이미 다른 객체에 저장된 참조를 교체하지 않습니다. Octane이나 큐 워커에서 사용하는 설계는 패키지 서비스의 라이프사이클도 참조하세요.
지연 해석과 배열화 테스트하기
패키지의 Composer 오토로드와 PHPUnit 환경에서tests/ReportCollectorTest.php에 다음 테스트를 추가할 수 있습니다. 컨테이너 단독으로 프로바이더 등록을 호출하고 생성 횟수를 검증합니다. 애플리케이션 통합이나 자동 검출까지 포함하는 테스트는 Orchestra Testbench로 보완합니다.
참고한 1차 정보
- Laravel 공식 문서: Tagging
- Laravel 공식 문서: Binding primitives・Variadic tag dependencies
- Laravel 공식 문서: 서비스 프로바이더
- Laravel 공식 문서: 패키지 개발
- Container: tag()・tagged()
- ContextualBindingBuilder: giveTagged()
- RewindableGenerator: 다시 열거와 개수
- Tag 어트리뷰트: resolve()
- 공식 테스트: ContainerTaggingTest
- 공식 테스트: ContextualBindingTest