Overview#
InterMix is a “zero-config-until-you-want-it” dependency-injection container. Start with one line, stay productive when your project grows – lifetimes, scopes, debug tracing, cache, preload generation … all optional.
Why another container?#
Simple first – one-liner definitions, no config files
Reflection-aware – autowiring you can switch off
Attribute powered –
#[Infuse],#[Autowire],#[Inject]Fluent API – four tiny managers that chain like one object
Performant – static reflection cache, parameter-resolution planning cache, optional PSR-6/16 cache, lazy services by default
Production ready – env-specific bindings, scoped lifetimes, preload file generator
Debuggable – built-in tracer (node / verbose)
15-second “hello world”#
use function Infocyph\InterMix\container;
interface Clock { public function now(): DateTimeImmutable; }
class SystemClock implements Clock
{ public function now(): DateTimeImmutable { return new DateTimeImmutable(); } }
class Greeter
{
public function __construct(private Clock $clock) {}
public function greet(string $name): string
{
return 'Hello '.$name.' — '.$this->clock->now()->format('c');
}
}
$c = container()
->definitions()->bind(Clock::class, SystemClock::class);
echo $c->get(Greeter::class)->greet('Alice');
// → “Hello Alice — 2025-06-18T12:34:56+00:00”
Managers & call chain#
Every manager shares a tiny proxy trait – so you can hop around fluently and still land back on the main container:
$c->definitions()
->bind(Logger::class, FileLogger::class)
->options()
->setOptions(injection:true)
->enableLazyLoading()
->registration()
->registerClass(App::class)
->invocation()
->call(App::class, 'boot')
->lock();
That proxy is ManagerProxy and it contributes three important behaviors:
Magic access:
$mgr->serviceId,$mgr('serviceId'),$mgr['serviceId'].Method pass-through: unknown manager calls are forwarded to the container.
Chain safety: if a forwarded call returns the container, the manager keeps the fluent chain.
Ascii peek#
+---------------------+
| DefinitionManager |
+----------+----------+
^
| .definitions()
+-----------------+ .options | | .registration()
| OptionsManager +-----------+ v
+-----------------+ +---------------------+
| RegistrationManager|
.invocation() +----------+--------+
^ |
| |
| v
+----+---------------+
| InvocationManager |
+--------------------+
The shared Repository (not pictured) stores:
functionReference – your definitions
classResource – extra constructor / method / property data
resolved / resolvedDefinition – cached objects & values
conditionalBindings – environment overrides
How resolution works#
When you call get(), getReturn() or call() InterMix walks the
pipeline below – applying lazy placeholders, caching and autowiring as
needed.
Already resolved?
Return immediately if found in the in-memory cache.
If the cache entry is a
DeferredInitializerlazy wrapper, execute it now and swap in the real object.
FunctionReference lookup
If the ID exists in your definitions, InterMix runs
resolveDefinition($id)which honours caching, env overrides, user closures, etc.Fallback: class name
If autowiring is on, reflection builds the class (constructor injection, property/parameter attributes, method call). If autowiring is off, the lightweight
GenericCallpath instantiates without reflection magic.Cache layer
With definition caching enabled, cache is consulted. Resolution results are stored back for next time.
User closure vs. lazy#
User-supplied closure
$c->definitions()->bind('heavy', fn () => new Expensive());
is executed immediately – you asked for a closure.
DeferredInitializer
For class strings/arrays and
enableLazyLoading(true), InterMix stores a small wrapper and postpones construction until the first realget().
Concurrency note#
Reflection metadata lives in a static cache. In common process-per-request set-ups (PHP-FPM, CLI), this is safe. In rare multi-thread situations (Swoole, ReactPHP, pthreads) you might clear or synchronise that cache manually.
Parameter resolution cache note#
InterMix caches parameter resolution plans, attribute metadata and reflection-derived lookup data. It does not cache resolved runtime argument values inside ParameterResolver. Runtime object reuse is controlled by the container lifetime layer: Singleton, Scoped or Transient.
Typical lifecycle#
Create a container (alias per app / test / worker)
Bind & register – definitions, classes, methods, properties
Tune options – autowire on/off, attributes, environment, cache …
Resolve with
get() / call() / make() / getReturn()(optional) Lock the container to freeze configuration
Next steps#
If you like to learn by code, jump straight to Quick-Start. Prefer concepts first? start with Understanding Dependency Injection & Container. Either way – happy mixing!