PHPackages                             phpdot/container - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. phpdot/container

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

phpdot/container
================

Server-agnostic service scoping for PHP-DI

v1.9.0(1mo ago)065↓90.9%20MITPHPPHP &gt;=8.3

Since Mar 30Pushed 1mo agoCompare

[ Source](https://github.com/phpdot/container)[ Packagist](https://packagist.org/packages/phpdot/container)[ RSS](/packages/phpdot-container/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (16)Versions (16)Used By (20)

phpdot/container
================

[](#phpdotcontainer)

Server-agnostic service scoping for PHP-DI.

Adds lifecycle management (Singleton, Scoped, Transient) on top of [PHP-DI](https://github.com/php-di/php-di) without replacing its resolution engine. Full autowiring, compilation, and all native features preserved.

The Problem
-----------

[](#the-problem)

### Traditional PHP-FPM

[](#traditional-php-fpm)

Every request spawns a fresh process. Everything is created from scratch and destroyed after the response. No sharing, no leaks.

```
Request 1                    Request 2                    Request 3
┌──────────────────┐        ┌──────────────────┐        ┌──────────────────┐
│ Process 1        │        │ Process 2        │        │ Process 3        │
│                  │        │                  │        │                  │
│  Router (new)    │        │  Router (new)    │        │  Router (new)    │
│  Config (new)    │        │  Config (new)    │        │  Config (new)    │
│  Request (new)   │        │  Request (new)   │        │  Request (new)   │
│  Session (new)   │        │  Session (new)   │        │  Session (new)   │
│  DB conn (new)   │        │  DB conn (new)   │        │  DB conn (new)   │
│                  │        │                  │        │                  │
│  Response → die  │        │  Response → die  │        │  Response → die  │
└──────────────────┘        └──────────────────┘        └──────────────────┘
     Born → Die                  Born → Die                  Born → Die

```

Simple. Safe. But slow — every request pays the full bootstrap cost (autoloader, config parsing, DB connection).

### Persistent Runtimes (Swoole, RoadRunner, FrankenPHP)

[](#persistent-runtimes-swoole-roadrunner-frankenphp)

One process handles many requests. Services created once, reused across requests. Fast — but dangerous.

```
┌──────────────────────────────────────────────────────────────┐
│ Worker Process (lives forever)                               │
│                                                              │
│  ┌─────────────── Shared (Singletons) ───────────────────┐  │
│  │  Router        Config        Redis Pool     LogBridge  │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Request 1 (coroutine)    Request 2 (coroutine)    ...       │
│  ┌────────────────────┐  ┌────────────────────┐             │
│  │  Request  (own)    │  │  Request  (own)    │             │
│  │  Session  (own)    │  │  Session  (own)    │  concurrent │
│  │  Signal   (own)    │  │  Signal   (own)    │             │
│  │  Auth     (own)    │  │  Auth     (own)    │             │
│  └────────────────────┘  └────────────────────┘             │
│        ↑ isolated              ↑ isolated                    │
│                                                              │
│  ⚠️  Problem: PHP-DI caches EVERYTHING as singletons.       │
│      Request from coroutine 1 leaks into coroutine 2!       │
└──────────────────────────────────────────────────────────────┘

```

### The Danger Without Scoping

[](#the-danger-without-scoping)

```
// PHP-DI default behavior: get() caches as singleton
$container->get(Session::class);  // Request 1: creates Session for User A
$container->get(Session::class);  // Request 2: returns User A's session! 💥

// User B sees User A's data. Security breach.
```

### The Solution: Three Scopes

[](#the-solution-three-scopes)

```
┌──────────────────────────────────────────────────────────────┐
│ Worker Process                                               │
│                                                              │
│  ┌─── Singleton (process lifetime) ──────────────────────┐  │
│  │  Router        Config        Redis        LogBridge    │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─── Scoped (per request) ──┐  ┌─── Scoped (per request)┐  │
│  │  Request   → User A       │  │  Request   → User B    │  │
│  │  Session   → User A       │  │  Session   → User B    │  │
│  │  Signal    → trace-abc    │  │  Signal    → trace-xyz  │  │
│  │  Auth      → User A       │  │  Auth      → User B    │  │
│  └───────────────────────────┘  └─────────────────────────┘  │
│        ✅ isolated                    ✅ isolated             │
│                                                              │
│  Transient: MailMessage — always new, never cached           │
└──────────────────────────────────────────────────────────────┘

```

This library adds these three scopes to PHP-DI. One library, any runtime.

Installation
------------

[](#installation)

```
composer require phpdot/container
```

Quick Start
-----------

[](#quick-start)

```
use Phpdot\Container\ContainerBuilder;
use function Phpdot\Container\singleton;
use function Phpdot\Container\scoped;
use function Phpdot\Container\transient;

$container = (new ContainerBuilder())
    ->addDefinitions([
        // Singleton — cached forever (same as PHP-DI default)
        Router::class => singleton(),
        Redis::class  => singleton(fn() => new Redis($config)),

        // Scoped — cached per request/context, fresh across requests
        SignalManager::class => scoped(fn($c) => new SignalManager($c->get(LogBridge::class))),
        Session::class       => scoped(fn($c) => Session::fromCookie($c->get(Request::class))),

        // Transient — always a new instance
        MailMessage::class => transient(),

        // Raw PHP-DI definitions work unchanged
        'config.name' => \DI\value('MyApp'),
    ])
    ->build();
```

Three Scopes
------------

[](#three-scopes)

ScopeLifetimeShared?Use For**Singleton**Entire processAll requestsStateless services: Router, Config, Redis pools**Scoped**One request/contextWithin request onlyPer-request state: Session, Auth, SignalManager**Transient**NoneNeverThrowaway objects: MailMessage, DTO builders### How They Behave

[](#how-they-behave)

```
// Singleton — same instance forever
$a = $container->get(Router::class);
$b = $container->get(Router::class);
assert($a === $b); // true, even across requests

// Scoped — same within a request, different across requests
$req1User = $container->get(AuthUser::class); // request 1
$req1Same = $container->get(AuthUser::class); // same request
assert($req1User === $req1Same); // true

// ... new request starts (context switches) ...
$req2User = $container->get(AuthUser::class); // request 2
assert($req1User !== $req2User); // true — fresh instance

// Transient — always new
$a = $container->get(MailMessage::class);
$b = $container->get(MailMessage::class);
assert($a !== $b); // true
```

Context Providers
-----------------

[](#context-providers)

The library needs to know what "current request" means. A context provider answers that question for each runtime:

```
use Phpdot\Container\ContainerBuilder;

// FPM (default) — one process = one context
$builder = new ContainerBuilder();
// No provider needed, uses ArrayContextProvider automatically

// Swoole — one coroutine = one context
$builder->withContextProvider(new SwooleContextProvider());

// Custom runtime
$builder->withContextProvider(new CallbackContextProvider(
    fn() => $myRuntime->getCurrentContext()
));
```

### Built-in Providers

[](#built-in-providers)

ProviderRuntimeHow It Works`ArrayContextProvider`FPM / CLISingle context — the process is the context`CallbackContextProvider`CustomYour closure returns the current context`TestContextProvider`PHPUnitSimulate context switching in tests### Adapter Packages (separate repos)

[](#adapter-packages-separate-repos)

```
composer require phpdot/container-swoole    # SwooleContextProvider
```

Registration Methods
--------------------

[](#registration-methods)

### Helper Functions (recommended)

[](#helper-functions-recommended)

```
use function Phpdot\Container\singleton;
use function Phpdot\Container\scoped;
use function Phpdot\Container\transient;

$builder->addDefinitions([
    // Class → autowired
    Router::class => singleton(),

    // Interface → implementation
    CacheInterface::class => singleton(RedisCache::class),

    // Factory closure
    LoggerInterface::class => scoped(function (ContainerInterface $c) {
        return new FileLogger($c->get('config.log_path'));
    }),

    // Always new
    MailMessage::class => transient(),
]);
```

### PHP Attributes

[](#php-attributes)

```
use Phpdot\Container\Attribute\Singleton;
use Phpdot\Container\Attribute\Scoped;
use Phpdot\Container\Attribute\Transient;

#[Singleton]
class Router { }

#[Scoped]
class AuthenticatedUser { }

#[Transient]
class MailMessage { }
```

Scan for attributes:

```
$builder->scanAttributesIn(__DIR__ . '/src');
```

### Loading Definitions from a File

[](#loading-definitions-from-a-file)

`addDefinitionsFromFile()` loads a PHP file that returns an array of definitions and merges it into the builder. Throws if the file is missing or doesn't return an array — silent no-ops hide bugs.

```
$builder->addDefinitionsFromFile(__DIR__ . '/config/services.php');
```

The file looks like any normal definitions file:

```
