Laravelアプリケーション学習⑥（コントラクト）

本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。

#目次
1.<font color="LimeGreen">Laravelのアーキテクチャ</font>
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.データベース
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践

#１．Laravelのアーキテクチャ

##1.6 コントラクトの基本
コントラクトとは、Laravelのコアコンポーネントで利用されている関数をインターフェースとして定義したもの。コンポーネント自身もこのインターフェース（コントラクト）を利用しており、コアコンポーネントを利用しているクラスはコントラクトに依存している。

***→コントラクト（インターフェース）に依存させることにより、クラス同士が疎結合になり、機能を簡単に差し替えることができる***

下記にコアコンポーネントの１つであるIlluminate\Encryption\Encrypter（暗号化及び復号処理を実行する機能）のコードを示す。

```php
(Illuminate\Encryption\Encrypter)
<?php
namespace Illuminate\Encryption;

use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Contracts\Encryption\StringEncrypter;
use RuntimeException;

class Encrypter implements EncrypterContract, StringEncrypter
{
  --[略]--
```
上記のコード例に示すEncrypterクラスは、Illuminate\Contracts\Encryption\Encrypterコントラクトをインターフェースとして利用している。

続いて、コントラクトの定義を下記示す。

```php
(Illuminate\Contracts\Encryption\Encrypter)
<?php
namespace Illuminate\Contracts\Encryption;

interface Encrypter
{
  public function encrypt($value, $serialize = true);

  public function decrypt($payload, $unserialize = true);
}
```
上記に示すコードのencryptは暗号化を行うメソッドであり、decryptは復号化を行うメソッドである。
フレームワーク内で、この暗号化の処理を利用しているクラスは以下の通りである。
・Illuminate\Session\EncryptedStore
・Illuminate\Foundation\Http\Middleware\VertifyCsrfToken
・Illuminate\Cookie\Middleware\EncryptCookies
・Illumiate\Queue\CallQueueHandler
・Illuminate\Queue\Console\RetryCommand
・Illuminate\Queue\Queue

上記の最初の３クラスはコンストラクタ時に暗号化処理のクラスを必要とする。この時引数で指定するのは、暗号化処理の具象クラスであるIlluminate\Encryption\Encrypterではなく、インターフェースであるIlluminate\Contracts\Encryption\Encrypterである。

下記にコンストラクタの第３引数でコントラクトをタイプヒンティングしている、Illuminate\Session\EncryptedStoreの例を示す。

```php
<?php
namespace Illuminate\Session;

use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
use SessionHandlerInterface;

class EncryptedStore extends Store
{
  protected $encrypter;

  public function __construct($name, SessionHandlerInterface $handler, EncrypterContract $encrypter, $id = null)
  {
    $this->encrypter = $encrypter;

    parent::__construct($name, $handler, $id);
  }
--[略]--
}
```
上記の通り、コアコンポーネントの機能（今回は暗号と復号の処理を記述したIlluminate\Encryption\Encryper）を利用するクラスがコアコンポーネントの抽象クラス(コントラクトであるIlluminate\Contracts\Encryption\Encrypter)に依存することで疎結合なコードを実現している。
また、上記の暗号化処理は、Encrypterコントラクトが持つ暗号化メソッド(encrypt)と復号メソッド(decrypt)を実装してさえいれば、Illuminate\Encryption\Encrypterクラス（コアコンポーネント）ではなく、独自の暗号化処理に差し替えることができる。

##コントラクトを利用した機能の差し替えの例
下記に実際に暗号化処理を差し替える例を示す。
PHPの暗号化処理や暗号化通信を行うライブラリphpseclib/phpseclibを使って、暗号化の方式をBlowfishに差し替える。

（作業手順）
1. compserを利用してphpseclib/phpseclibを読み込む
2. App配下にBlowfishEncrypterクラスを新規作成
3. 作成したクラスをバインドする処理を追加

### １．composerでphpseclib/phpseclibを読み込み

```
composer require phpseclib/phpsecilb:~3.0
```

ダウンロード完了後、venderディレクトリ直下に「phpseclib」ディレクトリが作成される。

### ２．App配下にBlowfishEncrypterクラスを新規作成

App\BlowfishEncrypterクラスを作成する。

```php
<?php
declare(strict_types=1)
namespace App;

use phpseclib3\Crypt\Blowfish;
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;

class BlowfishEncrypter implements EncrypterContract
{
  protected $encrypter;
  protected $key;
  
  public function __construct(string $key)
  {
    $this->key = $key;
    $this->encrypter = new Blowfish('ecb');
    $this->encrypter->setKey($this->key);
  }
  
  public function encrypt($value, $serialize = true)
  {
    return $this->encrypter->encrypt($value);
  }
  
  public function decrypt($payload, $unserialize = true)
  {
    return $this->encrypter->decrypt($payload);
  }

  public function getKey()
  {
    return $this->key;
  }
}
```
暗号化処理はEncrypterコントラクトに定義されたメソッドを実装したクラスとする。コンストラクタでBlowfishクラスのインスタンスを生成し、任意のキーを設定している。その後、encryptメソッドとdecryptメソッドを実装している。

### ３．作成したクラスをバインドする処理の追加
BlowfishEncrypterクラスをEncrypterコントラクトの実装として利用するため、サービスコンテナにバインドする。
まずは暗号化処理をサービスコンテナにバインドしているサービスプロバイダ(Illuminate\Encryption\EncryptionServiceProvider)を下記に示す。

```php
<?php
namespace Illuminate\Encryption;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Opis\Closure\SerializableClosure;

class EncryptionServiceProvider extends ServiceProvider
{
  public function register()
  {
    $this->registerEncrypter();
    $this->registerOpisSecurityKey();
  }

  protected function registerEncrypter()
  {
    $this->app->singleton('encrypter', function ($app) {
      $config = $app->make('config')->get('app');

      return new Encrypter($this->parseKey($config), $config['cipher']);
    });
  }
}
--[略]-- 
```
***＜コード解説＞***
registerメソッドからコールされるregisterEncrypterメソッドでencrypterのバインドを行っている。子のバインドではEncrypterクラスを生成して返しているので、BlowfishEncrypterクラスを返すように再度バインドする処理を、AppServiceProviderクラスに追記する。

```php
(App\Providers\AppServiceProvider)
namespace App\Providers;

use App\Blowfish\Encrypter;
use Illuminate\Encryption\MissingAppKeyException;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
  --[略]--
  public function register()
  {
    $this->app->singleton('encrypter',  function ($app) {
      $config = $app->make('config')->get('app');
      return new BlowfishEncrypter($this->parseKey($confing));
    });
  }

  protected function parseKey(array $config)
  {
    if (Str::startsWith($key = $this->key($config), $prefix = 'base64:')) {
      $key = base64_decode(Str::after($key, $prefix));
    }
    return $key;
  }
  
  protected function key(array $config)
  {
      return tap($config['key'], function($key) {
         if (empty($key)) {
            throw new MissingAppKeyException;
         }
      });
  }
}
```
上記のコードに示す通り、registerメソッドでencryptを再バインドしている。バインドしたクロージャでBlowfishEncrypterクラスのインスタンスを生成して返す。BlowfishEncrypterのコンストラクタメソッドに渡すキーの文字列の生成は、既存の処理をそのまま流用している。
上記の修正でEncrypterを解決する（Encrypterコントラクト名で解決する）と、BlowfishEncrypterクラスのインスタンスが帰り、ビジネスロジックだけでなくCookieやCSRFトークンなどフレームワーク全体の暗号化にBlowfishが利用されるようになる。

##まとめ
・独自コンポーネントを実装する場合は、コントラクトを実装しておけば、フレームワークが要求するメソッドを実装できる。
・コンポーネントを利用する側は、コントラクトに依存する形にしておけば、同じコントラクトを実装したクラスへと柔軟に差し替えが可能になる。
・コントラクト名に対する解決はサービスコンテナが行うため、サービスプロバイダなどでサービスコンテナへのバインドを変更すれば、追加したコンポーネントに機能を差し替えて、フレームワーク全体で利用できる。