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

sergey-milegov/service-annotation-bundle

A Symfony bundle for annotation-driven service registration: mark classes as services and configure tags or arguments without YAML/XML, keeping dependency injection setup concise and close to your code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register the bundle.

composer require sergey-milegov/service-annotation-bundle

Then register it in config/bundles.php:

return [
    // ...
    SergeyMilegov\ServiceAnnotationBundle\ServiceAnnotationBundle::class => ['all' => true],
];

Where to look first. This package has almost no ecosystem around it (2 stars, 0 dependents), so treat the source as the documentation:

  1. src/ — the attribute/annotation class itself (likely Attribute/Service.php) and one or more DependencyInjection/Compiler/*Pass.php classes. The attribute's constructor signature tells you exactly which options (id, tags, public, lazy, alias, factory) are actually supported.
  2. composer.json — check whether it requires doctrine/annotations (docblock @Service style) or PHP 8 attributes only. That determines whether you write #[Service] or /** @Service */.
  3. tests/ — the fixtures are the most reliable usage examples you'll get.

First use case. Take one hand-wired service out of config/services.yaml and annotate the class instead:

#[Service]
class InvoiceNumberGenerator
{
    public function __construct(private ClockInterface $clock) {}
}

Then bin/console cache:clear && bin/console debug:container InvoiceNumberGenerator and confirm the definition exists with autowire/autoconfigure inherited from your _defaults. If it isn't there, the bundle's compiler pass isn't finding the class — check the scan path/namespace config (see Gotchas).

Don't migrate a whole app on day one. Prove the round-trip on one class, then expand.

Implementation Patterns

Start from _defaults in services.yaml. The bundle registers definitions; it almost certainly does not re-implement autowiring. Keep your project-level defaults so annotated services inherit them:

services:
    _defaults:
        autowire: true
        autoconfigure: true

Tag-at-declaration for event subscribers and listeners. This is where the pattern actually pays for itself — the tag and the class live in the same file, so you never hunt for a YAML entry:

#[Service(tags: ['kernel.event_subscriber'])]
class OrderPlacedSubscriber implements EventSubscriberInterface { /* ... */ }

Compare with the default Symfony flow, where the tag is inferred from the implemented interface. If the bundle supports tags, prefer it only for tags that can't be inferred; otherwise let autoconfigure do the work and use the attribute purely for registration.

Named services and aliases for interface injection. Register the implementation under an explicit id and bind the interface:

#[Service(id: 'app.gateway.stripe', alias: 'App\Gateway\GatewayInterface')]
class StripeGateway implements GatewayInterface {}

Use a distinct id whenever you expect two implementations of the same interface (strategy pattern, decorators). Without an explicit id you're at the mercy of how the pass derives the default id — usually FQCN, but confirm in src/.

Keep it out of vendor-adjacent code. Attributes only work on classes you own and that the pass scans. Third-party classes and bundle-internal services still need services.yaml or a CompilerPass. A realistic split: attributes for src/, YAML for everything else.

Coexistence with services.yaml resource loading. If you already have:

App\:
    resource: '../src/'
    exclude: '../src/{Entity,Kernel.php}'

…you have two mechanisms registering the same classes. Symfony tolerates the overlap (the later definition wins) but it makes "why is this service public/tagged?" debugging miserable. Pick one per directory: either the resource: block or the annotation pass, not both.

Decorators and factories. If the attribute supports factory / decorates, use it sparingly — these are the options most likely to have shallow or absent test coverage in a package this size. For anything non-trivial, keep the definition in YAML where the behaviour is Symfony's, not the bundle's.

Testing seam. Symfony tests default to private services since 4.0. If the attribute defaults to private (it should, to match framework convention), pull services from the container in tests rather than via $container->get(), or mark test-only services public: true at the declaration.

Gotchas and Tips

The container is compiled — attributes are not read at runtime. Adding or changing an attribute requires bin/console cache:clear in dev. In CI without a warm cache this is a non-issue, but locally it produces "my attribute does nothing" confusion more than anything else in this package. If you're iterating, run bin/console cache:warmup after each edit, or work with debug:container --env=dev and a cleared cache.

Version pinning is mandatory. Last release is dated 2026-07-21 with 2 stars and 0 dependents — that combination means the package is effectively unmaintained from a community standpoint, and the version constraint is your only protection against a breaking attribute-signature change. Pin to a specific minor: "sergey-milegov/service-annotation-bundle": "~1.0" (adjust to the real tag), and commit composer.lock.

Read the compiler pass before trusting the options. Small packages often accept constructor arguments in the attribute that the pass silently ignores. Grep the pass for each option name you use (tags, public, lazy, alias). If it isn't referenced there, it's decoration.

Scan scope is the usual failure point. Annotation-based registration normally needs a configured namespace/root directory (the pass can't scan the entire filesystem). If a class in src/Module/Foo isn't registered but one in src/ is, that's a scan-path config problem, not a syntax problem. Check config/packages/service_annotation.yaml or the equivalent extension alias shown by bin/console config:dump-reference <alias>.

Watch for exclude mismatches. A class excluded in services.yaml but annotated will still be picked up (or vice versa), and the resulting error — "cannot autowire argument $x, no such service" — points at the consumer, not the missing definition. When a service vanishes, run:

bin/console debug:container 'App\Your\Class' --show-private
bin/console lint:container

lint:container catches the type errors debug:container won't.

Attributes on interfaces and abstract classes are a trap. The pass will either skip them or register a useless abstract definition; either way it fails quietly. Keep attributes on concrete classes only.

Annotations vs. attributes. If the bundle supports both @Service docblock annotations and #[Service], don't mix styles in one codebase — the docblock path requires doctrine/annotations (deprecated in recent Doctrine versions) and the two code paths rarely share test coverage. Standardise on PHP 8 attributes.

Debugging a definition you didn't write by hand. bin/console debug:container --tag=kernel.event_subscriber to confirm tagging worked, and bin/console debug:autowiring to confirm the alias resolves. If you need the raw compiled output, var/cache/dev/App_KernelDevDebugContainer.xml shows exactly what the pass emitted.

Extension point: write your own pass. The cleanest escape hatch for anything the bundle doesn't support is your own CompilerPassInterface using registerAttributeForAutoconfiguration() (Symfony 6.3+), which is essentially vendor-supported and needs no third-party bundle:

$container->registerAttributeForAutoconfiguration(
    Service::class,
    static function (ChildDefinition $d, Service $attr, \ReflectionClass $r) {
        $d->addTag('app.annotated');
    }
);

If that covers your need, you may not need this package at all — evaluate that before adding the dependency, given the maintenance profile.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource