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.

  1. Already resolved?

    • Return immediately if found in the in-memory cache.

    • If the cache entry is a DeferredInitializer lazy wrapper, execute it now and swap in the real object.

  2. FunctionReference lookup

    If the ID exists in your definitions, InterMix runs resolveDefinition($id) which honours caching, env overrides, user closures, etc.

  3. Fallback: class name

    If autowiring is on, reflection builds the class (constructor injection, property/parameter attributes, method call). If autowiring is off, the lightweight GenericCall path instantiates without reflection magic.

  4. 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 real get().

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#

  1. Create a container (alias per app / test / worker)

  2. Bind & register – definitions, classes, methods, properties

  3. Tune options – autowire on/off, attributes, environment, cache …

  4. Resolve with get() / call() / make() / getReturn()

  5. (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!