---
title: "ADR-022: Attribute-Based Provider Registration"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-022@0.35"
source: "Adr/Adr022AttributeBasedProviderRegistration.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-022: Attribute-Based Provider Registration

-   *Status:* Accepted
-   *Date:* 2026-04
-   *Authors:* Netresearch DTT GmbH

## Context

Registering a new provider previously required two places to stay in sync:
the class itself, and a `tags:` block in `Configuration/Services.yaml`
naming `nr_llm.provider` with a numeric priority. Omit either side and the
provider silently vanished from `LlmServiceManager::getProviderList()`.
For the seven shipped providers this is a footgun we kept stepping on during
refactors. For third-party providers it is an onboarding tax.

## Decision

Introduce `#[AsLlmProvider(priority: N)]` on the provider class and have
`ProviderCompilerPass` scan every container definition at compile time
for the attribute, auto-tagging matched services with `nr_llm.provider`.

The existing yaml-tagging path still works. When both are present, the yaml
tag wins (the attribute pass skips already-tagged services). This is
deliberate: overrides should be explicit, not silently merged.

The shipped providers now declare their priority via the attribute, and the
`tags:` entries have been removed from `Configuration/Services.yaml`.
`ProviderCompilerPass` collects every `nr_llm.provider`-tagged service
(from the attribute or a legacy yaml tag), sorts them by priority, and wires
each one into `LlmServiceManager` with a `registerProvider()` method
call. The providers stay **private** — they are never individually resolved
from the container (which keeps the public-services set locked by
[ADR-028](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-028@0.35)); the backend instantiates the concrete adapter for a
provider record directly through `ProviderAdapterRegistry`. The legacy
yaml-tagging path still works for third-party providers.

## Trade-offs

-   **\+ Single source of truth.** The priority lives next to the class, not in
    a sibling yaml file.
-   **\+ Third-party DX.** External providers drop in without editing yaml:
    `#[AsLlmProvider(priority: 100)]` on an autowired class is enough.
-   **\+ Backward-compatible.** Existing yaml-tagged providers keep working.
-   **\- Reflection at compile time.** The compiler pass reflects service
    definitions in the `Netresearch\NrLlm\` namespace; other definitions
    are skipped by a prefix match on the class name (no reflection). Cost
    is paid once per container build, cached via
    `ContainerBuilder::getReflectionClass()`, and negligible in
    practice.
-   **\- Implicit registration.** A new reader grepping `nr_llm.provider` in
    yaml no longer finds all providers. Mitigation: the attribute constant
    `AsLlmProvider::TAG_NAME` is discoverable via symbol search.

## Alternatives considered

-   **Symfony's \`\`registerAttributeForAutoconfiguration\`\`** — the idiomatic
    path, but TYPO3's DI bootstrap does not expose the underlying container
    builder at a hook point where attribute registration would work cleanly
    for every installed extension. A compiler pass runs at the right
    lifecycle stage and touches only our tag.
-   **Keep yaml tags only.** Rejected: the double-bookkeeping problem was the
    whole motivation.
-   **Scan providers directory by namespace.** Rejected as too magical —
    implicit "any class ending in Provider" registration is a known anti-pattern.
