2022-05-23

[Laravel読書録]その1: $app->make()

Laravelの $app->make() は何をしているのか調べてみたメモ。 バージョンは v9.8.1 です。

$app は Illuminate\Foundation\Application のインスタンス。

Illuminate\Foundation\Application のコンストラクタの処理

コンストラクタではbasePathなどを設定しつつデフォルトのbindings, providers, aliasの登録をしている。

public function __construct($basePath = null)
{
    if ($basePath) {
        $this->setBasePath($basePath);
    }

    $this->registerBaseBindings();
    $this->registerBaseServiceProviders();
    $this->registerCoreContainerAliases();
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Foundation/Application.php#L172-L181

registerBaseBindings() はこんな感じな処理になっている。 static::setInstance() はシングルトンとしてApplicationを登録。

protected function registerBaseBindings()
{
    static::setInstance($this);

    $this->instance('app', $this);

    $this->instance(Container::class, $this);
    $this->singleton(Mix::class);

    $this->singleton(PackageManifest::class, function () {
        return new PackageManifest(
            new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
        );
    });
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Foundation/Application.php#L198-L212

instance() はこんな感じで $this->instances にセットしている

public function instance($abstract, $instance)
{
    $this->removeAbstractAlias($abstract);

    $isBound = $this->bound($abstract);

    unset($this->aliases[$abstract]);

    // We'll check to determine if this type has been bound before, and if it has
    // we will fire the rebound callbacks registered with the container and it
    // can be updated with consuming classes that have gotten resolved here.
    $this->instances[$abstract] = $instance;

    if ($isBound) {
        $this->rebound($abstract);
    }

    return $instance;
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L468-L486

singleton()bind() を呼び出す。第3引数にtrueをセット。

public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L386-L389

bind() はこんな感じで$concreteがstringの場合 getClosure() をbindingsに登録する

public function bind($abstract, $concrete = null, $shared = false)
{
    // snip...

    // If the factory is not a Closure, it means it is just a class name which is
    // bound into this container to the abstract type and we will just wrap it
    // up inside its own Closure to give us more convenience when extending.
    if (! $concrete instanceof Closure) {
        if (! is_string($concrete)) {
            throw new TypeError(self::class.'::bind(): Argument #2 ($concrete) must be of type Closure|string|null');
        }

        $concrete = $this->getClosure($abstract, $concrete);
    }

    $this->bindings[$abstract] = compact('concrete', 'shared');
    
    // snip...
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L249-L279

getClosure() はこんな感じで $container->resolve()$container->build() を返すクロージャーを生成する

protected function getClosure($abstract, $concrete)
{
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }

        return $container->resolve(
            $concrete, $parameters, $raiseEvents = false
        );
    };
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L288-L299

registerBaseServiceProviders() はこんな感じでServiceProviderを登録する

protected function registerBaseServiceProviders()
{
    $this->register(new EventServiceProvider($this));
    $this->register(new LogServiceProvider($this));
    $this->register(new RoutingServiceProvider($this));
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Foundation/Application.php#L219-L224

register()$provider->register() を呼び出したり、 $bindings$singletonsbind() したり singleton() で設定する。

public function register($provider, $force = false)
{
    // 省略...

    $provider->register();

    // If there are bindings / singletons set as properties on the provider we
    // will spin through them and register them with the application, which
    // serves as a convenience layer while registering a lot of bindings.
    if (property_exists($provider, 'bindings')) {
        foreach ($provider->bindings as $key => $value) {
            $this->bind($key, $value);
        }
    }

    if (property_exists($provider, 'singletons')) {
        foreach ($provider->singletons as $key => $value) {
            $this->singleton($key, $value);
        }
    }

    $this->markAsRegistered($provider);

    // If the application has already booted, we will call this boot method on
    // the provider class so it has an opportunity to do its boot logic and
    // will be ready for any usage by this developer's application logic.
    if ($this->isBooted()) {
        $this->bootProvider($provider);
    }

    return $provider;
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Foundation/Application.php#L673-L713

例えば RoutingServiceProvider の register() はこんな感じでRouterなどのクラスをappに設定している

public function register()
{
    $this->registerRouter();
    $this->registerUrlGenerator();
    $this->registerRedirector();
    $this->registerPsrRequest();
    $this->registerPsrResponse();
    $this->registerResponseFactory();
    $this->registerControllerDispatcher();
}

/**
 * Register the router instance.
 *
 * @return void
 */
protected function registerRouter()
{
    $this->app->singleton('router', function ($app) {
        return new Router($app['events'], $app);
    });
}

Illuminate\Foundation\Application@make()

make() すると resolve() が呼ばれる。

protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $abstract = $this->getAlias($abstract);

    // 省略...
    
    $concrete = $this->getContextualConcrete($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null($concrete);

    // If an instance of the type is currently being managed as a singleton we'll
    // just return an existing instance instead of instantiating new instances
    // so the developer can keep using the same objects instance every time.
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    if (is_null($concrete)) {
        $concrete = $this->getConcrete($abstract);
    }

    // We're ready to instantiate an instance of the concrete type registered for
    // the binding. This will instantiate the types, as well as resolve any of
    // its "nested" dependencies recursively until all have gotten resolved.
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // 省略...
    
    // If the requested type is registered as a singleton we'll want to cache off
    // the instances in "memory" so we can return it later without creating an
    // entirely new instance of an object on each subsequent request for it.
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }
    
    // 省略...
    
    // Before returning, we will also set the resolved flag to "true" and pop off
    // the parameter overrides for this build. After those two things are done
    // we will be ready to return back the fully constructed class instance.
    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L726-L789

$abstract = $this->getAlias($abstract);

でabstractからaliasを取得

既にインスタンス化されていたらそれを返す。

if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
    return $this->instances[$abstract];
}

例えば app は↑の処理で instance() を呼び出して値をセットしているので、 make('app') すると $this->instances に入っている Illuminate\Foundation\Application のインスタンスが返る。

$concrete = $this->getConcrete($abstract);

でbindingsからconcreteのclosureを取得。 bindingsがなければ $abstract の文字列を返す。

$object = $this->build($concrete);

でclosureを呼び出してクラス化するか、ReflectionClass を使って $abstract のインスタンスを生成する。

build() はClosureの場合はClousureを呼び出し、そうでなければ文字列とみなしてReflectionClassで動的にインスタンスを生成する。

public function build($concrete)
{
    // If the concrete type is actually a Closure, we will just execute it and
    // hand back the results of the functions, which allows functions to be
    // used as resolvers for more fine-tuned resolution of these objects.
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    try {
        $reflector = new ReflectionClass($concrete);
    } catch (ReflectionException $e) {
        throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
    }

    // If the type is not instantiable, the developer is attempting to resolve
    // an abstract type such as an Interface or Abstract Class and there is
    // no binding registered for the abstractions so we need to bail out.
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    // If there are no constructors, that means there are no dependencies then
    // we can just resolve the instances of the objects right away, without
    // resolving any other types or dependencies out of these containers.
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    // Once we have all the constructor's parameters we can create each of the
    // dependency instances and then use the reflection instances to make a
    // new instance of this class, injecting the created dependencies in.
    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);

        throw $e;
    }

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L867-L918

$reflector->newInstanceArgs($instances) で動的にクラスを生成している。 引数は resolveDependencies() で生成している。

resolveDependencies()resolveClass()resolvePrimitive() で引数を生成する。

protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        // If the dependency has an override for this particular build we will use
        // that instead as the value. Otherwise, we will continue with this run
        // of resolutions and let reflection attempt to determine the result.
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);

            continue;
        }

        // If the class is null, it means the dependency is a string or some other
        // primitive type which we can not resolve since it is not a class and
        // we will just bomb out with an error since we have no-where to go.
        $result = is_null(Util::getParameterClassName($dependency))
                        ? $this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);

        if ($dependency->isVariadic()) {
            $results = array_merge($results, $result);
        } else {
            $results[] = $result;
        }
    }

    return $results;
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L928-L957

例えば resolveClass()make() をコールして再帰的にインスタンスを生成している。

protected function resolveClass(ReflectionParameter $parameter)
{
    try {
        return $parameter->isVariadic()
                    ? $this->resolveVariadicClass($parameter)
                    : $this->make(Util::getParameterClassName($parameter));
    }

    // If we can not resolve the class instance, we will check to see if the value
    // is optional, and if it is we will return the optional parameter value as
    // the value of the dependency, similarly to how we do this with scalars.
    catch (BindingResolutionException $e) {
        if ($parameter->isDefaultValueAvailable()) {
            array_pop($this->with);

            return $parameter->getDefaultValue();
        }

        if ($parameter->isVariadic()) {
            array_pop($this->with);

            return [];
        }

        throw $e;
    }
}

https://github.com/laravel/framework/blob/v9.8.1/src/Illuminate/Container/Container.php#L1022-L1048

build() 後は shared が設定されていればinstancesに設定してシングルトン的に振る舞うようになる

if ($this->isShared($abstract) && ! $needsContextualBuild) {
    $this->instances[$abstract] = $object;
}

今回のまとめ

このエントリーをはてなブックマークに追加