Laravelのファサード(Facade)を使うことでLaravel内のサービスを簡単に利用することができます。複雑な依存関係を持つサービスもファサードを利用することでシンプルな記述で済むため、入門者にとっても便利な機能です。

その反面簡単に利用できることでどのような仕組みで動いているかわからない人も多いのが現状です。今回は、ファサードがどうような仕組みで動作するのか確認を行い、自作のファサードを追加する方法について説明を行なっています。

ファサードを理解するためには、事前にサービスコンテナの仕組みとサービスプロバイダーの設定方法を理解しておくことをおすすめしています。下記の文書を参考にしてください。

ファーサードの仕組みを理解する

Routeファサード

Laravelをインストール後に一番最初に目にするファサードは、ルーティングファイルweb.phpに記述されているRouteファサードです。最も身近なRouteファサードを通してファサードがどのように実行されているか仕組みを確認していきます。


Route::get('/', function () {
    return view('welcome');
});

Routeファサードに関するコードの確認

ここから少しコードを追っていきますが、複雑なコードは全く出てこないので安心して読み進めてください。

Routeファサードで使用されているRouteクラスはconfig\app.phpに記述されているaliasesの中で’Route’ => Illuminate\Support\Facades\Route::classにひもづけられているため、Routeファサードを利用するとIlluminate\Support\Facades\Route::classが実行されます。


'aliases' => [
    'App' => Illuminate\Support\Facades\App::class,
    'Arr' => Illuminate\Support\Arr::class,
    'Artisan' => Illuminate\Support\Facades\Artisan::class,
    'Auth' => Illuminate\Support\Facades\Auth::class,
    'Blade' => Illuminate\Support\Facades\Blade::class,
    'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
    'Bus' => Illuminate\Support\Facades\Bus::class,
    'Cache' => Illuminate\Support\Facades\Cache::class,
    'Config' => Illuminate\Support\Facades\Config::class,
    'Cookie' => Illuminate\Support\Facades\Cookie::class,
      ・
    'Route' => Illuminate\Support\Facades\Route::class,
      ・
config/app.phpのaliasesにはRoute以外にもさまざまなファサードの設定が行われています。
fukidashi

Routeクラスには、getFacadeAccessorメソッドのみ記述されており、Facadeクラスを継承していることがわかります。


class Route extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'router';
    }
}   

下記は、Routeファサードを使ってstaticメソッドgetを実行していますが、Illuminate\Support\Facades\Facadeクラスの中身を見てもgetメソッドに関係する記述を見つけることができません。


Route::get('/',・・・);

しかしFacade.phpファイルの一番下にマジックメソッドである__callStaticメソッドを確認することができます。このマジックメソッドが重要な役割を果たします。


public static function __callStatic($method, $args)
{

    $instance = static::getFacadeRoot(); 

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}   ・

__callStaticメソッドは、staticメソッドを使ってそのクラスに存在しないメソッドを使おうとした時に呼ばれるメソッドです。

Route::get()メソッドを使った時、lluminate\Support\Facades\Routeクラスにも継承しているIlluminate\Support\Facades\Facadeクラスにもgetメソッドはありません。そのため、マジックメソッドである__callStaticメソッドが実行されます。

Facade.phpクラスにstaticメソッドでgetメソッドを追加すると__callStaticメソッドではなくgetメソッドが実行されます。このようにstaticメソッドがそのクラスに存在すると__callStaticメソッドは呼ばれません。
fukidashi

__callstaticメソッドの中身を確認していくと最初にgetFacadeRootメソッドが実行されています。


$instance = static::getFacadeRoot(); 

getFacadeRootメソッド内では、最初に確認したIlluminate\Support\Facades\RouteクラスのgetFacadeAccessorメソッドを使っていることを確認できます。getFacadeAccessorメソッドの戻り値はrouterです。


public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
}

static::getFacadeAccessor()にはrouterが入るので、理解しやすいようにrouter文字列を引数に入れてresolveFacadeInstanceメソッドを確認します。


public static function getFacadeRoot()
{
    return static::resolveFacadeInstance('router');
}

resolveFacadeInstanceメソッドのコードは下記の通りです。$nameには’router’が入っているため、オブジェクトではないので、後半にあるstatic::$app[‘router’]が何を行なっているかが重要になります。


protected static function resolveFacadeInstance($name)
{

    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }

    return static::$resolvedInstance[$name] = static::$app[$name];
}

static::$appはヘルパーメソッドapp()と同じ動作を行うので、static::$app[‘router]はapp()[‘router’]と同じことを意味します。これはサービスコンテナからrouterサービスのインスタンスが起動させる下記のコードと同じことを表しています。


app()->make('router')

__callStaticメソッドの$instanceがapp()->make(‘router’)に置き換えることができます。Router::getではgetはstaticメソッドでしたが、実際はファサードを使用してrouterインスンスを起動して、router->get()を実行しているのです。


public static function __callStatic($method, $args)
{

    $instance = app()->make('router'); // static::getFacadeRoot()

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}   ・

ここまで確認できれば、Router::get()がどのような仕組みで動作しているかが理解できたと思います。また、ファーサードはLaravelのコアであるサービスコンテナを利用しているため、サービスコンテナと深く結びついていることがわかります。

このためファサードを理解するためには、事前にサービスコンテナの知識が必要になります。

ファサードを作成する

ファサードの動作の仕組みが理解できれば自分で作ったサービスをファサードに登録することはそれほど難しいものではありません。先程までの説明を元にファーサードを新規で追加します。

サービスプロバイダーの作成

ファサードを登録するためにサービスプロバイダーを作成します。サービスプロバイダーの作成については下記の文書を参考にしてください。

追加するサービスプロバイダーファイルの名前はOwnServiceProviderでphp artisan make:provider OwnServiceProviderで作成します。中身はmyNameとい名前でサービスコンテナへ登録を行い、実行するとMyNameというクラスがインスタンス化されます。


namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class OwnServiceProvider extends ServiceProvider
{

    public function register()
    {
         $this->app->bind('myName', \Reffect\MyName::class);
    }

    public function boot()
    {
        //
    }
}

MyName.phpクラスにはrunメソッドを追加し、実行するとJohn Doeという名前が返されるシンプルなものです。


namespace Reffect;

class MyName
{
    
    public function run(){

        return 'John Doe';

    }
   
}

ファサードファイルの作成

ファサードファイルを作成する場所はLaravelのインストールフォルダの下にあるvendar/laravel/framework/src/illuminate/Supprt/Facadesの下です。Own.phpという名前でファイルを作成します。

中身はgetFacadeAccessorメソッドのみ記述し、戻り値にはサービスプロバイダーで設定したmyNameを設定します。


namespace Illuminate\Support\Facades;

class Own extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'myName';
    }
}

app.phpファイルへの追加

config/app.phpファイルに作成したファサードについての情報を追加します。


'aliases' => [

    'App' => Illuminate\Support\Facades\App::class,
    'Arr' => Illuminate\Support\Arr::class,
    ・
    ・
    'View' => Illuminate\Support\Facades\View::class,
    'Own' => Illuminate\Support\Facades\Own::class,  //これを追加

],

これでファサードを使用するための準備は完了です。

ファサードを実行

ファサードを使って下記のようにrunメソッドを実行するとJohn Doeが表示されます。


Own::run()
Class Not Foundのエラーが出る場合はuse Ownを設定してください。
fukidashi

【付録】Auth::attemptの処理の流れを理解する

Laravelの認証機能を利用するとAuthファサードが利用される場所をよく目にします。本文書を理解できた人であればAuthファサードの処理の流れも理解することができます。一緒にコードを見ていきましょう。Laravelのコードを直接読むので初心者の人にも難しいかもしれないのでわからなくても安心してください。Laravelの勉強を進めていくといずれ理解できるようになります。

Laravel8のBreezeの認証機能を利用してログイン処理を行うとLoginRequestのauthenticateメソッドの中でAuth::attemptが実行されログイン処理が行われます。


//略
use Illuminate\Support\Facades\Auth;
//略

public function authenticate()
{

    $this->ensureIsNotRateLimited();

    if (! Auth::attempt($this->only('email', 'password'), $this->filled('remember'))) {
        RateLimiter::hit($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => __('auth.failed'),
        ]);
    }

    RateLimiter::clear($this->throttleKey());
}

Auth::attemptを見ただけではどのファイルのattempt処理が行われるのか理解することはできません。ファサードとサービスプロバイダーの知識を使って読み進めることになります。途中までは先ほど説明したRouterファサードの流れと同じです。

Auth::attemptの処理を確認するためにLoginRequestの先頭に記述されているIlluminate\Support\Facades\Authファイルの中身の確認を行います。

しかしAuth.phpファイルの中にはattemptメソッドはありません。

後ほどstaticメソッドのgetFacadeAccessorを使うことになります。戻り値がauthになることを覚えておいてください。
fukidashi

class Auth extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'auth';
    }

    public static function routes(array $options = [])
    {
//略
    }
}

attemptメソッドがないのでAuthが継承しているFacadeを確認します。Illuminate\Support\Facades\Facadeクラスの中身を見てもattemptメソッドに関係する記述を見つけることができません。

メソッドが見つからない場合はFacadeファイルの一番下にあるマジックメソッドである__callStaticメソッドを確認します。


public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

__callstaticメソッドの中身を確認していくと最初にgetFacadeRootメソッドが実行されています。


$instance = static::getFacadeRoot();

getFacadeRootメソッド内では、最初に確認したIlluminate\Support\Facades\AuthクラスのgetFacadeAccessorメソッドを使っていることを確認できます。


public static function getFacadeRoot()
{
   return static::resolveFacadeInstance(static::getFacadeAccessor());
}

getFacadeAccessorメソッドの戻り値はauthなので下記のように書き換えることができます。


public static function getFacadeRoot()
{
   return static::resolveFacadeInstance('auth');
}

resolveFacadeInstanceメソッドのコードは下記の通りです。$nameには’auth’が入っているため、オブジェクトではないので、後半にあるstatic::$app[‘auth’]が何を行なっているかが重要になります。


protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }

    if (static::$app) {
        return static::$resolvedInstance[$name] = static::$app[$name];
    }
}

static::$appはヘルパーメソッドapp()と同じ動作を行うので、static::$app[‘auth]はapp()[‘auth’]と同じことを意味します。これはサービスコンテナからauthサービスのインスタンスが起動させる下記のコードと同じことを表しています。


app()->make('auth')

app()->make(‘auth’)によってどのサービスのインスタンスが作成されているのかを確認する必要があります。authのサービスは、Illuminate\Auth\AuthServiceProviderのregisterメソッドによって登録されており、AuthMangaerがインスタンス化されていることがわかります。


class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerAuthenticator();
        $this->registerUserResolver();
        $this->registerAccessGate();
        $this->registerRequirePassword();
        $this->registerRequestRebindHandler();
        $this->registerEventRebindHandler();
    }

    /**
     * Register the authenticator services.
     *
     * @return void
     */
    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', function ($app) {
            return new AuthManager($app);
        });

        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }
//略

ここまでの流れでstatic::getFacadeRoot()はapp()->make(‘auth’)が実行され、最終的にはAuthManagerのインスタンスが作成されることがわかりました。


public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot(); //app()->make('auth') → AuthManager

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }


    return $instance->$method(...$args); //AuthMangaer->attempt(...$args)
}

Auth::attemptはAuthManagerのattemptメソッドが実行されていることになりますがIlluminate\Auth\AuthManager.phpファイルでattemptメソッドを探しても見つけることはできません。

attemptメソッドが存在しないためAuthManagerファイルの一番下にある__callメソッドが実行されます。__callメソッドの中では$this->guard()メソッドが実行されているのでその処理を見ていきましょう。


public function __call($method, $parameters)
{
    return $this->guard()->{$method}(...$parameters);
}

guardメソッドは下記の通りです。$nameはnullなので$this->getDefaultDriverメソッドが実行されます。


public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

getDefaultDriverメソッドではconfigフォルダのauth.phpファイルのdefaultのguardの値が戻されます。


public function getDefaultDriver()
{
    return $this->app['config']['auth.defaults.guard'];
}

auth.phpファイルからwebであることがわかります。


'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

$nameの値が”web”だということがわかったので、$this->guards[‘web’]の値をチェックしますが値がないため$this->reslove(‘web’)メソッドが実行されます。


public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

resolveメソッドのコードを追っていくと$this->createSessionDriver(‘web’, $config)が実行されることになります。


protected function resolve($name)
{

    $config = $this->getConfig($name); //$config = ["driver"=>"session", "provider"=>"users"]

    if (is_null($config)) {
        throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
    }

    if (isset($this->customCreators[$config['driver']])) {
        return $this->callCustomCreator($name, $config);
    }

    $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; //createSessionDriver

    if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($name, $config); //$this->createSessionDriver('web', $config)
    }

    throw new InvalidArgumentException(
        "Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
    );
}

creteSessionDriverメソッドではSessionGuardクラスがインスタンス化され$guard変数に保存されていることがわかります。


public function createSessionDriver($name, $config)
{
    $provider = $this->createUserProvider($config['provider'] ?? null);

    $guard = new SessionGuard($name, $provider, $this->app['session.store']);

    // When using the remember me functionality of the authentication services we
    // will need to be set the encryption instance of the guard, which allows
    // secure, encrypted cookie values to get generated for those cookies.
    if (method_exists($guard, 'setCookieJar')) {
        $guard->setCookieJar($this->app['cookie']);
    }

    if (method_exists($guard, 'setDispatcher')) {
        $guard->setDispatcher($this->app['events']);
    }

    if (method_exists($guard, 'setRequest')) {
        $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
    }

    return $guard;
}

__callメソッドのguard()の戻り値がSessonGuardクラスであることがわかりました。


public function __call($method, $parameters)
{
    return $this->guard()->{$method}(...$parameters); //SessionGuard->attempt(...$parameters)
}

ここまでの流れを追ってようやくAuth::attemptがSessionGuard()->attemptであることがわかりました。

SessionGuard.phpファイルにはattemptメソッドも存在します。


public function attempt(array $credentials = [], $remember = false)
{

    $this->fireAttemptEvent($credentials, $remember);

    $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

    // If an implementation of UserInterface was returned, we'll ask the provider
    // to validate the user against the given credentials, and if they are in
    // fact valid we'll log the users into the application and return true.
    if ($this->hasValidCredentials($user, $credentials)) {
        $this->login($user, $remember);

        return true;
    }

    // If the authentication attempt fails we will fire an event so that the user
    // may be notified of any suspicious attempts to access their account from
    // an unrecognized user. A developer may listen to this event as needed.
    $this->fireFailedEvent($user, $credentials);

    return false;
}

このようにLaravelではファサードを理解できたことでファサードを使ってどのような処理が行われるのかを理解することができるようになりました。もしファーサードの理解できていなければLaravelの認証を機能を理解できないのでファサードがLaravelでは重要な機能であることがわかりました。