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
Requires
- php: >=8.4
- php-di/php-di: ^7.1
- psr/container: ^2.0
- symfony/finder: ^8.0
Requires (Dev)
- phpunit/phpunit: ^13.0
This package is auto-updated.
Last update: 2026-02-18 21:55:49 UTC
README
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 scopedi/request/→ sub-scope namedrequestdi/request/user/→ nested sub-scopeuserunderrequestdi/cli/→ sub-scope namedcli
Rules:
- All
*.phpfiles 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:
- Current scope definitions
- Exported definitions
- Parent scope definitions
- 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.
