randomphp/scoped

A small library implementing DI scopes on top of PHP-DI

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/randomphp/scoped

0.1.0 2026-02-18 21:50 UTC

This package is auto-updated.

Last update: 2026-02-18 21:55:49 UTC


README

RandomPHP

RandomPHP Scoped

Directory-driven Dependency Injection scopes for PHP, built on top of PHP-DI.

RandomPHP Scoped adds hierarchical, lifecycle-aware scopes on top of PHP-DI using a filesystem-driven configuration model. Each directory represents a scope. Each subdirectory represents a nested sub-scope. Definition files inside each directory are standard PHP-DI definition files.

This allows you to model application lifecycles such as:

  • Long-lived root application container
  • Per-request scopes
  • Per-job / per-message scopes
  • Nested contextual scopes (e.g.request.user, request.transaction)
  • CLI execution scopes
  • Worker attempt scopes

Requirements

  • PHP >= 8.4

Installation

Install via Composer:

composer require randomphp/scoped

Core Concepts

RandomPHP Scoped introduces the concept of runtime invocation scopes layered on top of PHP-DI.

Each scope:

  • Has its own container
  • Can override parent definitions
  • Can access parent definitions when not overridden
  • Can register teardown finalizers
  • Is destroyed automatically when invocation completes

Scopes are created and destroyed dynamically via invokeScope().

The root scope is long-lived. Sub-scopes are typically short-lived.

Directory Structure and Scope Hierarchy

The library builds the scope hierarchy from a directory structure.

Example:

di/
  00-root.php
  10-services.php
  request/
    00-request.php
    user/
      00-user.php
  cli/
    00-cli.php

Meaning:

  • di/ → root scope
  • di/request/ → sub-scope named request
  • di/request/user/ → nested sub-scope user under request
  • di/cli/ → sub-scope named cli

Rules:

  • All *.php files in a directory are loaded as definition files.
  • Files are sorted alphabetically.
  • Each subdirectory name becomes the scope name.
  • Scope nesting mirrors directory nesting.

PHP-DI definition files

A definition file is a PHP file that returns an array of definitions:

<?php

use function DI\create;
use function DI\get;

return [
    App\Config::class => create(App\Config::class),
    'db.dsn' => 'sqlite::memory:',
    App\Db::class => create(App\Db::class)
        ->constructor(get('db.dsn')),
];

See PHP-DI documentation for all supported helpers.

Usage

Creating the Root Scope

use RandomPHP\Scoped\RootScope;

$root = RootScope::create(__DIR__ . '/di');

RootScope::create parameters:

  • definitionRootDirectory (required)
  • autowiring (optional)
  • subScopeMutationDefinitionFile (optional)
  • globalFinalizers (optional)

Working With Scopes

You can use root like a normal container:

$db = $root->get(App\Db::class);

$service = $root->make(App\Service::class);

$result = $root->call([App\Handler::class, 'handle']);

Nested Scopes

Invoke a sub-scope:

use RandomPHP\Scoped\Interface\ScopeInterface;

$response = $root->invokeScope('request', function (ScopeInterface $scope) {
    $handler = $scope->get(App\Http\RequestHandler::class);
    return $handler->handle();
});

Nested invocation:

$root->invokeScope('request', function (ScopeInterface $requestScope) {
    return $requestScope->invokeScope('user', function (ScopeInterface $userScope) {
        return $userScope->get(App\Auth\UserContext::class);
    });
});

Scope Path Introspection

Each scope knows its path:

$root->invokeScope('request', function (ScopeInterface $scope) {
    echo $scope->getScopePath(); // root.request
});

Parent Resolution Rules

Resolution order:

  1. Current scope definitions
  2. Exported definitions
  3. Parent scope definitions
  4. Continue upward until root

Children override parents.

Parents cannot override children.

Passing Definitions at Invocation Time

use function DI\value;

$root->invokeScope(
    'request',
    function (ScopeInterface $scope) {
        return $scope->get('request.id');
    },
    definitions: [
        'request.id' => value(bin2hex(random_bytes(16))),
    ]
);

These definitions apply only for that invocation.

Exporting Definitions to Sub-Scopes

Parents may export definitions into direct children.

use function DI\value;

$root->exportDefinition('request', 'request.id', value('abc-123'));

When request scope is invoked, it will include this definition.

Export rules:

  • Only direct child scope names allowed
  • Export applies to all future invocations
  • Children cannot export to parents

Use exportDefinition() when the parent wants to inject contextual runtime values.

Sub-Scope Mutation Definition File

You may provide a mutation file applied to all non-root scopes.

$root = RootScope::create(
    definitionRootDirectory: __DIR__ . '/di',
    subScopeMutationDefinitionFile: __DIR__ . '/di/_mutations.php',
);

This file is included automatically in every sub-scope container.

Use cases:

  • Decorators
  • Logging context injection
  • Cross-cutting overrides
  • Per-scope instrumentation

Using DI\decorate()

When using DI\decorate() (or any overriding definition), file load order matters. Definition files inside a scope directory are loaded alphabetically, and PHP-DI applies definitions according to their load priority. Decorators must be loaded before the definition they are meant to decorate; otherwise, the decoration will not apply. For this reason, it is recommended to prefix filenames numerically (e.g. 00-decorators.php, 10-services.php) to ensure decorators are processed first. See the PHP-DI documentation on definition source priorities for more details: https://php-di.org/doc/definition-overriding.html#priorities-of-definition-sources

Decorators (DI\decorate()) only apply to definitions that exist within the same scope container. A child scope cannot directly decorate a definition that exists only in a parent scope. This is because parent entries are exposed to children only when no local definition exists for that entry. If a child defines a decorator for a parent-only entry, the parent definition is no longer bridged into the child container, and the decoration will not apply (and may result in a resolution error).

If you need to alter parent services within a child scope, you must redefine the entry explicitly in the child scope (for example using a factory that retrieves the parent entry and wraps it).

Using exportDefinition() to inject a DI\decorate() definition into a child scope is not an intended use case and may lead to undefined or surprising behavior. Decoration relies on predictable definition source ordering and the presence of a concrete base definition within the same scope container. Exported definitions are merged dynamically during scope invocation, which can interfere with PHP-DI’s decoration resolution rules. For this reason, exportDefinition() should be used for injecting contextual values or concrete definitions — not for cross-scope decoration.

Autowiring Options

By default: no autowiring.

Enable reflection-based autowiring:

use DI\Definition\Source\ReflectionBasedAutowiring;

$root = RootScope::create(
    definitionRootDirectory: __DIR__ . '/di',
    autowiring: new ReflectionBasedAutowiring(),
);

The project also provides a strict no-discovery autowiring helper if you want reflection-based wiring without implicit resolution.

There is also a RandomPHP\Scoped\Container\NoDiscoveryReflectionBasedAutowiring helper in this project, which enforces that only explicit autowire definitions are allowed (i.e. it won’t “discover” entries dynamically unless they’re explicitly defined as autowire definitions). This can be useful if you want reflection-based wiring but still want strict control over what can be resolved.

Finalizers and Scope Teardown

Scopes support teardown hooks called finalizers.

Finalizers:

  • Implement FinalizerInterface
  • Provide getPriority(): int
  • Provide execute(): void
  • Run when scope is destroyed
  • Sorted descending by priority

Example:

use RandomPHP\Scoped\Interface\FinalizerInterface;

final class CloseConnectionFinalizer implements FinalizerInterface
{
    public function __construct(private App\Connection $conn) {}

    public function execute(): void
    {
        $this->conn->close();
    }

    public function getPriority(): int
    {
        return 100;
    }
}

Register inside a scope:

$scope->addFinalizer($scope->get(CloseConnectionFinalizer::class));

Global Finalizers

Provide at root creation:

use RandomPHP\Scoped\Finalizers\GarbageCollectionFinalizer;

$root = RootScope::create(
    definitionRootDirectory: __DIR__ . '/di',
    globalFinalizers: [
        new GarbageCollectionFinalizer(),
    ],
);

Global finalizers are inherited by all sub-scopes.

Error handling

Invoking a non-existing scope throws:

  • SubScopeNotFoundException

Example:

try {
    $root->invokeScope('missing', fn() => null);
} catch (Exception $e) {
    // handle
}

Lifecycle Notes & Caveats

  • Scopes are intended to be short-lived.
  • Do not store scope references globally.
  • Teardown occurs on destruction.
  • Weak references are used internally.
  • Avoid long-lived references to scoped services outside invocation.