flyokai / amphp-injector
A dependency injector for bootstrapping object-oriented PHP applications.
Requires
- php: >=8.0
- marcj/topsort: ^2.0.0
- psr/container: ^1.1 | ^2
Requires (Dev)
- amphp/php-cs-fixer-config: dev-master
- monolog/monolog: ^2.2
- ocramius/proxy-manager: ^2.14.0
- phpbench/phpbench: ^1.0
- phpunit/phpunit: ^9.5.2
Replaces
This package is auto-updated.
Last update: 2026-06-04 07:40:56 UTC
README
User docs →
README.md· Agent quick-ref →CLAUDE.md· Agent deep dive →AGENTS.md
A dependency injection container for PHP 8.1+ with weaver-based parameter resolution, ordered compositions, attribute-driven wiring, and lifecycle management.
flyokai/amphp-injector is a substantially-evolved descendant of amphp/injector. The container is now driven by weavers (composable parameter resolvers), built around an immutable Application that owns a Container and an Injector, with first-class support for Composition collections, PHP 8 attributes, lifecycle hooks, and alias resolution.
Heads up. Despite the package name and namespace, this is a fork. If you arrived here looking for upstream
amphp/injector'sInjector::make(),define(),share(),delegate()API — that API is gone. Read on for what replaced it.
Features
Application— entry point that holds aContainerandInjector, implementsLifecycle- Definition helpers —
singleton(),object(),value(),factory(),injectableFactory(),compositionFactory(),compositionItem() - Weavers —
names(),types(),runtimeTypes(),automaticTypes(),any()for parameter resolution - PHP 8 attributes —
#[ServiceParameter],#[SharedParameter],#[PrivateParameter],#[FactoryParameter(Class)] - Compositions —
CompositionOrderedwith topological sort viabefore/after/depends - Aliases — one-way interface → implementation via
AliasResolverImpl - Lifecycle —
start()in dependency order,stop()in reverse - Lazy proxies — pluggable via
ProxyDefinition(seeexamples/proxy.php)
Installation
composer require flyokai/amphp-injector
The package's composer.json replaces amphp/injector so the namespace Amp\Injector\… resolves to this fork.
Quick start
use Amp\Injector\Application; use Amp\Injector\Definitions; use Amp\Injector\Injector; use function Amp\Injector\{any, arguments, names, object, singleton, value}; class MyService { public function __construct(public array $config) {} } $definitions = (new Definitions()) ->with( singleton(object(MyService::class, arguments(names() ->with('config', value(['key' => 'val'])) ))), 'my_service', ); $application = new Application(new Injector(any()), $definitions, 'my-app'); $application->start(); /** @var MyService $svc */ $svc = $application->getContainer()->get('my_service'); $application->stop();
Build flow
- Create a
Definitionscollection containing service / object / value / factory / composition definitions. - Create an
Injectorwith a rootWeaver(typicallyany(...)chaining several weavers). - Construct
Application(injector, definitions, name, ?aliasResolver). - The
Applicationcallsdefinition->build($injector)for every definition and registers providers in theContainer. application->start()walks the providers and starts everyLifecycleinstance in dependency order.$container->get($id)retrieves services.application->stop()stops in reverse order.
Definition helpers
singleton(Definition, mustStart = false)
Wraps any definition to cache the instance — subsequent get() calls return the same object. mustStart=true requires the service to be started before first get().
singleton(object(MyService::class)); singleton(object(HttpServer::class), mustStart: true);
object(string $class, ?Arguments $arguments = null)
Prototype factory — creates a new instance via constructor reflection every call. Wrap in singleton() for sharing.
object(Foobar::class) object(Foobar::class, arguments(names() ->with('a', factory(fn() => new \stdClass())) ->with('b', value(42)) ))
value(mixed $value)
Wraps a literal — no construction logic.
value(['key' => 'val']) value(new \Monolog\Processor\PsrLogMessageProcessor())
factory(\Closure $factory, ?Arguments $arguments = null)
Prototype factory from a closure. The closure can accept a ProviderContext to inspect the injection site:
factory(function (ProviderContext $context): PsrLogger { $param = $context->getParameter(1); $name = $param?->getDeclaringClass() ?? 'unknown'; return $logger->withName($name); })
injectableFactory(string $class, ?\Closure $factory = null, ?Arguments $arguments = null)
Returns a callable from the container, not an instance. Some parameters are pre-injected; remaining ones are passed at call time:
injectableFactory(BarImpl::class) // fn(...$runtimeArgs): BarImpl => new BarImpl($injectedBaz, $injectedQux, ...$runtimeArgs)
compositionFactory(\Closure $factory, ?Definitions $itemDefinitions = null, ?Arguments $arguments = null)
Creates a composition — a collection of items built from sub-definitions. The factory receives all items as named arguments.
compositionFactory(CompositionOrdered::selfFactory(), $itemDefinitions) compositionFactory(MyCompositionImpl::selfFactory())
compositionItem(Definition $definition, array $before = [], array $after = [], array $depends = [])
Wraps a definition as a CompositionItem for use inside CompositionOrdered:
$itemDefs = definitions() ->with(object(CompositionItem::class, arguments()->with(names() ->with('after', value(['bar'])) ->with('value', object(BazImpl::class)) )), 'baz') ->with(object(CompositionItem::class, arguments()->with(names() ->with('before', value(['bar'])) ->with('value', object(FooImpl::class)) )), 'foo') ->with(object(CompositionItem::class, arguments()->with(names() ->with('value', object(BarImpl::class)) )), 'bar'); // Final order: foo → bar → baz
Weavers
Weavers resolve constructor / function parameters to definitions. They're chained inside Arguments — first match wins.
names(array $definitions = [])
Resolves by parameter name — the most common weaver:
arguments(names() ->with('config', value(['key' => 'val'])) ->with('logger', singleton(object(Logger::class))) )
types(array $definitions = [])
Resolves by parameter type — explicit class → definition mapping. Also indexes parent classes / interfaces.
types()->with(ProviderContext::class, new ProviderDefinition(new ContextProvider()))
runtimeTypes(Definitions $defs, AliasResolver $aliasResolver)
Resolves via PHP 8 attributes on parameters. Supported attributes:
| Attribute | Resolves to |
|---|---|
#[ServiceParameter] |
shared singleton instance of the parameter's type |
#[SharedParameter] |
shared instance scoped to the current definition |
#[PrivateParameter] |
new instance per injection site |
#[FactoryParameter(Class::class)] |
injectable factory (callable) returning a Class instance |
class FooImpl { public function __construct( #[PrivateParameter] protected Bar $bar, #[SharedParameter] protected Baz $baz, #[ServiceParameter] protected Qux $qux, #[FactoryParameter(Bar::class)] protected \Closure $barFactory, ) {} public function makeBar(): Bar { return ($this->barFactory)(); } }
automaticTypes(Definitions $defs, AliasResolver $aliasResolver)
Auto-wires by type from all registered definitions. Returns a definition only if exactly one matches the type — ambiguous matches return null.
$defs = definitions() ->with(object(Foo::class)) ->with(object(Bar::class)); $injector = new Injector(automaticTypes($defs)); // Bar's constructor parameter of type Foo is auto-resolved.
any(Weaver ...$weavers)
Tries multiple weavers in order, returns the first match. Typical setup:
$injector = new Injector(any( automaticTypes($defs, $aliasResolver), runtimeTypes(new Definitions(), $aliasResolver), ));
Alias resolution
Maps interfaces to implementations via AliasResolverImpl. Aliases are one-way — requesting Foo yields FooImpl, but requesting FooImpl directly resolves through FooImpl's own definition.
$aliasResolver = (new \Amp\Injector\AliasResolverImpl()) ->with(Foo::class, FooImpl::class) ->with(Bar::class, BarImpl::class); $injector = (new Injector(any(...))) ->withAlias($aliasResolver->alias(...)); $application = new Application($injector, $definitions, 'app', $aliasResolver); $application->getContainer()->get(Foo::class); // → FooImpl
Compositions (ordered collections)
CompositionOrdered items get topologically sorted via before / after / depends:
$items = definitions() ->with(object(CompositionItem::class, arguments()->with(names() ->with('after', value(['bar'])) ->with('value', object(BazImpl::class)) )), 'baz') ->with(object(CompositionItem::class, arguments()->with(names() ->with('value', object(BarImpl::class)) )), 'bar'); $ordered = compositionFactory(CompositionOrdered::selfFactory(), $items);
CompositionImpl is the simple unordered variant. Use selfFactory() as the factory closure for both.
Lifecycle
Services implementing Lifecycle are managed by the application:
start()— called after every definition is built; walks the dependency graph; starts in dependency orderstop()— called on shutdown; reverse ordersingleton($definition, mustStart: true)— service must be started before firstget()SingletonProvider->lazy()— defer initialization to firstget()instead ofstart()
Lazy proxies
Pluggable via custom Definitions using ocramius/proxy-manager. See examples/proxy.php:
$definitions = (new Definitions()) ->with(proxy(Car::class, object(Car::class)), 'car') ->with(proxy(V8::class, object(V8::class)), 'engine'); $car = $container->get('car'); // Car constructor NOT called yet $car->turnRight(); // NOW Car is constructed
The built-in ProxyDefinition currently throws not supported yet — provide your own Definition subclass.
Examples
The examples/ directory contains runnable scripts for every feature:
| File | Demonstrates |
|---|---|
singleton.php |
Basic singleton + value definitions |
runtime.php |
Attribute-driven runtime types + compositions |
delegation.php |
Factories and injectableFactory |
logger.php |
ProviderContext for site-aware factories |
proxy.php |
Lazy-loading proxy definitions |
benchmark.php |
Performance harness |
Gotchas
- Aliases are one-way.
Interface ⇒ Impllets you request the interface. RequestingImpldirectly usesImpl's own definition, not the alias. - Class names are normalised to lowercase internally — don't rely on case-sensitive keys.
- Ambiguous auto-wiring — if two definitions share a type,
automaticTypesreturnsnull. Disambiguate withnames()ortypes(). - Circular dependencies are not detected — they cause infinite recursion. Refactor or use
lazysingletons. - Containers are immutable — every
with()returns a clone; theApplicationholds the final reference. mustStartsingletons —get()beforeapplication->start()throwsLifecycleException.- Variadic parameters — only supported via
injectableFactory(). Plainfactory()doesn't pass variadics through. - Built-in
ProxyDefinitionis not implemented — seeexamples/proxy.phpfor a custom approach.
Differences from upstream amphp/injector
| Upstream | This fork |
|---|---|
Injector::make(), define(), share(), delegate(), prepare() |
gone — replaced by Application + Definitions + weavers |
define() arrays |
arguments(names()->with(...)) |
share() |
singleton(...) |
alias() (Injector method) |
AliasResolverImpl (separate object) |
| Per-injector instance API | Immutable Application / Container / Definitions |
| No compositions | Composition, CompositionOrdered, CompositionItem first-class |
| No attribute-driven wiring | #[ServiceParameter], #[SharedParameter], #[PrivateParameter], #[FactoryParameter] |
| No formal lifecycle | Lifecycle::start() / stop() walked in dependency order |
See also
flyokai/application— uses this DI as the runtime container; see alsovendor/flyokai/flyokai/docs/dependency-injection.mdfor diconfig structureflyokai/composition— module-level topological sort (different layer thanCompositionOrdered)flyokai/generic—TunerContainer/ExecutionContaineruse compositions internally- Original: https://github.com/amphp/injector
License
MIT