phug/dependency-injection
Phug Dependency Injection is a lightweight PHP DI container offering autowiring, lazy service resolution, and flexible configuration for clean, testable applications.
Install
composer require phug/dependency-injection
In practice you will rarely install this directly. It is one of the split packages of the phug-php/util monorepo and arrives transitively whenever you pull in phug/phug, phug/renderer, or the Phug formatter/filter stack. Check whether it is already in your tree before adding it explicitly:
composer why phug/dependency-injection
Where to look first
vendor/phug/dependency-injection/src/ — the whole package is a handful of classes. Read the container class top to bottom; it is small enough to finish in one sitting. Confirm the exact namespace against the installed version (Phug\DependencyInjection\…) rather than trusting a blog post.vendor/phug/dependency-injection/tests/ — the test suite is the real API documentation here. With 1 star and 0 dependents on Packagist, there is no tutorial ecosystem to fall back on.composer.json of the package — note the replace/provide entries and the branch-alias. The GitHub repo is phug-php/util, a monorepo; the package directory is what Composer actually installs, so issues and PRs go to the monorepo, not to a standalone repo.First use case
The realistic first use is the one Phug itself has: holding the shared objects that a renderer needs — formatters, filters, extensions, adapters — so that a template engine can be constructed once and reused across requests instead of re-instantiating dependencies per render.
use Phug\DependencyInjection\Container;
$container = new Container();
$container->set('formatter', function () {
return new MyFormatter();
});
$formatter = $container->get('formatter');
Once that round-trip works, wire the same container into your renderer/extension bootstrap and pull services out of it instead of calling new at the call site. That is the entire day-to-day value of the package: one place to swap an implementation, one place to stub it in tests.
Register lazily, resolve at the edge. Store closures, not instances, and call get() only where the dependency is actually needed. This keeps bootstrap cheap and avoids constructing a formatter on every request that never renders a template.
$container->set(MyFormatter::class, fn () => new MyFormatter());
$container->set(MyFilter::class, fn (Container $c) => new MyFilter($c->get(MyFormatter::class)));
Passing the container into the factory closure is the standard hand-rolled wiring pattern — the package gives you the registry, not the graph resolution, so the constructor arguments are yours to state explicitly.
Shared vs. transient. Decide per service whether repeated get() calls should return the same object. Anything stateful (a compiled-template cache, a PSR-3 logger adapter, a connection) must be shared; anything cheap and stateless can be transient. If the container offers only one behaviour, wrap the transient case in a factory service rather than relying on the container to re-instantiate.
Constructor injection into Phug extensions. Phug modules and extensions are the natural consumers. Build the container in your bootstrap, then construct extensions with their dependencies pulled from it:
$container->set(AssetExtension::class, fn (Container $c) =>
new AssetExtension($c->get('asset.manifest'), $c->get('asset.base_path'))
);
$renderer = new \Phug\Renderer([
'extensions' => [$container->get(AssetExtension::class)],
]);
This keeps extensions in the options array as a list of configured objects, and keeps all the "which manifest, which base path" decisions in one bootstrap file rather than sprinkled through templates and controllers.
PSR-11 interop. If a third-party library type-hints Psr\Container\ContainerInterface, register your services there too, or register the Phug container as the delegate. Keep one container as the source of truth — two parallel registries drift within a sprint.
Testing. Because everything is keyed and registered in bootstrap, tests get a free seam: build a container with stub factories for the slow or external services, and assert that your extension asks for what it should.
$container = new Container();
$container->set('asset.manifest', fn () => new InMemoryManifest(['app.css' => '/a.css']));
$extension = $container->get(AssetExtension::class);
self::assertSame('/a.css', $extension->resolve('app.css'));
Aliases as a migration tool. When you rename a service or move it behind an interface, register the new key and alias the old one, then delete the alias in a follow-up commit. With no community churn to guide you, your own deprecation hygiene matters more than usual.
It is not an autowiring container. There is no reflection-based resolution, no attribute scanning, no compile step. Every argument is explicit. If you are coming from Symfony or Laravel, expect to write substantially more bootstrap code and to miss Autowire-style conveniences. Budget for a services.php that grows linearly with your service count.
The GitHub URL in the metadata points at the monorepo. https://github.com/phug-php/util is the umbrella repo; the split package is what Composer installs. When you file a bug or read history, read it in monorepo context — a commit touching dependency-injection may have been made to satisfy phug/renderer. Do not assume a package-specific changelog exists; check the monorepo tags.
Treat version pinning as mandatory. One star, zero dependents, and a release date that is effectively forward-dated relative to normal Packagist cadence mean you are the only QA this package has. Pin an exact version ("phug/dependency-injection": "1.0.2" or your verified tag) and commit composer.lock. Do not use a caret constraint on a package this thin unless you have a lockfile you actually honour.
Namespace collisions with Phug\Util. Both packages ship from the same monorepo and share a maintainer and conventions. It is easy to import the wrong class. Let the IDE autocomplete from vendor/ rather than from memory, and verify the use statement resolves before debugging a Class not found at runtime.
No lifecycle, no scopes. There is no request scope, no dispose hook, no circular-dependency detection that will explain itself. A circular factory will recurse until the stack blows. When you hit an unexplained fatal during bootstrap, the fastest diagnostic is to add a log line inside each factory closure and see which one repeats.
Debugging a missing service. The two failure modes are "key never registered" and "key registered under a different string". Dump the keys the container knows about (var_dump(array_keys($container->getItems())) or whatever accessor your version exposes — check the source) and grep your bootstrap for the string. Prefer ::class constants over string keys precisely so this is greppable.
Extension points are limited, so use decorators. The package deliberately does very little. If you need resolution logic — defaults, aliases with lazy targets, conditional wiring — implement it in your own thin wrapper class that composes the container and exposes get/set, rather than forking or patching the package. That keeps upgrades trivial and keeps your logic testable.
Watch for the transitive upgrade path. Because phug/renderer and friends pull this in, a composer update on the renderer can silently bump the container under you. After any Phug-family update, re-run your render smoke tests — the failure surfaces as a template-time error, far from the dependency that changed.
How can I help you explore Laravel packages today?