0

Container events — resolving, afterResolving, beforeResolving

Advanced5 min read·lv-02-007
laravel-src

Concept

The Laravel container emits a set of lifecycle hooks called container events that fire at specific moments during object resolution. These hooks are the extension point for cross-cutting concerns — logging every resolved service, decorating instances, configuring third-party objects, or enforcing invariants. The three hooks are resolving(), afterResolving(), and beforeResolving().

resolving(string|null $abstract, Closure $callback) fires immediately after the container builds an instance but before it is returned to the caller. The callback receives the resolved object and the container itself: function (object $instance, Container $container). If $abstract is null, the callback fires for every resolved type — use this carefully as it runs on hot paths. Internally, these callbacks are stored in Container::$resolvingCallbacks (a string → Closure[] map) and executed inside fireResolvingCallbacks().

afterResolving(string|null $abstract, Closure $callback) fires after resolving() callbacks have all completed. It uses Container::$afterResolvingCallbacks and is the right place for work that depends on the fully decorated object. Laravel itself uses afterResolving inside several core service providers — for example, Illuminate\Auth\AuthServiceProvider uses it to inject the auth guard into request macros after every Request is resolved.

beforeResolving(string|null $abstract, Closure $callback) (added in Laravel 10) fires before build() is called, receiving only the abstract name and parameters. It is useful for validation (prevent resolving certain types in certain contexts) or logging the resolution chain before it begins.

The order of execution for a resolved class is: beforeResolving callbacks → build()resolving callbacks → afterResolving callbacks → return to caller.

Container events are powerful for package authors who need to configure resolved instances without forcing application developers to extend classes. They keep service providers clean and avoid the temptation to reach into resolved objects from the outside in an ad-hoc way.

HookFired whenReceivesCommon use
beforeResolvingBefore build()abstract name, parametersValidation, tracing
resolvingAfter build, before returnobject instance, containerDecoration, configuration
afterResolvingAfter all resolving callbacksobject instance, containerPost-decoration setup

Code Example

php
<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Container\Container;
use Illuminate\Support\ServiceProvider;
use App\Contracts\LoggerAwareInterface;
use Psr\Log\LoggerInterface;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // --- resolving(): inject logger into any LoggerAware service ---
        // Fires after build(), receives the new instance.
        $this->app->resolving(LoggerAwareInterface::class, function (
            LoggerAwareInterface $instance,
            Container $container
        ): void {
            $instance->setLogger($container->make(LoggerInterface::class));
        });

        // --- afterResolving(): configure after all decoration is done ---
        // Useful when you need the fully decorated object.
        $this->app->afterResolving(\App\Services\PaymentGateway::class, function (
            \App\Services\PaymentGateway $gateway,
            Container $container
        ): void {
            $gateway->setWebhookSecret(config('services.stripe.webhook_secret'));
        });

        // --- beforeResolving(): trace every resolution in debug mode ---
        if ($this->app->hasDebugModeEnabled()) {
            $this->app->beforeResolving(null, function (string $abstract, array $parameters): void {
                logger()->debug("Resolving: {$abstract}", ['params' => array_keys($parameters)]);
            });
        }

        // --- Global resolving(): fire for EVERY resolved class ---
        // Use carefully — this runs on every make() call including framework internals.
        $this->app->resolving(function (object $instance, Container $container): void {
            if ($instance instanceof \App\Contracts\TenantAwareInterface) {
                $instance->setTenant($container->make(\App\Models\Tenant::class));
            }
        });
    }
}

// --- Testing container events ---
// In a test, you can assert callbacks fired by checking side effects:
class PaymentGatewayTest extends \Tests\TestCase
{
    public function test_webhook_secret_is_injected_via_container_event(): void
    {
        config(['services.stripe.webhook_secret' => 'whsec_test']);

        $gateway = $this->app->make(\App\Services\PaymentGateway::class);

        $this->assertSame('whsec_test', $gateway->getWebhookSecret());
    }
}

Interview Q&A

Q: What are container events in Laravel, and how do resolving() and afterResolving() differ?

Container events are lifecycle hooks registered on Illuminate\Container\Container that fire during the resolution of a type. resolving() fires immediately after build() constructs the instance — this is where you perform decoration or inject additional dependencies that the constructor did not receive. afterResolving() fires after all resolving callbacks have run, giving you access to a fully decorated object. Practically, you should use resolving() for configuration that the object needs before any afterResolving work can happen, and use afterResolving() when you depend on the result of earlier decoration. Both callbacks receive the instance and the container, so you can use the container to resolve further dependencies inside the callback.


Q: How does Laravel store and execute container event callbacks internally?

In Illuminate\Container\Container, callbacks registered with resolving() are stored in the protected $resolvingCallbacks array, keyed by abstract name. Global callbacks (no abstract) are stored under an empty-string key. After build() completes, make() calls fireResolvingCallbacks($abstract, $object), which collects matching callbacks from $resolvingCallbacks for both the concrete type and all its implemented interfaces and parent classes (via getCallbacksForType()). This means registering a callback against an interface will fire for every class that implements it — the exact mechanism Laravel uses for LoggerAwareInterface injection. After those fire, fireAfterResolvingCallbacks() runs the $afterResolvingCallbacks array the same way.


Q: When would you use a beforeResolving callback, and what are the performance implications of global container event callbacks?

beforeResolving is useful for auditing, tracing, or enforcing architectural rules before an object is built — for example, throwing an exception if a database-touching class is resolved during a test that marks itself as "no database". It receives only the abstract name and the parameter array, not an instance, because the object does not yet exist. The performance implication of global callbacks (passing null as the abstract) is that they execute on every single Container::make() call, including framework internals like resolving the Request, Router, and View during every request. A naive global callback that does I/O in production would be catastrophic. Limit global callbacks to lightweight checks and gate them behind app()->isDebugMode() or a feature flag.