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:

  • Inherits Bindings:
    It automatically inherits all bindings and instances from its parent container.

  • Local Overrides:
    You can register new bindings or override existing ones specific to that scope.

  • Fallback Resolution:
    If a service is not found in the sub-container, resolution falls back to the parent container.

This behavior ensures that services can be tailored for a specific context without impacting the global configuration.


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 returns the global logger.
$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 returns the global instance, confirming that the scope did not affect the global state.

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 inherit global bindings yet allow local modifications, you can fine-tune dependency resolution for different parts of your application without compromising the integrity of the global configuration.