Service Container

Introduction

The Framework service container is a powerful tool for managing class dependencies and performing dependency injection. Dependency injection is a fancy phrase that essentially means this: class dependencies are “injected” into the class via the constructor or, in some cases, “setter” methods.

Let’s look at a simple example:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Services\OrdersService;
use MacropaySolutions\Kernel\View\View;

class OrderController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct(
        protected OrdersService $ordersService,
    ) {}

    /**
     * Process the payment for the given order.
     */
    public function process(string $id): View
    {
        $status = $this->ordersService->charge($id);

        return view('order.status', ['status' => $status]);
    }
}

In this example, the OrderController needs to interact with a payment provider. So, we will inject a service that handles orders. Since the OrdersService is injected, we are able to easily swap it out with another implementation. We are also able to easily “mock”, or create a dummy implementation of the OrdersService when testing our application.

A deep understanding of the Framework service container is essential to building a powerful, large application, as well as for contributing to the core itself.

Zero Configuration Resolution

If a class has no dependencies or only depends on other concrete classes (not interfaces), the container does not need to be instructed on how to resolve that class. For example, if you register a route to a controller:

\app('router')->get('/', [\App\Http\Controllers\ServiceController::class, 'index']);

And define the controller like so:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Services\BillingService;

class ServiceController extends Controller
{
    public function index(BillingService $service)
    {
        die($service::class);
    }
}

In this example, hitting your application’s / route will automatically resolve the BillingService class and inject it into your controller’s method. This is game changing. It means you can develop your application and take advantage of dependency injection without worrying about bloated configuration files.

Thankfully, many of the classes you will be writing when building an application automatically receive their dependencies via the container, including controllers, event listeners, middleware, and more. Additionally, you may type-hint dependencies in the handle method of queued jobs. Once you taste the power of automatic and zero configuration dependency injection, it feels impossible to develop without it.

When to Utilize the Container

Thanks to zero configuration resolution, you will often type-hint dependencies on controllers, event listeners, and elsewhere without ever manually interacting with the container. For example, you might type-hint the MacropaySolutions\Kernel\Http\Request object on your controller definition so that you can easily access the current request. Even though we never have to interact with the container to write this code, it is managing the injection of these dependencies behind the scenes:

namespace App\Http\Controllers;

use MacropaySolutions\Kernel\Http\Request;

class HomeController
{
    public function index(Request $request)
    {
        // ...
    }
}

In many cases, thanks to automatic dependency injection, you can build applications without ever manually binding or resolving anything from the container. So, when would you ever manually interact with the container? Let’s examine two situations.

First, if you write a class that implements an interface and you wish to type-hint that interface on a controller or class constructor, you must tell the container how to resolve that interface. Secondly, if you are writing a Framework package that you plan to share with other developers, you may need to bind your package’s services into the container.

Binding

Binding Basics

Simple Bindings

Almost all of your service container bindings will be registered within service providers, so most of these examples will demonstrate using the container in that context.

Within a service provider, you always have access to the container via the $this->app property. We can register a binding using the bind method, passing the class or interface name that we wish to register along with an array callable that returns an instance of the class:

[!CRITICAL]
Closures are Forbidden To enforce zero-overhead Ahead-of-Time (AOT) compilation and OPcache efficiency, PHP-Framework has strictly removed \Closure support from container binding methods. You must use static array callables (e.g., [FactoryClass::class, 'method']) or fully qualified class name strings.

use App\Services\Transistor;
use App\Services\PodcastParser;
use MacropaySolutions\Kernel\Contracts\Foundation\Application;

$this->app->bind(Transistor::class, [\App\Factories\PodcastFactory::class, 'createTransistor']);

public static function createTransistor(Application $app)
{
    return new Transistor($app->make(PodcastParser::class));
}

Note that factory array callables will receive the container instance explicitly as an argument ($app or $container) when invoked.

As mentioned, you will typically be interacting with the container within service providers; however, if you would like to interact with the container outside of a service provider, you may do so via the app() helper:

use App\Services\Transistor;
use MacropaySolutions\Kernel\Contracts\Foundation\Application;

\app()->bind(Transistor::class, [\App\Factories\PodcastFactory::class, 'createTransistor']);

public static function createTransistor(Application $app)
{
    // ...
}

You may use the bindIf method to register a container binding only if a binding has not already been registered for the given type:

$this->app->bindIf(Transistor::class, [\App\Factories\Factory::class, 'createTransistor']);

public static function createTransistor(Application $app) {
    return new Transistor($app->make(PodcastParser::class));
}

[!TIP] Closure Scoping & Performance Guidelines (static) Container factory closures receive the container instance explicitly as an argument ($app or $container). Therefore, they do not require $this scope binding.

Always declare container closures as static (static fn(...) or static function(...)). This prevents implicit $this context capturing, reduces zval allocations, and prevents cyclic memory references.

[!NOTE]
Do not use static closures for Obvious Model accessors/mutators or dynamic Macros, as those features rely on Closure::bindTo() to bind $this at runtime. There is no need to bind classes into the container if they do not depend on any interfaces. The container does not need to be instructed on how to build these objects, since it can automatically resolve these objects using reflection during autowiring:cache. Just make sure to include those FQNs into the app.autowiring config.

Binding A Singleton

The singleton method binds a class or interface into the container that should only be resolved one time. Once a singleton binding is resolved, the same object instance will be returned on subsequent calls into the container:

use App\Services\Transistor;
use App\Services\PodcastParser;
use MacropaySolutions\Kernel\Contracts\Foundation\Application;

$this->app->singleton(Transistor::class, [\App\Factories\PodcastFactory::class, 'createTransistor']);

public static function createTransistor(Application $app): Transistor
{
    return new Transistor($app->make(PodcastParser::class));
}

You may use the singletonIf method to register a singleton container binding only if a binding has not already been registered for the given type:

$this->app->singletonIf(Transistor::class, [\App\Factories\PodcastFactory::class, 'createTransistor']);

public static function createTransistor(Application $app): Transistor
{
    return new Transistor($app->make(PodcastParser::class));
}

Binding Scoped Singletons

The scoped method binds a class or interface into the container that should only be resolved one time within a given Framework request / job lifecycle. While this method is similar to the singleton method, instances registered using the scoped method will be flushed whenever the Framework application starts a new “lifecycle”, such as when a queue worker processes a new job:

use App\Services\Transistor;
use App\Services\PodcastParser;
use MacropaySolutions\Kernel\Contracts\Foundation\Application;

$this->app->scoped(Transistor::class, [\App\Factories\PodcastFactory::class, 'createTransistor']);

public static function createTransistor(Application $app): Transistor
{
    return new Transistor($app->make(PodcastParser::class));
}

Binding Instances

You may also bind an existing object instance into the container using the instance method. The given instance will always be returned on subsequent calls into the container:

use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);

Binding Interfaces to Implementations

A very powerful feature of the service container is its ability to bind an interface to a given implementation. For example, let’s assume we have an EventPusher interface and a RedisEventPusher implementation. Once we have coded our RedisEventPusher implementation of this interface, we can register it with the service container like so:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;

$this->app->bind(EventPusher::class, RedisEventPusher::class);

This statement tells the container that it should inject the RedisEventPusher when a class needs an implementation of EventPusher. Now we can type-hint the EventPusher interface in the constructor of a class that is resolved by the container. Remember, controllers, event listeners, middleware, and various other types of classes are always resolved using the container:

use App\Contracts\EventPusher;

/**
 * Create a new class instance.
 */
public function __construct(
    protected EventPusher $pusher
) {}

Contextual Injection

Sometimes you may have two classes that utilize the same interface, but you wish to inject different implementations into each class. For example, two controllers may depend on different implementations of the MacropaySolutions\Kernel\Contracts\Filesystem\Filesystem contract.

Since PHP-Framework optimizes for absolute maximum execution speed, it does not provide a slower fluent contextual binding builder (like ->when()->needs()->give()). Instead, you can achieve this with zero overhead using the following methods:

1. The Explicit Bindings Map

The fastest way to bypass contextual lookup is by defining your array callables directly inside the $bindings property of your app/Application.php file. This places your resolution factories directly into the core bindings registry during container bootstrapping, keeping the evaluation path optimized and OPcache-friendly.

protected array $bindings = [
    // Explicitly wire PhotoController to use LocalFilesystem
    \App\Http\Controllers\PhotoController::class => [
        'concrete' => [\App\Factories\FilesystemContextFactory::class, 'createPhotoController'],
        'shared' => false
    ],

    // Explicitly wire VideoController to use S3Filesystem
    \App\Http\Controllers\VideoController::class => [
        'concrete' => [\App\Factories\FilesystemContextFactory::class, 'createVideoController'],
        'shared' => false
    ],
];

public static function createPhotoController(
    \MacropaySolutions\Kernel\Contracts\Container\Container $container,
    array $parameters = []
): \App\Http\Controllers\PhotoController
{
    return new \App\Http\Controllers\PhotoController(
        $container->make(\App\Services\Filesystem\LocalFilesystem::class, $parameters)
    );
}

public static function createVideoController(
    \MacropaySolutions\Kernel\Contracts\Container\Container $container,
    array $parameters = []
): \App\Http\Controllers\VideoController {
    return new \App\Http\Controllers\VideoController(
        $container->make(\App\Services\Filesystem\S3Filesystem::class, $parameters)
    );
}

[!NOTE] DO NOT use aliases (keys from $aliases app property) in $bindings as keys! Same is valid for $instances.

2. Manual Factory Callables in Service Providers

If you prefer to configure your bindings within standard Service Providers, you can avoid contextual evaluations by using a factory callable inside a standard bind or singleton method. This allows you to manually instantiate the target class and explicitly pass the concrete dependencies it requires.

use App\Http\Controllers\PhotoController;
use App\Services\Filesystem\LocalFilesystem;
use MacropaySolutions\Kernel\Contracts\Foundation\Application;

$this->app->bind(PhotoController::class, [\App\Factories\Factory::class, 'createPhotoControllerContext']);

public static function createPhotoControllerContext(Application $app)
{
    return new PhotoController($app->make(LocalFilesystem::class));
}

3. Type-Hinting Concrete Classes (Zero-Configuration)

Often, the need for contextual binding indicates that an interface is serving too many distinct roles across your application. You can eliminate manual bindings completely by type-hinting unique concrete classes or descriptive sub-interfaces directly in your class constructors.

By targeting specific concrete dependencies, the container can instantly resolve the object using its highly efficient Zero Configuration Resolution pipeline via reflection.

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Services\Filesystem\LocalFilesystem;

class PhotoController extends Controller
{
    // Fully resolved automatically via Zero-Configuration reflection
    public function __construct(
        protected LocalFilesystem $disk
    ) {}
}

Binding Primitives

Sometimes you may have a class that receives some injected classes, but also needs an injected primitive value such as an integer. You can easily inject any value your class may need using a manual factory callable:

use App\Http\Controllers\UserController;
use MacropaySolutions\Kernel\Contracts\Foundation\Application;

$this->app->bind(UserController::class, [\App\Factories\UserFactory::class, 'createControllerWithDefault']);

public static function createControllerWithDefault(Application $app)
{
    return new UserController($app->make(SomeService::class), 'default_value');
}

Sometimes a class may depend on an array of tagged instances. Using the tagged method within your factory, you may easily inject all the container bindings with that tag:

$this->app->bind(ReportAggregator::class, [\App\Factories\ReportFactory::class, 'createAggregator']);

public static function createAggregator(Application $app)
{
    return new ReportAggregator($app->tagged('reports'));
}

If you need to inject a value from one of your application’s configuration files, you may manually resolve the config service:

$this->app->bind(ReportAggregator::class, [\App\Factories\Factory::class, 'createAggregatorWithTimezone']);

public static function createAggregatorWithTimezone(Application $app)
{
    return new ReportAggregator($app->make('config')->get('app.timezone'));
}

Binding Typed Variadics

Occasionally, you may have a class that receives an array of typed objects using a variadic constructor argument:

<?php

use App\Models\Filter;
use App\Services\Logger;

class Firewall
{
    /**
     * The filter instances.
     *
     * @var array
     */
    protected $filters;

    /**
     * Create a new class instance.
     */
    public function __construct(
        protected Logger $logger,
        Filter ...$filters,
    ) {
        $this->filters = $filters;
    }
}

You may resolve this dependency by providing the bind method with a callable that explicitly injects the resolved Filter instances:

use MacropaySolutions\Kernel\Contracts\Foundation\Application;

$this->app->bind(Firewall::class, [\App\Factories\SecurityFactory::class, 'createFirewall']);

public static function createFirewall(Application $app) {
    return new Firewall(
        $app->make(Logger::class),
        $app->make(NullFilter::class),
        $app->make(ProfanityFilter::class),
        $app->make(TooLongFilter::class),
    );
}

Variadic Tag Dependencies

Sometimes a class may have a variadic dependency that is type-hinted as a given class (Report ...$reports). Using the spread operator ... and the tagged method within a factory callable, you may easily inject all the container bindings with that tag for the given dependency:

$this->app->bind(ReportAggregator::class, [\App\Factories\ReportFactory::class, 'createVariadicAggregator']);

public static function createVariadicAggregator(Application $app)
{
    return new ReportAggregator(...$app->tagged('reports'));
}

Tagging

Occasionally, you may need to resolve all of a certain “category” of binding. For example, perhaps you are building a report analyzer that receives an array of many different Report interface implementations. After registering the Report implementations, you can assign them a tag using the tag method:

$this->app->bind(CpuReport::class, [\App\Factories\ReportFactory::class, 'createCpuReport']);

$this->app->bind(MemoryReport::class, [\App\Factories\ReportFactory::class, 'createMemoryReport']);

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

Once the services have been tagged, you may easily resolve them all via the container’s tagged method:

$this->app->bind(ReportAnalyzer::class, [\App\Factories\ReportFactory::class, 'createAnalyzer']);

public static function createAnalyzer(Application $app)
{
    return new ReportAnalyzer($app->tagged('reports'));
}

Extending Bindings

The extend method allows the modification of resolved services. For example, when a service is resolved, you may run additional code to decorate or configure the service. The extend method accepts two arguments, the service class you’re extending and a callable that should return the modified service. The callable receives the service being resolved and the container instance:

$this->app->extend(Service::class, [\App\Factories\Factory::class, 'extendService']);

public static function extendService(Service $service, Application $app)
{
    return new DecoratedService($service);
}

Resolving

The make Method

You may use the make method to resolve a class instance from the container. The make method accepts the name of the class or interface you wish to resolve:

use App\Services\Transistor;

$transistor = $this->app->make(Transistor::class);

If some of your class’s dependencies are not resolvable via the container, you may inject them by passing them as an associative array (or LIST array with all the needed parameters to avoid runtime reflection) into the makeWith method. For example, we may manually pass the $id constructor argument required by the Transistor service:

use App\Services\Transistor;

$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
$transistor = $this->app->make(Transistor::class, [1]);

[!WARNING]
Singletons and Custom Parameters
If you pass custom parameters to a service that is registered as a singleton (e.g., via makeWith or the $parameters array), the container will intentionally bypass the shared singleton cache. It will construct and return a completely new instance using your provided parameters, and it will not save this customized instance back into the container’s shared memory. This safeguard ensures that custom configurations do not silently overwrite the global state for the rest of the application. beforeResolving, resolving and afterResolving events will be fired only on the first resolve for a singleton and never for a prepopulated instance (when no parameters are given).

The bound method may be used to determine if a class or interface has been explicitly bound in the container:

if ($this->app->bound(Transistor::class)) {
    // ...
}

If you are outside a service provider in a location of your code that does not have access to the $app variable, you may use the app helper to resolve a class instance from the container:

use App\Services\Transistor;

$transistor = \app(Transistor::class);

If you would like to have the Framework container instance itself injected into a class that is being resolved by the container, you may type-hint the MacropaySolutions\Kernel\Container\Container class on your class’s constructor:

use MacropaySolutions\Kernel\Container\Container;

/**
 * Create a new class instance.
 */
public function __construct(
    protected Container $container
) {}

[!NOTE] You can use the di helper exclusively with list parameters when resolving from the container if you want to make sure it will not introduce issues if called before the application is fully booted.

Because most of the macroable classes are resolved from the container in PHP-Kernel, there are some corner cases like MacropaySolutions\Kernel\Console\Scheduling\Schedule which, if resolved from the container, would generate a circular dependency that leads to a segmentation fault or memory exhaustion in \MacropaySolutions\Framework\Console\Kernel::defineConsoleSchedule. This can happen also to developers leading to lost debug time.

To shorten the detection of such cases, we introduced a check, configurable via app.circular_dependency_memory_limit:

/**
 * Limit of extra memory used to detect circular dependencies when resolving an abstract from container (bytes)
 */
'circular_dependency_memory_limit' => 'production' === \env('APP_ENV') ? 0 : 10485760,

By setting it to 0 bytes you disable the check. When enabled, an increment is used until 7 and then the memory check kicks in for benchmark speed reasons.

The makeWithoutAlias Method

Internally, the Framework container relies heavily on short string aliases (like 'events' or 'mailer') to map core components to their implementations. However, this can introduce a dangerous “Instantiation Loop” if a foundational Service Provider attempts to resolve a core class from the container while that exact class is still in the middle of being booted.

To safely break this chicken-and-egg cycle without sacrificing the flexibility of the alias registry, you may use the makeWithoutAlias method. This method forces the container to evaluate the concrete class directly through the reflection and cache pipeline, completely bypassing the forward and reverse alias lookup arrays:

use MacropaySolutions\Kernel\Mail\Mailer;

// Triggers the alias factory closure, potentially causing an infinite boot loop...
$mailer = $this->app->make(Mailer::class, $parameters);

// Safely resolves the concrete class without triggering alias redirects...
$mailer = $this->app->makeWithoutAlias(Mailer::class, $parameters);

This method is strictly required when you are authoring custom service providers, foundational decorators, or extending core framework managers (like MailManager or Dispatcher) that need to resolve framework classes from dependency injection while the application is still bootstrapping.

Automatic Injection

Alternatively, and importantly, you may type-hint the dependency in the constructor of a class that is resolved by the container, including controllers, event listeners, middleware, and more. Additionally, you may type-hint dependencies in the handle method of queued jobs. In practice, this is how most of your objects should be resolved by the container.

For example, you may type-hint a service defined by your application in a controller’s constructor. The service will automatically be resolved and injected into the class:

<?php

namespace App\Http\Controllers;

use App\Services\BillingService;
use App\Models\User;

class UserController extends Controller
{
    /**
     * Create a new controller instance.
     */
    public function __construct(
        protected BillingService $billing,
    ) {}

    /**
     * Process billing for the given ID.
     */
    public function bill(string $id): User
    {
        $user = $this->billing->processPayment($id);

        return $user;
    }
}

Array Access

While the service container implements PHP’s ArrayAccess interface for legacy compatibility (e.g., $request = $this->app['request'];), its usage is strictly forbidden across the framework.

[!CRITICAL]
Array Access Strictly Forbidden (OPcache & JIT Penalties)
Resolving services via ArrayAccess bracket notation ($app['service']) is strictly forbidden and might be removed in a minor version in the future.

In addition to completely breaking IDE autocompletion, static analysis (PHPStan), and .phpstorm.meta.php return type mappings, ArrayAccess incurs VM execution overhead (FETCH_DIM_R dispatch). OPcache cannot inline dynamic offsetGet() interface calls, and PHP 8+ JIT cannot optimize C-level method lookup tables compared to direct method invocation.

For absolute type safety, maximum OPcache efficiency, and JIT optimizations, always use explicit container methods (make(), get(), instance(), forgetInstance()), constructor injection, or app() / di() helpers.

This is valid for ANY class that implements ArrayAccess.

Method Invocation and Injection

Sometimes you may wish to invoke a method on an object instance while allowing the container to automatically inject that method’s dependencies. You may invoke a method via the container using the call method.

The call method is highly flexible and accepts a variety of callback formats—including arrays, strings, and closures—while automatically resolving and injecting all required dependencies:

Array Callables

You can pass a standard PHP array callable. If you pass a class name string as the first element and it is not statically callable, the container will automatically resolve the class instance before calling the method:

<?php

namespace App;

use App\Services\ReportService;

class UserReport
{
    /**
     * Generate a new user report.
     */
    public function generate(ReportService $service): array
    {
        return [
            // ...
        ];
    }
}

// Static array callable (Instantiated automatically by the container)
$result = \app()->call([\App\UserReport::class, 'generate']);

// Object instance array callable
$result = \app()->call([new \App\UserReport, 'generate']);

String Callables

The container supports multiple string-based callable formats. It can parse @ delimited strings, :: static method strings, and automatically detect and execute invokable classes:

// Class and method delimited by '@'
$result = \app()->call('App\UserReport@generate');

// Invokable class (Container resolves the class and automatically calls __invoke)
$result = \app()->call(\App\UserReport::class);

The container’s call method may also be used to invoke a closure while automatically injecting its dependencies via reflection, which is SLOW and does not account for autowiring:cache command:

use App\Services\ReportService;

$result = \app()->call(static function (ReportService $service) {
    // ...
});

Container Events

The service container fires events at various stages of an object’s resolution lifecycle. You may hook into these stages using the beforeResolving, resolving, and afterResolving methods.

[!WARNING]
Use resolution callbacks sparingly. The container engine must compute dynamic cache keys, perform key intersections (\array_intersect_key), and flatten callback matrices via \array_merge on every single resolution. As the number of registered callbacks increases, container execution speed degrades significantly.

beforeResolving, resolving and afterResolving events will be fired only on the first resolve for a singleton and never for a prepopulated instance (when no parameters are given).

Before Resolving

The beforeResolving method fires right before the container begins looking up or instantiating a target class. It receives the abstract name and any parameters passed to the build sequence:

use App\Services\Transistor;

$this->app->beforeResolving(Transistor::class, [\App\Listeners\ContainerEventListener::class, 'onBeforeTransistor']);

public static function onBeforeTransistor(string $abstract, array $parameters, \MacropaySolutions\Kernel\Contracts\Foundation\Application $app): void
{
    // Executed immediately before the container starts building "Transistor"...
} #### Resolving

The resolving method fires immediately after the object has been successfully instantiated and after any extender decorators are applied. It passes the fully constructed instance and the container:

use App\Services\Transistor;

$this->app->resolving(Transistor::class, [\App\Listeners\ContainerEventListener::class, 'onResolvingTransistor']);

public static function onResolvingTransistor(Transistor $transistor, \MacropaySolutions\Kernel\Contracts\Foundation\Application $app): void
{
    // Called when the container resolves objects of type "Transistor"...
}

// Global listener: Called when any object type is resolved...
$this->app->resolving([\App\Listeners\GlobalContainerListener::class, 'onResolvingAny']);

After Resolving

The afterResolving method fires at the absolute tail end of the resolution pipeline, right after all class configuration, object instantiation, and factory extensions (extend()) have finished processing:

use App\Services\Transistor;

$this->app->afterResolving(Transistor::class, [\App\Listeners\ContainerEventListener::class, 'onAfterTransistor']);

public static function onAfterTransistor(Transistor $transistor, \MacropaySolutions\Kernel\Contracts\Foundation\Application $app): void
{
    // Called after the instance is completely built and decorated...
}

Internal Mechanics: State vs. Services and Bootstrapping

When contributing to the core framework or building deeply integrated packages, it is critical to understand the distinction between static services and runtime state, as well as how the container manages alias resolution during the boot phase.

Services vs. Runtime State

The container’s $bindings array is exclusively designed for Services (e.g., Router, Logger, DatabaseManager). A binding acts as a static recipe that tells the container how to construct an object from scratch if a developer requests it.

Foundation objects like the Request or the Application itself are Runtime State. The container cannot conjure a meaningful HTTP request out of thin air—it depends entirely on the incoming PHP superglobals ($_GET, $_POST, etc.) present at that exact millisecond.

For this reason, runtime state objects are never placed in the $bindings array. Instead, they are fully formed by the framework’s bootstrapper and injected directly into the container’s resolved $instances array.

Core Bootstrapping (Direct Assignment)

When bootstrapping the foundational core of the framework (like injecting the initial Request or the Application instance), the framework uses highly optimized direct array assignment to the object’s Root Target (its Fully Qualified Class Name or primary identifier):

// Inside early framework bootstrapping
$this->instances[Request::class] = $request;
$this->instances['app'] = $this;

Why direct assignment? Direct assignment bypasses standard container overhead and, crucially, preserves container aliases.

If the container has aliases configured (e.g., 'request' => Request::class), those aliases act as signposts pointing to the root target. By injecting the object directly into the root target key (Request::class), all existing aliases remain perfectly intact and successfully route developer requests (like app('request')) to your injected object.

The instance() Method and Alias Protection

While direct assignment is used for core bootstrapping, developers and runtime processes must use the $this->instance() method when injecting or overriding objects dynamically:

// Injecting a mock during testing or runtime
$this->app->instance('cache', $mockCache);

To guarantee container state integrity and prevent split-state bugs, instance(), bind(), and alias() enforce strict target rules:

  1. Alias Protection (LogicException): Passing an alias key (e.g., '\MacropaySolutions\Kernel\Cache\CacheManager::class' or 'request') to bind() or instance() is strictly prohibited. If an alias key is passed, the container throws a LogicException instructing you to target the root FQCN directly.
  2. Rebound Events: If the root target object being replaced was already bound previously, instance() fires a rebound event. This notifies all singleton services that hold the old instance to update their internal state with the new one.

[!CRITICAL]
Always Target Root FQCNs
To prevent silent alias deletion and container split-state bugs, never use alias keys when calling bind(), instance(), or defining $bindings / $instances array keys. Always bind directly to the Fully Qualified Class Name (FQCN) root target.

[!NOTE]
Rebinding the Request in the Pipeline
If global middleware mutates or completely replaces the incoming Request object mid-flight, the framework safely uses $this->instance(Request::class, $mutatedRequest). Because Request::class is the target (the value on the right side of the alias map) and not an alias itself, the container skips alias checks but correctly fires the rebound events to synchronize the rest of the application.

PSR-11

Framework’s service container implements the PSR-11 interface. Therefore, you may type-hint the MacropaySolutions\Kernel\Contracts\Container\Container contract (which extends the PSR-11 container interface) to obtain an instance of the container:

namespace App\Http\Controllers;

use App\Services\Transistor;
use MacropaySolutions\Kernel\Contracts\Container\Container as ContainerInterface;

class AudioController
{
    public function index(ContainerInterface $container)
    {
        $service = $container->get(Transistor::class);

        // ...
    }
}

An exception is thrown if the given identifier can’t be resolved. The exception will be an instance of Psr\Container\NotFoundExceptionInterface if the identifier was never bound. If the identifier was bound but was unable to be resolved, an instance of Psr\Container\ContainerExceptionInterface will be thrown.


This site uses Just the Docs, a documentation theme for Jekyll.