Skip to main content

Container Scopes

Overview

Container scopes let you create isolated sub-containers that maintain their own bindings and instances while still falling back to the parent container. This is particularly useful when you need to adjust or override dependencies in a specific context (such as a request, session, or test scenario) without affecting the global container state.


How Scopes Work

When you create a scope, the container instantiates a new sub-container that:

  • Checks Its Own Bindings First:
    has(), get(), and make() are overridden on the scope so they look at the scope's own bindings and retained instances before anything else.

  • Local Overrides:
    You can register new bindings in the scope, and they take precedence over the parent's binding for the same identifier without changing the parent's configuration at all.

  • Falls Back to the Parent:
    If an identifier isn't bound or retained in the scope, resolution falls back to the parent container. If the parent doesn't have it either but it's an autowireable class, the scope builds it itself.

The scope does not copy or pre-populate the parent's bindings into itself — it simply defers to the parent at resolution time for anything it doesn't have locally. This ensures that services can be tailored for a specific context without impacting the global configuration.

caution

When the requested top-level identifier exists only in the parent, the scope delegates the entire make() call to the parent. Dependencies built during that parent resolution do not see nested overrides that exist only in the scope. Bind the top-level consumer in the scope as well when its object graph must use local overrides.


The scope() Method

The scope() method is used to create or retrieve a lazy-scoped sub-container. Its signature is:

scope(
string $scopeName,
callable $callback
): mixed
ParameterTypeRequiredDescription
$scopeNamestringYesA unique name to identify the sub-container (scope).
$callbackcallable(Container): mixedYesA callback that receives the sub-container instance to configure or retrieve services within this scope.

Extended Example: Using Scopes with Instance Creation

Below is an extended example that demonstrates how to use scopes, override a global binding within the scope, and utilize the created instance.

// Create the main container.
$container = new Container();

// Global binding: LoggerInterface resolves to DefaultLogger.
$container->bind(LoggerInterface::class, DefaultLogger::class);

// Resolve the global logger and use it.
$globalLogger = $container->get(LoggerInterface::class);
$globalLogger->log("This is from the global DefaultLogger.");

// Create a sub-container (scope) for a request.
$requestLogger = $container->scope('request', function (Container $scope) {
// Override the LoggerInterface binding for this scope.
$scope->bind(LoggerInterface::class, RequestLogger::class);

// Resolve and return the instance within the scope.
return $scope->get(LoggerInterface::class);
});

// Use the instance created within the request scope.
$requestLogger->log("This is from the RequestLogger within the 'request' scope.");

// Demonstrate that resolving LoggerInterface from the parent still uses the global binding.
$anotherGlobalLogger = $container->get(LoggerInterface::class);
$anotherGlobalLogger->log("Back to the global DefaultLogger.");

In this example:

  1. The container is set up with a global binding where LoggerInterface maps to DefaultLogger.
  2. A new scope named "request" is created via the scope() method. Inside the callback, the binding for LoggerInterface is overridden to use RequestLogger.
  3. The instance resolved from the scoped container ($requestLogger) reflects the local override.
  4. Resolving LoggerInterface from the parent container still uses the global DefaultLogger binding, confirming that the scope did not affect the global state. Because bind() is transient by default, the two resolutions are different objects; use singleton() when object identity must be shared.

Disposing and Resetting Scopes

Two methods manage a scope's lifecycle once it has been created:

resetScope(string $scopeName): void

Discards the scope's retained values — its resolved singleton instances, resolution cache, and in-progress resolution tracking — while keeping the scope itself, its bindings, and any other local configuration intact. Calling this on a name with no existing scope is a no-op. Use this at a lifecycle boundary (e.g. the start of a new request handled by a long-running worker) where the scope's identity and configuration should persist but any previously retained objects must not leak into the next unit of work.

disposeScope(string $scopeName): void

Removes the named scope entirely, including its bindings and configuration. The next call to scope() with that name creates a brand-new sub-container from scratch.

$container = new Container();
$container->bind(LoggerInterface::class, DefaultLogger::class);

$container->scope('request', function (Container $scope) {
$scope->singleton(RequestContext::class, RequestContext::class);
return $scope->get(RequestContext::class);
});

// End of the request: drop retained instances, keep the scope's bindings.
$container->resetScope('request');

// Later: remove the "request" scope completely, bindings included.
$container->disposeScope('request');

Underlying Implementation Details

  • Scoped Storage:
    Each scope is stored in an internal associative array using its unique scope name.

  • Method Overrides:
    The sub-container overrides key methods like has(), get(), and make() to first check its own bindings and instances, then fallback to the parent container if necessary.

  • Lazy Initialization:
    If a scope with the given name already exists, it is reused rather than recreated, ensuring efficient resource usage.


Benefits of Using Scopes

  • Isolation:
    Tailor service definitions for specific contexts without modifying global bindings.

  • Override Capability:
    Temporarily override or extend service bindings for testing, requests, or other scenarios.

  • Resource Management:
    Create temporary sub-containers that can be disposed of after use, reducing potential memory overhead in unit tests or per-request handling.


Summary

Container scopes in the DomainFlow Container provide a flexible mechanism to isolate and override service bindings for specific contexts. By creating sub-containers that fall back to the parent yet allow local modifications, you can fine-tune dependency resolution for different parts of your application without compromising the integrity of the global configuration.