Laravelをデザインパターンで考察する ~ Builder Pattern ~


デザインパターン

デザインパターンは、いろんな文脈で使われるが、ここでのデザインパターンはGoFのデザインパターン。
その中で今回は、Builder Patternでの実装箇所を取り上げる。
Builder Patternは、デザインパターンの分類、生成・構成・振る舞いのうちの生成に分類される。
生成過程を抽象化したり、コンストラクタの引数が多いとか複雑なときに使うデザインパターン。

基本的な説明は以下のリンクに任せる

[デザインパターン]
https://www.techscore.com/tech/DesignPattern/index.html/

[Builderパターン | PHPデザインパターン]
https://www.ritolab.com/entry/125

Illuminate\Support\Managerクラス

Illuminate\Support\Managerクラスは、Laravelに用意されているBuilder Patternを簡単に利用するための抽象クラス。
インスタンス生成過程を隠蔽し、シンプルに再利用するために継承して使用することができる。

Session, Cache, Auth等で使用されていて、Authの認証方式やSessionのストレージをLaravelで初期状態で使用できるもの以外を使いたい場合は、自前ですべて作成するのではなく、Manageクラスに従って拡張すれば、Laravelの機能をできるだけ残したまま拡張できる。
また、configの変更等最小のステップでほぼコードを書くことなく、設定を変更してSessionを別プロジェクトでも再利用が簡単にできる。

なので機能拡張の際、Managerクラスを継承あるいは引用しているクラスをLaravelが使用している場合、これに乗っかって拡張するのが楽。
新規機能を作る時も、Managerクラスを使えば、Driverを使ったBuilderパターンを容易に使用できる。

最近はLaravelのManagerコンポーネント(AuthManager等)は、継承せずに実装されるように変更されている(処理自体は踏襲されている)。
現在継承して使用されているのはSessionManagerなのでSessionManagerを使用して説明する。

SessionManager

Sessionで使用する機能(Illuminate\Session\Store)を再利用しつつ、使用者は保存先のストレージをconfigを使用して簡単に変更し、Session用のオブジェクト(Illuminate\Session\Store)を生成できるようにというのが、SessionManagerの趣旨
Sessionのストレージの選択肢は、ほぼ確定しているのでそのストレージごとに生成過程を見やすく管理できる。

SessionServiceProviderh

Illuminate\Session\SessionServiceProvider
protected function registerSessionManager()
{
    $this->app->singleton('session', function ($app) {
        return new SessionManager($app);
    });
}

SessionServiceProviderでSessionManagerがコンテナに登録されている
SessionManagerは、あくまで生成過程を抽象化してくれているもので、最終的にはIlluminate\Session\Storeのインスタンスが帰ってくる。

SessionManager

Illuminate\Session\SessionManager
protected function callCustomCreator($driver) //たぶん独自にDriverを登録できるやつ
{
    return $this->buildSession(parent::callCustomCreator($driver));
}

// 既存のDriverメソッドの抜粋
protected function createFileDriver()
{
    return $this->createNativeDriver();
}

protected function createMemcachedDriver()
{
    return $this->createCacheBased('memcached');
}

protected function createDynamodbDriver()
{
    return $this->createCacheBased('dynamodb');
}

// ここで/config/session.phpのdriverをDriverとしてセットしている。
public function getDefaultDriver()
{
    return $this->app['config']['session.driver'];
}

生成過程で必要になる要素を、Driverとして登録する。
ここではSessionを保存するためのStorageをDriverとして登録しておく。
既にFile、memcached、Redis、Dynamoとかいくつか用意されている。
callCustomCreatorで独自のDriverを登録できるはず(試したことない)
各Driverメソッド内で生成過程の差異を吸収する。
Sessionに別のストレージを使いたい場合この仕組みを利用するとLaravelの仕様に乗っかれて便利。

また、configでSessionを設定できるように、$this->app['config']を使用してStoreの生成を抽象化している。

Manager

Illuminate\Support\Manager
protected function createDriver($driver)
{
    if (isset($this->customCreators[$driver])) {
        return $this->callCustomCreator($driver);
    } else {
        $method = 'create'.Str::studly($driver).'Driver';
        if (method_exists($this, $method)) {
            return $this->$method();
        }
    }
    throw new InvalidArgumentException("Driver [$driver] not supported.");
}

// これでdriverメソッドで生成したStoreのメソッドをSessionManagerから直接使用できる
// Session::get('hoge')て感じで(SessionはSessionManagerのファサード)
public function __call($method, $parameters)
{
    return $this->driver()->$method(...$parameters);
}

createDriverにはDefaultDriverが渡されて、
\$method = 'create'.Str::studly($driver).'Driver';
のところでDriverメソッドを確定している。

SessionManagerの効果

SessionManagerにより、SessionのストレージをDATABASEにしたい場合、下記を設定するだけで、SessionファサードでDatabaseストレージに対応したStoreインスタンスが取得できるようになる。

.env
SESSION_DRIVER=database
Artisanコマンド実行
php artisan session:table

php artisan migrate
Session処理
Session::all();

Session::get('hoge');

SMS用のManagerクラスを作ってみる

Smsは外部サービスに処理を投げるので、Driverとして使用するSmsサービスを定義しておく。
今回は、NexmoとTwilioのDriverメソッドを定義する。このメソッドからSms送信用のDriverクラスを定義しておく。なのでSmsManagerが返すインスタンスは、Driverクラス。

App\Components\Sms\SmsManager

SmsManager
<?php

namespace App\Components\Sms;

use Illuminate\Support\Manager;
use Nexmo\Client as NexmoClient; // NexmoのPHPクライアント
use Twilio\Rest\Client as TwilioClient; // TwilioののPHPクライアント
use App\Components\Sms\Drivers\NullDriver; // configに設定がない時用のDriver
use App\Components\Sms\Drivers\NexmoDriver;
use App\Components\Sms\Drivers\TwilioDriver;
use Nexmo\Client\Credentials\Basic as NexmoBasicCredentials; // Nexmoの認証に使うやつ

class SmsManager extends Manager
{
    public function channel($name = null)
    {
        return $this->driver($name);
    }

    public function createNexmoDriver()
    {
        return new NexmoDriver(
            $this->createNexmoClient(),
            $this->app['config']['sms.nexmo.from']
        );
    }

    public function createTwilioDriver()
    {
        return new TwilioDriver(
            $this->createTwilioClient(),
            $this->app['config']['sms.twilio.from']
        );
    }

    protected function createNexmoClient()
    {
        return new NexmoClient(
            new NexmoBasicCredentials(
                $this->app['config']['sms.nexmo.key'],
                $this->app['config']['sms.nexmo.secret']
            )
        );
    }

    protected function createTwilioClient()
    {
        return new TwilioClient(
            $this->app['config']['sms.twilio.key'],
            $this->app['config']['sms.twilio.secret']
        );
    }

    public function createNullDriver()
    {
        return new NullDriver;
    }

    public function getDefaultDriver()
    {
        return $this->app['config']['sms.default'] ?? 'null';
    }
}

createClientで、SMSのapiの初期化をして各DriverClassに渡す。

App\Components\Sms\Drivers\Driver

各外部サービスのDriverクラスの抽象クラス、共通の設定処理を抽象化しておく。

App\Components\Sms\Drivers\Driver
<?php

namespace App\Components\Sms\Drivers;

use Illuminate\Support\Arr;
use App\Components\Sms\Exceptions\SmsException;

abstract class Driver implements
{
    protected $recipient;

    protected $message;

    abstract public function send();

    public function to(string $recipient) // 宛先の設定
    {
        $this->recipient = $recipient;

        return $this; // メソッドチェーンしたいので$thisを返しておく。
    }

    public function content(string $message) // メッセージの設定
    {
        $this->message = $message;

        return $this;
    }
}

App\Components\Sms\Drivers\TwilioDriver

Twilio用のDriverクラス

App\Components\Sms\Drivers\TwilioDriver
<?php

namespace App\Components\Sms\Drivers;

use Twilio\Rest\Client as TwilioClient;

class TwilioDriver extends Driver
{
    /**
     * The Twilio client.
     * 
     * @var \Twilio\Rest\Client
    */
    protected $client;

    /**
     * The phone number this sms should be sent from.
     *
     * @var string
     */
    protected $from;

    /**
     * Create a new Twilio driver instance.
     *
     * @param  \Twilio\Rest\Client  $twilio
     * @param  string  $from
     * @return void
    */
    public function __construct(TwilioClient $twilio, $from)
    {
        $this->client = $twilio;
        $this->from = $from;
    }

    public function send() // 送信処理
    {
        return $this->client->messages->create(
            $this->recipient, [
                'from' => $this->from,
                'body' => trim($this->message)
            ]
        );
    }
}

使ってみる

SMS::channel('twilio')
    ->to($phoneNumber)
    ->content('Using twilio driver to send SMS')
    ->send();

channelメソッドでDriver(TwilioかNexmo)を指定して、各種設定をしてメッセージを送信できて便利。

参考サイト

https://www.slideshare.net/BobbyBouwmann/laravel-design-patterns-86729254
https://itnext.io/building-driver-based-components-in-laravel-5b390dc25bd9
https://github.com/orobogenius/building-driver-based-components