0

Resolving events — beforeResolving and afterResolving hooks

Expert5 min read·fw-02-008

Concept

Container events are callbacks that fire at specific moments in the resolution lifecycle. They let you observe or modify resolved objects without touching either the class being resolved or the code that requested it. The three canonical hooks, matching Laravel's API exactly, are: resolving() (fires for every resolution of the abstract), afterResolving() (fires after all resolving callbacks), and beforeResolving() (fires before the resolution chain begins).

The typical use case for resolving() is property injection or configuration that cannot happen through a constructor — for example, setting a locale on every Translator instance, or calling a setContainer() method on every service that implements ContainerAwareInterface. Because the callback receives the resolved object, it can call any public method or set any public property.

Our implementation stores callbacks in two associative arrays: $resolvingCallbacks[abstract] and $afterResolvingCallbacks[abstract]. Each key maps to an array of Closure values. When make() finishes building an object, it iterates both maps and fires matching callbacks. A wildcard pattern (the empty-string or '*' key) catches every resolution, which is how Laravel supports $this->app->resolving(fn ($obj, $app) => ...) without specifying an abstract.

The sequence matters: (1) the object is built, (2) all resolving callbacks fire, (3) all afterResolving callbacks fire, (4) if shared, the instance is cached, (5) the object is returned. Caching happens after callbacks so the callbacks see the fully-configured object before it becomes the singleton.

In Laravel, these hooks are used internally by the framework itself — for example, to inject Container references into ContainerAware implementations and to fire resolving events that packages listen to. Understanding them is essential for writing framework-level service providers.

Code Example

php
<?php
declare(strict_types=1);

namespace Framework\Container;

use Closure;
use ReflectionClass;
use ReflectionParameter;
use ReflectionNamedType;
use Framework\Container\Exceptions\BindingResolutionException;

class Container
{
    protected array $bindings   = [];
    protected array $instances  = [];
    protected array $contextual = [];
    protected array $buildStack = [];
    protected array $with       = [];

    /**
     * Callbacks fired before the object is built.
     * @var array<string, Closure[]>
     */
    protected array $beforeResolvingCallbacks = [];

    /**
     * Callbacks fired immediately after the object is built (before caching).
     * @var array<string, Closure[]>
     */
    protected array $resolvingCallbacks = [];

    /**
     * Callbacks fired after all resolving callbacks complete.
     * @var array<string, Closure[]>
     */
    protected array $afterResolvingCallbacks = [];

    // -------------------------------------------------------------------------
    // Event registration API
    // -------------------------------------------------------------------------

    /**
     * Register a callback to run before an abstract is resolved.
     *
     * Laravel equivalent: Container::beforeResolving()
     */
    public function beforeResolving(string $abstract, Closure $callback): void
    {
        $this->beforeResolvingCallbacks[$abstract][] = $callback;
    }

    /**
     * Register a callback to run after an abstract is resolved.
     * The callback receives ($resolvedObject, $container).
     *
     * Laravel equivalent: Container::resolving()
     */
    public function resolving(string $abstract, Closure $callback): void
    {
        $this->resolvingCallbacks[$abstract][] = $callback;
    }

    /**
     * Register a callback to run after all resolving() callbacks.
     *
     * Laravel equivalent: Container::afterResolving()
     */
    public function afterResolving(string $abstract, Closure $callback): void
    {
        $this->afterResolvingCallbacks[$abstract][] = $callback;
    }

    // -------------------------------------------------------------------------
    // Core bindings (abbreviated from previous lessons)
    // -------------------------------------------------------------------------

    public function bind(string $abstract, Closure|string $concrete, bool $shared = false): void
    {
        if (is_string($concrete)) {
            $concrete = fn (self $c) => $c->build($concrete);
        }
        $this->bindings[$abstract] = ['concrete' => $concrete, 'shared' => $shared];
    }

    public function singleton(string $abstract, Closure|string $concrete): void
    {
        $this->bind($abstract, $concrete, shared: true);
    }

    public function instance(string $abstract, object $instance): void
    {
        $this->instances[$abstract] = $instance;
        $this->fireResolvingCallbacks($abstract, $instance);
        $this->fireAfterResolvingCallbacks($abstract, $instance);
    }

    // -------------------------------------------------------------------------
    // make() — now fires lifecycle events
    // -------------------------------------------------------------------------

    public function make(string $abstract): mixed
    {
        // Return cached singletons without firing events again.
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        // Fire beforeResolving hooks.
        $this->fireBeforeResolvingCallbacks($abstract);

        $object = $this->resolve($abstract);

        // Fire resolving and afterResolving hooks.
        $this->fireResolvingCallbacks($abstract, $object);
        $this->fireAfterResolvingCallbacks($abstract, $object);

        return $object;
    }

    protected function resolve(string $abstract): mixed
    {
        $contextual = $this->findContextualBinding($abstract);
        if ($contextual !== null) {
            return $this->resolveContextual($contextual);
        }

        if (isset($this->bindings[$abstract])) {
            $binding = $this->bindings[$abstract];
            $object  = ($binding['concrete'])($this);

            if ($binding['shared']) {
                $this->instances[$abstract] = $object;
            }

            return $object;
        }

        return $this->build($abstract);
    }

    protected function findContextualBinding(string $abstract): mixed
    {
        if (empty($this->buildStack)) {
            return null;
        }
        $parent = end($this->buildStack);
        return $this->contextual[$parent][$abstract] ?? null;
    }

    protected function resolveContextual(mixed $binding): mixed
    {
        if ($binding instanceof Closure) {
            return $binding($this);
        }
        if (is_string($binding) && class_exists($binding)) {
            return $this->make($binding);
        }
        return $binding;
    }

    public function build(string $concrete): object
    {
        $reflector = new ReflectionClass($concrete);

        if (! $reflector->isInstantiable()) {
            throw new BindingResolutionException("Target [{$concrete}] is not instantiable.");
        }

        $this->buildStack[] = $concrete;

        try {
            $constructor = $reflector->getConstructor();
            if ($constructor === null) {
                return new $concrete();
            }
            $deps = array_map(
                fn (ReflectionParameter $p) => $this->resolveParameter($p),
                $constructor->getParameters()
            );
            return $reflector->newInstanceArgs($deps);
        } finally {
            array_pop($this->buildStack);
        }
    }

    protected function resolveParameter(ReflectionParameter $parameter): mixed
    {
        $type = $parameter->getType();
        if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) {
            try {
                return $this->make($type->getName());
            } catch (BindingResolutionException $e) {
                if ($parameter->isOptional()) {
                    return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
                }
                throw $e;
            }
        }
        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getDefaultValue();
        }
        throw new BindingResolutionException(
            "Unresolvable dependency [\${$parameter->getName()}]."
        );
    }

    // -------------------------------------------------------------------------
    // Event firing helpers
    // -------------------------------------------------------------------------

    protected function fireBeforeResolvingCallbacks(string $abstract): void
    {
        foreach ($this->beforeResolvingCallbacks[$abstract] ?? [] as $callback) {
            $callback($abstract, $this);
        }
        // Wildcard callbacks
        foreach ($this->beforeResolvingCallbacks['*'] ?? [] as $callback) {
            $callback($abstract, $this);
        }
    }

    protected function fireResolvingCallbacks(string $abstract, object $object): void
    {
        foreach ($this->resolvingCallbacks[$abstract] ?? [] as $callback) {
            $callback($object, $this);
        }
        foreach ($this->resolvingCallbacks['*'] ?? [] as $callback) {
            $callback($object, $this);
        }
    }

    protected function fireAfterResolvingCallbacks(string $abstract, object $object): void
    {
        foreach ($this->afterResolvingCallbacks[$abstract] ?? [] as $callback) {
            $callback($object, $this);
        }
        foreach ($this->afterResolvingCallbacks['*'] ?? [] as $callback) {
            $callback($object, $this);
        }
    }

    // -------------------------------------------------------------------------
    // PSR-11
    // -------------------------------------------------------------------------

    public function has(string $id): bool
    {
        return isset($this->bindings[$id]) || isset($this->instances[$id]);
    }

    public function get(string $id): mixed
    {
        return $this->make($id);
    }
}
php
<?php
// Practical example: auto-inject container reference into every service
// that implements ContainerAwareInterface.

interface ContainerAwareInterface
{
    public function setContainer(Container $container): void;
}

$container = new Framework\Container\Container();

// Wildcard resolving hook — fires for every resolved object.
$container->resolving('*', function (object $object, Container $c): void {
    if ($object instanceof ContainerAwareInterface) {
        $object->setContainer($c);
    }
});

// Typed hook — fires only when LoggerInterface is resolved.
$container->afterResolving(LoggerInterface::class, function (object $logger, Container $c): void {
    // e.g. inject a formatter
});

Interview Q&A

Q: What is the practical difference between resolving() and afterResolving() in Laravel — when would you use one versus the other?

Both callbacks receive the same fully-built object and fire in the same request cycle, so the distinction is about ordering when multiple packages register hooks. resolving() callbacks fire first (in registration order), then afterResolving() callbacks fire. A service provider that needs its configuration applied before other packages inspect the object registers with resolving(); a package that wants to observe the object after all configuration is done uses afterResolving(). In practice, resolving() is the common choice for property injection, and afterResolving() is used for logging, validation, or assertion that the object was configured correctly. Laravel's own framework code (e.g. the HTTP kernel wiring) uses resolving() to inject references like $this->app->resolving(Router::class, fn ($r) => $r->setContainer($this->app)).


Q: Why does instance() also fire resolving callbacks in our implementation?

Because code consuming the container via make() or get() should not need to know whether the object was built by build(), returned from cache, or registered via instance(). If you register a pre-built logger with $container->instance(Logger::class, $logger) and you have a wildcard resolving('*') callback that injects a formatter, that callback should run so the logger is in the expected state. Laravel's Container::instance() calls fireResolvingCallbacks() and fireAfterResolvingCallbacks() for exactly this reason — see Illuminate/Container/Container.php::instance().


Q: How would you implement a beforeResolving hook that prevents a class from being resolved in a specific application state?

In fireBeforeResolvingCallbacks(), allow the callback to throw an exception. The hook signature receives ($abstract, $container) and has no return value in our implementation, so the only way to abort is with an exception. Laravel follows the same approach — beforeResolving callbacks cannot veto a resolution by returning false; they can only throw. A real-world example is a frozen() state on the container during testing that throws when any new binding is resolved after the test has started, ensuring the test's mock configuration is complete before execution begins.