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

Di Annotations Bundle Laravel Package

loso/di-annotations-bundle

A Symfony bundle for annotation-based dependency injection: configure services and inject dependencies directly in class annotations instead of XML or YAML. Helps keep service wiring concise and close to the code it configures.

View on GitHub
Deep Wiki
Context7

Getting Started

The honest first step: confirm you cannot install it. In a Laravel app this is a ten-minute check, not an afternoon:

composer require loso/di-annotations-bundle
# -> conflicts with php ^8.x and symfony/symfony 2.x constraints
composer why-not loso/di-annotations-bundle

If that errors (it will), the package is a reading reference and a migration marker, not a dependency. Everything below assumes you are either (a) reading it to understand annotation-driven DI before replacing it, or (b) inheriting a Symfony 2.x service that uses it and translating that wiring into Laravel.

Where to look first, depending on what you actually have:

Situation Look here first
Legacy Symfony 2.x app in your estate app/AppKernel.php for new Loso\Bundle\DiAnnotationsBundle\LosoDiAnnotationsBundle(), then grep -rn "@Inject" src/
Only a vendored copy / forked code The bundle's DependencyInjection/Annotations/ directory — that's the annotation reader and the reflection logic, the whole package in a few hundred lines
Nothing in your estate, just an audit hit composer.lock constraints — the symfony/symfony ~2.x and doctrine/annotations ~1.0 pins are the actionable signal
A mixed Symfony-2 + Laravel monorepo Any YAML/XML service definition, plus controllers that have neither a services.yml entry nor constructor args — those are the annotation-wired ones

The first use case you will actually hit. A legacy controller that looks like this:

class ReportController extends Controller
{
    /** @Inject("app.mailer") */
    protected $mailer;

    public function sendAction() { /* ... */ }
}

Your job is to read that as "this class has a hidden constructor dependency on the app.mailer service," then re-express it either as Symfony autowiring (if you're keeping Symfony) or as Laravel constructor injection (if you're porting). In Laravel, the equivalent is:

class ReportController extends Controller
{
    public function __construct(private Mailer $mailer) {}
}

No registration needed — Illuminate\Container resolves Mailer by reflection. That is the entire value proposition this bundle was built to provide, shipped natively since Laravel 5.

Do not spend time wiring the bundle's compiler pass into Laravel. There is no CompilerPass hook, no Extension, no bundle layer. Any port means reimplementing the reader against Illuminate\Container, which is strictly worse than using the container you already have.

Implementation Patterns

Translate @Inject to native Laravel, one class at a time

Treat each annotation as a constructor dependency and delete the indirection. The mapping is mechanical:

Legacy annotation Laravel native equivalent
@Inject on a property, type-hinted Constructor property promotion: __construct(private Foo $foo)
@Inject("service.id") (explicit id) Interface binding in a service provider: $this->app->bind(Foo::class, FooImpl::class)
@Inject("service.id") where two consumers need different impls Contextual binding: $this->app->when(ReportController::class)->needs(Foo::class)->give(FooImpl::class)
Setter injection Constructor injection, or $this->app->afterResolving() if genuinely lifecycle-bound
Annotation-driven service registration Autodiscovery + register() in a service provider; Laravel 11's #[Config]/#[Context] attributes cover the config-injection cases

Pattern: the anti-corruption shim during migration

While a legacy service is still live, do not let annotation-wired classes leak into your Laravel domain code. Put a single seam at the boundary:

// app/Support/Legacy/LegacyMailerGateway.php
final class LegacyMailerGateway implements MailerContract
{
    public function __construct(private \LegacyMailer $inner) {}

    public function send(Message $m): void
    {
        // translate, log, and let the old annotation-discovered object stay old
        $this->inner->send($m->toLegacyPayload());
    }
}

Bind it once in a provider, and every new class depends on MailerContract — never on the legacy service id and never on an annotation. When the legacy service dies, you delete one binding, not thirty call sites.

Pattern: inventory before you touch anything

Annotation wiring spreads because it is invisible in config. Before estimating, produce a real list:

grep -rn --include=*.php -E '@Inject|@Service|@Autowired' src/ | tee di-annotations-inventory.txt

Then, for each hit, record: class, service id if explicit, whether the class is a controller (cheap to migrate) or a shared service (needs a binding decision), and whether any test asserts the annotation behaviour. That file is your migration scope — not a guess.

Pattern: replace the compile-time magic with a boot-time provider

Symfony's compiler pass runs at container build; Laravel's equivalent moment is register()/boot() in a service provider. If you need any of the bundle's "auto-register everything with this annotation" behaviour, write it explicitly and cache it:

public function register(): void
{
    foreach ($this->annotatedServices() as $id => $class) {
        $this->app->bind($id, $class);
    }
}

private function annotatedServices(): array
{
    return Cache::rememberForever('legacy.di.map', function () {
        // scan once, return [id => class], never scan again
    });
}

This is the one pattern worth stealing from the bundle: resolve the metadata once, at boot, and never reflect per-request.

Gotchas and Tips

Composer resolution is fatal, not slow. The php ~5.3 and symfony/symfony ~2.x constraints cannot be relaxed into a PHP 8 app without a fork. If you fork to fix constraints, you have signed up to own it permanently — price that in before you start, and prefer the ~150-line in-house resolver if you truly need attribute-driven wiring.

Docblock annotations are invisible to tooling. No IDE navigation, no PHPStan/PHPCS rules, no debug:container output that shows the wiring. This is the single biggest day-to-day cost: a new hire cannot trace why $this->mailer is populated. Laravel's constructor injection is greppable; annotations are not. Weight onboarding cost heavily when defending a migration ticket.

Silent mis-injection is the characteristic failure. A typo'd service id or a class that the reader skips produces a null collaborator or a "call on null" at runtime rather than a container error. When debugging a legacy service, do not trust the annotation — add a temporary throw in the setter, or dump the container, and prove the value is actually set.

Annotation errors surface at deploy, not at test time. The reader only runs when the container warms. If your CI never warms the production container, a parse error ships green. Add a container-warm step (bin/console cache:warmup / php artisan optimize) to CI before you start touching legacy classes.

@Inject targets non-public properties via reflection, so protected $mailer "working" tells you nothing about whether a subclass overrode it. Prefer reading the bundle's DependencyInjection/Annotations/ source over guessing which targets it supports — it is short, and the supported-target list is the exact behavioural contract you are replacing.

Doctrine annotations are deprecated ecosystem-wide, replaced by PHP 8 attributes. Even conceptually, this mechanism is on a dead-end path — don't build a convention around it in a codebase you will maintain past 2024.

Extension point that actually transfers. The bundle's compiler pass is conceptually a "mutate the container before it's frozen" hook. Laravel's equivalents, in order of preference: service provider register(), $this->app->extend(), afterResolving(), and Laravel 11's ContextualAttribute. Reach for these rather than porting the pass.

Don't let the bundle be the reason an upgrade stalls. On a legacy Symfony path, upgrade Symfony first (2.x → 5.4 LTS → 6.4/7.x), delete the bundle mid-flight, and let native autowiring take over. The bundle's annotations are a small per-class edit; the surrounding Symfony version bump is the real work. Sequencing it the other way round (migrate annotations first, upgrade later) means doing it twice.

Quarantine rule of thumb. Pin the version, write the affected-class inventory to a doc, add a @deprecated note pointing at the inventory, and fold removal into the next DI-normalization window. Per class it's low effort; the surface area is the problem, so budget by the inventory count, not by "it looks small."

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin