Help us understand the problem. What is going on with this article?

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした