Search by

internal / container

Lightweight PSR-11 container for CLI tools: autowiring, scopes, inflectors and explicit destruction without a DI framework.

Maintainers

Package info

github.com/php-internal/container

pkg:composer/internal/container

Transparency log

Fund package maintenance!

Boosty

Statistics

Installs: 340

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.1 2026-09-07 21:11 UTC

This package is auto-updated.

Last update: 2026-09-07 21:11:22 UTC


README

Container

A lightweight PSR-11 container for CLI tools

Vibe Index Support


A small PSR-11 container for command-line tools and other short-lived PHP processes: autowiring, a handful of bindings and deterministic cleanup without a full-blown DI framework. No compilation, no config files, no lazy proxies, nothing to set up beyond new ObjectContainer(). It was born in Trap, grew up in DLoad and Testo, and is extracted here so all of them can share one implementation.

What it does:

  • Resolves classes by constructor autowiring through yiisoft/injector; resolved services are cached.
  • Accepts bindings as a factory closure, an alias class name or predefined constructor arguments.
  • Creates objects that implement Factoriable through their static create() method with autowired parameters.
  • Passes every resolved object through registered Inflectors before it is cached.
  • Opens nested scopes: services resolved inside a scope live only until the scope closes.
  • Destroys managed services on destroy(), see internal/destroy.

Installation

composer require internal/container

PHP Latest Version on Packagist License Total Downloads

Usage

use Internal\Container\Container;
use Internal\Container\ObjectContainer;

$container = new ObjectContainer();

// Autowired on first request, cached afterwards
$downloader = $container->get(Downloader::class);

// Factory binding
$container->bind(Logger::class, static fn(Container $c) => new Logger($c->get(OutputInterface::class)));

// Alias binding: an interface resolves to a concrete class
$container->bind(ClockInterface::class, SystemClock::class);

// Constructor arguments binding
$container->bind(HttpClient::class, ['timeout' => 30]);

// Register an existing instance; `destroy: true` hands its lifecycle over to the container
$container->set($output, OutputInterface::class, destroy: true);

// A fresh instance that is not cached
$request = $container->make(Request::class, ['uri' => '/']);

Factoriable

A class that needs custom construction implements Factoriable and exposes a static create(). The container calls it with autowired parameters instead of the constructor.

use Internal\Container\Factoriable;

final class Config implements Factoriable
{
    private function __construct(private readonly Logger $logger) {}

    public static function create(Logger $logger): self
    {
        return new self($logger);
    }
}

Inflectors

An Inflector sees every object right after it is resolved and may replace or configure it.

use Internal\Container\Container;
use Internal\Container\Inflector;

$container->addInflector(new class implements Inflector {
    public function inflect(object $object, Container $container): object
    {
        $object instanceof LoggerAwareInterface and $object->setLogger($container->get(Logger::class));
        return $object;
    }
});

Scopes

scope() runs a closure against a child state. Bindings are inherited, cached services are cloned into the scope (readonly objects and enums are shared as is), and everything resolved inside is destroyed when the closure returns. The parent state is left untouched.

$result = $container->scope(static function (Container $scoped): Result {
    $scoped->set(new SuiteConfig(...));

    return $scoped->get(SuiteRunner::class)->run();
});

Scopes are fiber-aware: a scope opened outside an event loop is the active one for everything that runs on the loop under it. Opening a scope inside a loop-driven fiber and suspending within it is not supported.

Testing

composer test