incompass/injection-bundle
A dependency-injection bundle for PHP that centralizes service registration and container configuration, reducing boilerplate and making it easier to wire, replace, and test application components.
First, confirm you are on Symfony. This is a Symfony bundle (IncompassInjectionBundle, registered via config/bundles.php), not a Laravel package. It hooks the Symfony kernel, DI extension, and compiler-pass lifecycle. There is no service provider, no config/injection.php to publish, and no path by which it loads into a Laravel app. If your composer.json requires laravel/framework, stop here — use the framework container, constructor injection, and AppServiceProvider contextual binding instead. Everything below assumes a Symfony host app where the package is a business requirement (usually inherited legacy code).
Install and register (Symfony only):
composer require incompass/injection-bundle
// config/bundles.php
return [
// ...
Incompass\InjectionBundle\IncompassInjectionBundle::class => ['all' => true],
];
Before you write a single annotation, verify three things against src/ in the repo (there is essentially no documentation, so the repo is the source of truth, not the README):
Incompass\InjectionBundle\Annotation\Inject is the likely path but confirm it — a wrong use statement fails silently at scan time and you will debug the container, not the annotation.doctrine/annotations installed and the reader wired — it is not enabled by default on Symfony 6+. This is the single most common reason the bundle appears to do nothing.DependencyInjection/Compiler/ first. It is the entire behaviour of the package. Its getPriority() and whether it runs TYPE_BEFORE_OPTIMIZATION determines whether you can inject things that other passes still need to touch.First use case — a dependency you cannot put on the constructor.
The bundle earns its keep only when you have a class whose constructor you cannot or will not touch: vendor services, legacy classes with long constructor signatures, or a service where only one method needs one collaborator. Property injection is the canonical fit:
namespace App\Service;
use Incompass\InjectionBundle\Annotation\Inject; // verify namespace
class ReportBuilder
{
/** @Inject("doctrine.orm.entity_manager") */
private $em;
public function build(): array
{
return $this->em->getRepository(Report::class)->findAll();
}
}
First thing to run after wiring one class:
bin/console cache:clear
bin/console debug:container App\Service\ReportBuilder --show-arguments
If the injected reference is not in the --show-arguments output, the pass is not scanning your class and every downstream assumption is wrong. Fix that before adding a second @Inject.
Property injection for cross-cutting or optional dependencies. Loggers, entity managers, event dispatchers, and mailers are the classic case. Keeping them off the constructor lets you new ReportBuilder() in a unit test without a container at all. This is the pattern 90% of real usage collapses into.
Setter injection for required dependencies. When the dependency must be present, prefer a setter the pass can call after instantiation. That gives you a single place to assert non-null and produces a clearer failure than a silently-null property. If the pass supports it, use it; if not, wrap property access with an explicit assert($this->x !== null) at the top of the consuming method.
Never mix styles on one class. A class using @Inject alongside constructor injection is a debugging trap — the container report will show the constructor args but not the reflection-written properties, so debug:container lies about what the object actually holds. Pick one style per class and stick to it.
Reference services by id, not by class, when the bundle is annotation-only. @Inject("doctrine.orm.entity_manager") is the safe form. Class-name autowiring through a third-party scan is exactly where annotation-based DI historically breaks (aliases, decorators, and the _defaults block in services.yaml do not participate the way native autowiring does).
Integration tips:
incompass_injection:, set the scan path explicitly rather than relying on the default src/.autowire/autoconfigure, verify the bundle's pass runs after autowiring has run, otherwise it will see argument lists that are not yet populated.Laravel incompatibility is not a config issue — it is a hard stop. No amount of composer require in a Laravel app produces a working container. If the request that brought you here framed it as a Laravel package, that is a filing error. The equivalent in Laravel is:
$this->app->when(Foo::class)->needs(...)->give(...) for contextual binding.#[Bind]/tagged bindings for the rest.That is native, statically analyzable, IDE-navigable, and does not require you to own a fork.
Silent mis-injection is the dominant failure mode. Annotation/attribute scanning fails at runtime, not compile time. Rename a property, move a namespace, or change a service id and the container boots green — but the property is null until the exact code path that touches it runs in the environment where it first executes. Add an integration test that instantiates the service and exercises the injected dependency, or you will ship this.
Container cache staleness after deploy. Annotation scanning is a build-time step. A deploy that ships new classes but reuses the warmed container produces "works locally, not in staging" bugs. Make cache:clear/cache:warmup a deploy step and verify the pass runs after every composer install.
Doctrine annotations are on a deprecation clock. If the bundle only supports annotations (not PHP 8 attributes), you are betting on doctrine/annotations, which is maintenance-mode. Any future Symfony major that removes annotation support removes your DI configuration with it. Plan the port to native #[Autowire]/#[Required]/#[TaggedIterator] as a dated ticket, not a someday.
Debugging checklist when the injection does not happen:
bin/console debug:container <id> --show-arguments — is the argument/reference present?bin/console debug:container --tag=<any-tag-the-pass-adds> — did the pass run at all?doctrine/annotations loaded and the reader registered?cache:clear — 60% of "the pass isn't running" is a stale container cache.Extension points. The description advertises "custom providers." Look in the repo for an interface (something like a ProviderInterface or a registry) the compiler pass iterates. If it exists, that is the only sanctioned way in the container; if it does not, there is no extension point and you are patching the compiler pass directly — treat that as a fork and budget accordingly.
Support reality: zero stars, zero dependents, no release cadence, no visible maintainer. Every issue you hit, you own. Pin an exact version in composer.lock, isolate the bundle to the legacy module that needs it, and log its removal as tech debt for the next major rather than expanding its footprint.
How can I help you explore Laravel packages today?