Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Service Annotation Bundle Laravel Package

terminal42/service-annotation-bundle

A Symfony bundle that lets you define and configure dependency-injection services using PHP annotations, reducing boilerplate YAML or XML. Annotate classes to register services, tags, and dependencies automatically.

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a Laravel package — it is a Symfony bundle (terminal42/service-annotation-bundle) that adds an attribute/annotation-driven service registration compiler pass to the Symfony DI container. There is no ServiceProvider, no config/*.php to publish, and no illuminate/* dependency. If you composer require it into a Laravel app you will pull Symfony DI/HttpKernel components in and get nothing usable. A Laravel developer will only meet this package when working inside a Symfony app (or a Symfony-based distribution) that already depends on it.

Assuming a Symfony host application:

  1. composer require terminal42/service-annotation-bundle (1.1.6 now accepts symfony/*: ^7.0, so it will install cleanly on Symfony 6.x and 7.x).
  2. Register the bundle in config/bundles.php:
    Terminal42\ServiceAnnotationBundle\ServiceAnnotationBundle::class => ['all' => true],
    
  3. Declare what each attribute means in config/packages/service_annotation.yaml:
    terminal42_service_annotation:
        annotations:
            App\Attribute\AsFoo:
                tags:
                    - { name: 'app.foo' }
                # public / autowire / autoconfigure as supported by your version
    
  4. Declare the marker attribute (PHP 8 attribute, TARGET_CLASS):
    #[Attribute(Attribute::TARGET_CLASS)]
    final class AsFoo {}
    
  5. Annotate a class and it becomes a container service carrying the mapped tag:
    #[AsFoo]
    final class PaymentHandler {}
    

Where to look first: the README (it is short and is the real documentation), then DependencyInjection/Configuration.php for the exact keys the config tree accepts, and the bundle's compiler pass to understand which classes get scanned. First use case: replacing a hand-maintained list of services.yaml entries + tags with a single attribute on each class, then consuming them via a tagged iterator.

Implementation Patterns

  • One attribute per role, not per service. The idiomatic use is a semantic marker (#[AsExporter], #[AsPaymentHandler]) mapped once to a tag in YAML. The individual classes stay annotation-free apart from the marker, and the tag name lives in exactly one place.
  • Collect, don't hardcode. The payoff is !tagged_iterator:
    App\Export\ExporterCollection:
        arguments:
            - !tagged_iterator app.exporter
    
    Now dropping #[AsExporter] on a new class is the entire registration step — no YAML edit, no constructor wiring.
  • Pair with _defaults deliberately. Auto-registered services do not necessarily inherit your services.yaml _defaults (autowire/autoconfigure/public). If a tagged service is injected by id anywhere, set public: true or autowire: true in the annotation's config block; otherwise expect "service not found" or "cannot autowire" at compile time.
  • Prefer native Symfony attributes when they suffice. #[AutoconfigureTag], #[AsTaggedItem], #[AsEventListener] and #[AsCommand] cover the majority of cases with zero config. Reach for this bundle only for your own domain markers that core autoconfiguration cannot express.
  • Wrap third-party services. Because the attribute lives on the class, you cannot annotate a vendor class directly — write a thin subclass or a services.yaml definition with #[AsAlias]-style indirection instead.
  • Keep the attribute classes in src/ so they are autoloadable at container-compile time. A dev-only or test-only autoload path will break cache:warmup in prod.
  • Migration workflow: add the attribute → add the YAML mapping → run bin/console cache:clear → bin/console debug:container --tag=app.foo to confirm → delete the old services.yaml entry → run the test suite. Doing it one class at a time keeps the diff reviewable.

Gotchas and Tips

  • 1.1.6 is a compatibility-only release. The single change (PR #9, "Allow Symfony 7") just widens the symfony/* constraint. No API changes, no deprecations, no behavioural differences — upgrading from 1.1.5 is safe. The practical takeaway: if this package was the last thing blocking your Symfony 7 upgrade, that blocker is gone; re-run composer update terminal42/service-annotation-bundle -W and check composer why-not symfony/framework-bundle ^7.0 again.
  • Attributes are read at compile time only. Anything computed at runtime (feature flags, env-dependent registration) will not work. Attributes → compiler pass → frozen container. Runtime conditionals belong in a factory or a tagged iterator consumer.
  • Docblock annotations are legacy. The package name still says "annotation", but doctrine/annotations is deprecated and gone from most modern stacks. Use #[Attribute] classes exclusively; if you inherit a codebase using @Foo docblocks, budget for that migration before touching anything else.
  • Stale container after attribute edits. Adding or removing an attribute on an existing class does not always invalidate the compiled container in dev. bin/console cache:clear (or rm -rf var/cache/*) is the first debugging step for "my new tagged service isn't there".
  • Silent no-ops are the failure mode. A typo in the attribute FQCN in service_annotation.yaml, or an attribute not matching the configured class exactly, produces no error — the class simply never becomes a service. Verify with bin/console debug:container --tag=<tag> and, if needed, debug:container --show-private.
  • Never install it into Laravel. Check composer.json for symfony/dependency-injection before adding. If you want this pattern in Laravel, do not use this package — use olvlvl/composer-attribute-collector plus a small ServiceProvider that reflects over app/ and calls $this->app->tag(...), or just bind an explicit interface → implementation map.
  • Extension point for anything non-trivial: a custom CompilerPassInterface registered in your Kernel::build() gives you full control and is usually cleaner than stretching the YAML annotations mapping beyond tags/public/autowire.
  • Pin the version in libraries. If you publish a package that depends on this bundle, ^1.1.6 is a reasonable floor now that Symfony 7 is supported; consumers on Symfony 6.4 are unaffected.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core