コンテンツにスキップ

コンポーネント@Laravel

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. Laravelの全体像

ライフサイクル

laravel-lifecycle

大まかな処理フローは以下の通りである。

用語 説明
1 リクエストを受信する。
2 index.phpファイル エントリーポイントから処理が開始する。
3 Autoload autoload.phpファイルにて、パッケージを自動的にロードする。
4 Load App bootstrap/app.phpファイルにて、ServiceContainer (Illuminate\Foundation\Application.php) を実行する。
5 Http Kernel Kernelを実行する。
6 ・Register ServiceProviders
・Boot Service Providers
ServiceProviderのregisterメソッドやbootメソッドを実行する。これにより、ServiceContainerにクラスがバインドされる。
7 Middleware BeforeMiddlewareを実行する。
8 ・Dispatch by Router
・Routes Match
web.phpファイル、app.phpファイルなどのルーティング定義を元に、Routerが実行する。
9 FormRequest バリデーションを実行する。
10 Controller Controllerを基点として、DBにまで処理が走る。
11 Resource DBから取得したコレクション型データを配列型データに変換する。
12 Response Responseを実行する。配列型データをJSON型データに変換する。
13 Terminate Middleware AfterMiddlewareが実行される。
14 View blade.phpファイルを基に静的ファイルが作成される。
15 レスポンスを返信する。


コンポーネントのコード

Laravelの各コンポーネントには、似たような名前のメソッドが多く内蔵されている。

そのため、同様の能力を実現するために、各々が異なるメソッドを使用しがちになる。

その時、各メソッドがブラックボックスにならないように、処理の違いをコードから確認する必要がある。


02. Application

App

▼ 設定方法

APP_NAME=<サービス名>
APP_ENV=<実行環境名>
APP_KEY=<セッションの作成やパスワードの暗号化に使用する認証キー>
APP_DEBUG=<デバッグモードの有効無効化>
APP_URL=<アプリケーションのURL>

app.phpファイルの基本設定

<?php

return [
    // アプリケーション名
    "name"            => env("APP_NAME", "Laravel"),

    // 実行環境名
    "env"             => env("APP_ENV", "production"),

    // エラー時のデバッグ画面の有効化
    "debug"           => (bool)env("APP_DEBUG", false),

    // アプリケーションのURL
    "url"             => env("APP_URL", "http://localhost"),

    // assetヘルパーで付与するURL
    "asset_url"       => env("ASSET_URL", null),

    // タイムゾーン
    "timezone"        => "UTC",

    // 言語設定
    "locale"          => "ja",
    "fallback_locale" => "en",
    "faker_locale"    => "ja_JP",

    // セッションの作成やパスワードの暗号化に使用する認証キー
    "key"             => env("APP_KEY"),

    // 暗号アルゴリズム
    "cipher"          => "AES-256-CBC",

    // サービスプロバイダー
    "providers"       => [

    ],

    // クラス名のエイリアス
    "aliases"         => [

    ],
];


03. Config


04. Console

Command

artisanコマンドで実行できるコマンド処理を定義する。

<?php

declare(strict_types=1);

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Log;

class FooCommand extends Command
{
    /**
     * @var string
     */
    protected $signature = 'do-foo {bar}'; // コマンドとパラメータ

    /**
     * @var string
     */
    protected $description = 'コマンドの仕様を記入します'; // コマンドの説明文

    /**
     * @return void
     */
    public function handle(): void
    {
        Log::info('START: artisan do-foo');

        // パラメーターを取得します。
        $bar = $this->argument('bar');

        // 何らかのコマンド処理

        Log::info('END: artisan do-foo');
    }
}

定義したCommandクラスは、以下の様に実行できる。

$ php artisan command:do-foo


05. Database

DB

▼ 設定方法

環境変数を.envファイルに実装する。

database.phpファイルから、指定された設定が選択される。

DB_CONNECTION=<RDB名>
DB_HOST=<ホスト名>
DB_PORT=<ポート番号>
DB_DATABASE=<DB名>
DB_USERNAME=<アプリケーションユーザー名>
DB_PASSWORD=<アプリケーションユーザーのパスワード>

▼ RDBとRedisの選択

<?php

return [

    // 使用するDBMSを設定
    "default"     => env("DB_CONNECTION", "mysql"),

    "connections" => [

        // DB接続情報 (SQLite)
        "sqlite" => [

        ],

        // DB接続情報 (MySQL)
        "mysql"  => [

        ],

        // DB接続情報 (pgSQL)
        "pgsql"  => [

        ],

        // DB接続情報 (SQLSRV)
        "sqlsrv" => [

        ],
    ],

    // DBマイグレーションファイルのあるディレクトリ
    "migrations"  => "migrations",

    // Redis接続情報
    "redis"       => [

    ],
];


エンドポイントに応じた設定

▼ 単一のエンドポイント

単一のエンドポイントしかない場合、DB_HOSTを1つだけ設定する。

<?php

return [

    ...

    "default" => env("DB_CONNECTION", "mysql"),

    "connections" => [

        ...

        "mysql" => [
            "driver"         => "mysql",
            "url"            => env("DATABASE_URL"),
            "host"           => env("DB_HOST", "127.0.0.1"),
            "port"           => env("DB_PORT", 3306),
            "database"       => env("DB_DATABASE", "forge"),
            "username"       => env("DB_USERNAME", "forge"),
            "password"       => env("DB_PASSWORD", ""),
            "unix_socket"    => env("DB_SOCKET", ""),
            "charset"        => "utf8mb4",
            "collation"      => "utf8mb4_unicode_ci",
            "prefix"         => "",
            "prefix_indexes" => true,
            "strict"         => true,
            "engine"         => null,
            "options"        => extension_loaded("pdo_mysql") ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env("MYSQL_ATTR_SSL_CA"),
            ]) : [],
        ],
    ],

    ...

];

▼ 複数のエンドポイント

複数のエンドポイントがある場合、書き込み処理と読み出し処理をそれ専用のエンドポイントに向けるようにする。

例えば、AWS RDSを採用している場合、プライマリーインスタンスに向け、また読み出し処理をリードレプリカに向けることにより、負荷を分散できる。

この場合、環境変数に2個のインスタンスのエンドポイントを実装する必要がある。

DB_HOST_PRIMARY=<プライマリーインスタンスのエンドポイント>
DB_HOST_READ=<リードレプリカのエンドポイント>

注意点として、stickyキーを有効化しておくと良い。

プライマリーインスタンスにおけるデータ更新がリードレプリカに同期される前に、リードレプリカに対して読み出し処理が起こるような場合、これを防げる。

<?php

return [

    "default"     => env("DB_CONNECTION", "mysql"),

    "connections" => [

        // 〜 中略 〜

        'mysql' => [

            // 〜 中略 〜

            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'read' => [
                'host' => [
                    # リーダーエンドポイントを設定
                    env('DB_HOST_READER', '127.0.0.1'),
                ],
            ],
            'write' => [
                'host' => [
                    # クラスターエンドポイントを設定
                    env('DB_HOST_PRIMARY', '127.0.0.1'),
                ],
            ],
            'sticky' => true,

            // 〜 中略 〜

        ],

        // 〜 中略 〜
    ],
];


Redis

▼ クエリキャッシュ管理

環境変数を.envファイルに実装する必要がある。

CACHE_DRIVER=redis
REDIS_HOST=<Redisのホスト>
REDIS_PASSWORD=<Redisのパスワード>
REDIS_PORT=<Redisのポート>


06. Event/Listener

Event

▼ Eventとは

ビジネスの出来事がモデリングされたイベントオブジェクトとして動作する。

▼ 構成

イベントに関するデータを保持するのみで、ビジネスロジックを持たない構成となる。

*実装例*

<?php

namespace App\Events;

use App\Models\User;

final class UserCreatedEvent
{
    /**
     * @var User
     */
    protected User $user;

    /**
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

任意の場所でイベントを発行できる。

<?php

event(new UserCreatedEvent($user));

▼ EloquentモデルのCRUDイベント

Eloquentモデルでは、DBアクセスに関するメソッドの実行開始や終了の処理タイミングをイベントクラスに紐付けられる。

紐付けるために、プロパティで定義するか、あるいは各タイミングで実行される無名関数でイベントを発生させる必要がある。

*実装例*

プロパティにて、CREATE処理とDELETE処理に独自イベントクラスに紐付ける。

<?php

namespace App\Models;

use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * @var array
     */
    protected $dispatchesEvents = [
        // 処理タイミングを独自イベントに紐付ける
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

*実装例*

無名関数にて、CREATE処理に独自イベントクラスに紐付ける。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * @return void
     */
    protected static function booted()
    {
        static::created(function ($user) {
            event(new UserCreated($user));
        });
    }
}


Listener

▼ Listenerとは

イベントが発生した時に、これに紐付いてコールされるオブジェクトのこと。

▼ 構成

Listenerクラスがコールされた時に実行する処理をhandle関数に定義する。

*実装例*

ユーザーが作成された時に、メールアドレスにメッセージを送信する。

<?php

namespace App\Listeners;

use App\Events\UserCreatedEvent;
use Illuminate\Support\Facades\Notification;

final class UserCreatedEventListener
{
    /**
     * @param UserCreatedEvent $userEvent
     * @return void
     */
    public function handle(UserCreatedEvent $userEvent)
    {
        // UserクラスがNotifiableトレイトに依存せずに通知を実行できるように、オンデマンド通知を使用します。
        Notification::route('mail', $userEvent->user->userEmailAddress->emailAddress)
            ->notify(new UserCreatedEventNotification($userEvent->user));
    }
}

任意の場所でイベントを発行すると、リスナーが自動的にコールされる。

<?php

event(new UserCreatedEvent($user));

▼ イベントとリスナーの紐付け

EventServiceProviderクラスにて、Eventクラスに紐付ける1つ以上のListenerクラスを設定する。

*実装例*

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Events\UserCreatedEvent;
use App\Listeners\UserCreatedEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * @var array
     */
    protected $listen = [
        UserCreatedEvent::class => [
            UserCreatedEventListener::class,
        ],
    ];

    /**
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

▼ 任意のEloquentモデルCRUDイベントの検知

Laravelの多くのコンポーネントに、bootメソッドが定義されている。

Eloquentモデルでは、インスタンス作成時にbootメソッドがコールされ、これによりにbootTraitsメソッドが実行される。

Traitにboot+<クラス名>という名前の静的メソッドが定義されていると、bootTraitsメソッドはこれをコールする。

bootTraitsメソッドの中でEloquentモデルのイベントを発生させることにより、全てのEloquentモデルのイベントを一括で発火させられる。

*実装例*

あらかじめTraitを定義する。

savedメソッドにEloquentモデルの更新イベントを登録可能にする。

<?php

namespace App\Models\Traits;

use Illuminate\Database\Eloquent\Model;
use App\Events\UpdatedModelEvent;

trait UpdatedModelTrait
{
    /**
     * @return void
     */
    protected static function bootUpdatedModelTrait(): void
    {
        // 任意のEloquentモデルのsaveメソッド実行時
        static::saved(function (Model $updatedModel) {
            // イベントを発生させる。
            event(new UpdatedModelEvent($updatedModel));
        });

        // 任意のEloquentモデルのdeleteメソッド実行時
        static::deleted(function (Model $updatedModel) {
            // イベントを発生させる。
            event(new UpdatedModelEvent($updatedModel));
        });
    }

    /**
     * イベントを発火させずにModelを保存します。


     *
     * @return void
     */
    protected static function saveWithoutEvents(): void
    {
        // 無限ループを防ぐために、save実行時にイベントが発火しないようにする。
        return static::withoutEvents(function () use ($options) {

            // プロパティの変更を保存。
            return $this->save($options);
        });
    }
}

イベントを定義する。

<?php

namespace App\Events;

class UpdatedModelEvent
{
    /**
     * @var Model
     */
    public $updatedModel;

    /**
     * @param Model
     */
    public function __construct(Model $updatedModel)
    {
        $this->$updatedModel = $updatedModel;
    }
}

Model更新イベントが発火してコールされるリスナーを定義する。

create_byカラムまたはupdated_byカラムを指定した更新者名に更新可能にする。

注意点として、イベントとリスナーの対応関係は、EventServiceProviderで登録する。

<?php

namespace App\Listeners;

use App\Events\UpdatedModelEvent;
use App\Constants\ExecutorConstant;

class UpdatedModelListener
{
    /**
     * @param UpdatedModelEvent
     * @return void
     */
    public function handle(UpdatedModelEvent $updatedModelEvent): void
    {
        $by = $this->getModelUpdater();

        // create_byプロパティに値が設定されているかを判定。
        if (is_null($updatedModelEvent->updatedModel->created_by)) {
            $updatedModelEvent->updatedModel->created_by = $by;
        }

        $updatedModelEvent->updatedModel->updated_by = $by;

        $updatedModelEvent->updatedModel->saveWithoutEvents();
    }

    /**
     * 更新処理の実行者を取得します。


     *
     * @return string
     */
    private function getModelUpdater(): string
    {
        // コンソール経由で実行されたかを判定。
        if (app()->runningInConsole()) {
            return ExecutorConstant::ARTISAN_COMMAND;
        }

        // API認証に成功したかを判定。
        if (auth()->check()) {
            return ExecutorConstant::STAFF . ":" . auth()->id();
        }

        return ExecutorConstant::GUEST;
    }
}

実行者名は、定数として管理しておくと良い。

<?php

namespace App\Constants;

/**
 * 実行者定数クラス
 */
class ExecutorConstant
{
    /**
     * Artisanコマンド
     */
    public const ARTISAN_COMMAND = "Artisan Command";

    /**
     * スタッフ
     */
    public const STAFF = "Staff";

    /**
     * ゲスト
     */
    public const GUEST = "Guest";
}


07. Exception

Laravelにおけるエラーハンドリング

エラーハンドリングは4個のステップからなる。

LaravelではデフォルトでHandlerクラスが全てのステップをカバーしている。

また加えて、異常系レスポンスを自動的に返信してくれる。

エラーハンドリングのステップのうち、エラー検出については言及しないこととする。


例外スロー

▼ 例外

ドキュメントとしてまとめられていないが、デフォルトで様々な例外が備わっている。

▼ スタックトレース

Laravelはスローされる例外のメッセージをスタックトレースで作成する。

また、Laravel内部で例外キャッチと新たな例外の投げ直しが行われるため、[previous exception]によって例外が結合される。

スタックトレースには機密性の高い情報が含まれるため、クライアントへの異常系レスポンスのエラーメッセージには割り当てずに、ロギングだけしておく。

エラーが複数行にまたがるため、CloudWatchやFluentBitなどのログ収集ツールでは、各行を繋げて扱えるように設定が必要である。

補足として、ログの詳細度はAPP_DEBUG環境変数で制御できる。

[2021-09-00 00:00:00] local.ERROR: ***** (エラーメッセージ)
[stacktrace]
#0 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(652): Illuminate\\Database\\Connection->runQueryCallback('insert into `us...', Array, Object(Closure))
#1 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(486): Illuminate\\Database\\Connection->run('insert into `us...', Array, Object(Closure))
#2 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(438): Illuminate\\Database\\Connection->statement('insert into `us...', Array)

...

#60 /var/www/foo-app/framework/src/Illuminate/Foundation/Http/Kernel.php(141): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#61 /var/www/foo-app/framework/src/Illuminate/Foundation/Http/Kernel.php(110): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#62 /var/www/foo-app/public/index.php(55): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#63 {main}

[previous exception] [object] ***** (エラーメッセージ)
[stacktrace]
#0 /var/www/foo-app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php(114): Doctrine\\DBAL\\Driver\\PDO\\Exception::new(Object(PDOException))
#1 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(485): Doctrine\\DBAL\\Driver\\PDOStatement->execute()
#2 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(685): Illuminate\\Database\\Connection->Illuminate\\Database\\{closure}('insert into `us...', Array)

...

#63 /var/www/foo-app/framework/src/Illuminate/Foundation/Http/Kernel.php(141): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#64 /var/www/foo-app/framework/src/Illuminate/Foundation/Http/Kernel.php(110): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#65 /var/www/foo-app/public/index.php(55): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#66 {main}

[previous exception] [object] ***** (エラーメッセージ)
[stacktrace]
#0 /var/www/foo-app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php(112): PDOStatement->execute(NULL)
#1 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(485): Doctrine\\DBAL\\Driver\\PDOStatement->execute()
#2 /var/www/foo-app/framework/src/Illuminate/Database/Connection.php(685): Illuminate\\Database\\Connection->Illuminate\\Database\\{closure}('insert into `us...', Array)

...

#63 /var/www/foo-app/framework/src/Illuminate/Foundation/Http/Kernel.php(141): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#64 /var/www/foo-app/framework/src/Illuminate/Foundation/Http/Kernel.php(110): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#65 /var/www/foo-app/public/index.php(55): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#66 {main}
"}


ロギング

reportメソッド

Laravel内部でキャッチされた例外を基に、ロギングを実行する。

<?php

namespace App\Exceptions;

use Throwable;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        //
    ];

    public function report(Throwable $exception)
    {
        parent::report($exception);
    }

    ...

}


異常系レスポンスの返信

renderメソッド

Laravel内部でキャッチされた例外を基に、異常系レスポンスを自動的に返信する。

異常系レスポンスの返信処理もこれに追加できるが、異常系レスポンス間が密結合になるため、できるだけいじらない。

代わりとして、各コントローラーにtry-catchと異常系レスポンスの返信処理を実装する。

<?php

namespace App\Exceptions;

use Throwable;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    public function render($request, Throwable $exception)
    {
        return parent::render($request, $exception);
    }

    ...
}


08. Facade

Facade

▼ Facadeとは

Facadeに登録されたクラス (Facadeクラス) とServiceContainerを繋ぐ静的プロキシとして働く。

メソッドをコールできるようになる。

▼ Facadeを使用しない場合

new演算子でインスタンスを作成する。

*実装例*

<?php

namespace App\Domain\DTO;

class Foo
{
    public function method()
    {
        return "foo";
    }
}
<?php

// 通常
$foo = new Foo();
$foo->method();

▼ Facadeの静的プロキシを使用する場合

静的メソッドの記法でコールできる。

ただし、自作クラスをFacadeを使用してインスタンス化すると、スパゲッティな『Composition (合成) 』の依存関係を生じさせてしまう。

例えば、Facadeの中でも、Routeのような、代替するよりもFacadeを使用したほうが断然便利である部分以外は、使用しないほうが良い。

*実装例*

Facadeとして使用したいクラスを定義する。

<?php

namespace App\Domain\DTO;

class Foo
{
    public function method()
    {
        return "foo";
    }
}

エイリアスとクラスの名前空間をconfig/app.phpファイルをaliasesキーに登録すると、そのエイリアスでインスタンス化とメソッドコールを行えるようになる。

<?php

"aliases" => [
    "Foo" => App\Models\Foo::class,
]

インスタンス化とメソッドコールを実行する。

<?php

use Illuminate\Support\Facades\Foo;

// Facade利用
$result = Foo::method();

▼ Facadeを使用した方が良い場合

Facadeがトレイトの代わりになる場合、Facadeを使用することにより、責務がドメインモデルに集中せずにすむ。

*例*

NotifiableトレイトをUserクラスで使用せずに、Notificationファサードによるオンデマンド通知を使用することにより、Userクラスが通知処理の責務を持たずに済む。

▼ 標準登録されたFacadeクラスの種類

以下のクラスは、デフォルトで登録されているFacadeである。

エイリアス クラス名 サービスコンテナ結合キー
App Illuminate\Foundation\Application app
Artisan Illuminate\Contracts\Console\Kernel artisan
Auth Illuminate\Auth\AuthManager auth
Auth (Instance) Illuminate\Contracts\Auth\Guard auth.driver
Blade Illuminate\View\Compilers\BladeCompiler blade.compiler
Broadcast Illuminate\Contracts\Broadcasting\Factory
Broadcast (Instance) Illuminate\Contracts\Broadcasting\Broadcaster
Bus Illuminate\Contracts\Bus\Dispatcher
Cache Illuminate\Cache\CacheManager cache
Cache (Instance) Illuminate\Cache\Repository cache.store
Config Illuminate\Config\Repository config
Cookie Illuminate\Cookie\CookieJar cookie
Crypt Illuminate\Encryption\Encrypter encrypter
DB Illuminate\Database\DatabaseManager db
DB (Instance) Illuminate\Database\Connection db.connection
Event Illuminate\Events\Dispatcher events
File Illuminate\Filesystem\Filesystem files
Gate Illuminate\Contracts\Auth\Access\Gate
Hash Illuminate\Contracts\Hashing\Hasher hash
Lang Illuminate\Translation\Translator translator
Log Illuminate\Log\LogManager log
Mail Illuminate\Mail\Mailer mailer
Notification Illuminate\Notifications\ChannelManager
Password Illuminate\Auth\Passwords\PasswordBrokerManager auth.password
Password (Instance) Illuminate\Auth\Passwords\PasswordBroker auth.password.broker
Queue Illuminate\Queue\QueueManager queue
Queue (Instance) Illuminate\Contracts\Queue\Queue queue.connection
Queue (Base Class) Illuminate\Queue\Queue
Redirect Illuminate\Routing\Redirector redirect
Redis Illuminate\Redis\RedisManager redis
Redis (Instance) Illuminate\Redis\Connections\Connection redis.connection
Request Illuminate\Http\Request request
Response Illuminate\Contracts\Routing\ResponseFactory
Response (Instance) Illuminate\Http\Response
Route Illuminate\Routing\Router router
Schema Illuminate\Database\Schema\Builder
Session Illuminate\Session\SessionManager session
Session (Instance) Illuminate\Session\Store session.store
Storage Illuminate\Filesystem\FilesystemManager filesystem
Storage (Instance) Illuminate\Contracts\Filesystem\Filesystem filesystem.disk
URL Illuminate\Routing\UrlGenerator url
Validator Illuminate\Validation\Factory validator
Validator (Instance) Illuminate\Validation\Validator
View Illuminate\View\Factory view
View (Instance) Illuminate\View\View


Authファサード

▼ Authファサードとは

認証に関する処理を提供する。

Laravelからあらかじめ提供されている認証を使用しない場合、Authファサードを使用して、認証ロジックを実装できる。


DBファサード

▼ DBファサードとは

DBの操作処理を提供する。

Eloquentの代わりとして、DBファサードを使用しても良い。

Active Recordのロジックを持たないため、Repositoryパターンのロジックとして使用できる。

transactionメソッド

一連のトランザクション処理を実行する。

引数として渡した無名関数が例外を返却した場合、ロールバックを自動的に実行する。

例外が発生しなかった場合、無名関数の返却値が、そのままtransactionメソッドの返却値になる。

加えてtransactionメソッドの返却値を返却するようにすれば、無名関数の返却値をそのまま使用できる。

補足として、トランザクション処理は必須ではなく、使用するとアプリケーションがDBを操作するために要する時間が増えるため、使用しなくても良い。

参考リンクによると、MongoDBに対してトランザクション処理を実行する/行わない場合を比較して、処理時間が17%弱長くなったとのこと。

*実装例*

<?php

namespace App\Infrastructure\Repositories;

use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTO\FooDTO;
use Throwable;

class FooRepository extends Repository implements DomainFooRepository
{
    /**
     * @var FooDTO
     */
    private FooDTO $fooDTO;

    public function __construct(FooDTO $fooDTO)
    {
        $this->fooDTO = $fooDTO;
    }

    /**
     * @param Foo $foo
     * @throws Throwable
     */
    public function save(Foo $foo): void
    {
        // トランザクション処理を開始する。
        DB::beginTransaction();

        try {

            $this->fooDTO->fill([
                    "name"  => $foo->name(),
                    "age"   => $foo->age(),
                ])
                ->save();

            // コミットメントを実行する。
            DB::commit();
        } catch (Exception $e) {

            // ロールバックする。
            DB::rollback();
        }
    }
}

beginTransactionメソッド、commitメソッド、rollbackメソッド、

トランザクション処理の各操作を分割して実行する。

基本的には、transactionメソッドを使用してトランザクション処理を実行すれば良い。

*実装例*

<?php

namespace App\Infrastructure\Repositories;

use App\Domain\Foo\Entities\Foo;
use App\Domain\Foo\Repositories\FooRepository as DomainFooRepository;
use App\Infrastructure\Foo\DTO\FooDTO;

class FooRepository extends Repository implements DomainFooRepository
{
    /**
     * @var FooDTO
     */
    private FooDTO $fooDTO;

    public function __construct(FooDTO $fooDTO)
    {
        $this->fooDTO = $fooDTO;
    }

    /**
     * Fooを更新します。


     *
     * @param Foo $foo
     */
    public function save(Foo $foo)
    {
        // トランザクション処理を開始する。
        DB::beginTransaction();

        try {
            $this->fooDTO
            // オブジェクトにデータを設定する。
            ->fill([
                "name"  => $foo->name(),
                "age"   => $foo->age(),
                "email" => $foo->email()
            ])
            // update文を実行する。
            ->save();

            // コミットメントを実行する。
            DB::commit();
        } catch (\Exception $e) {

            // ロールバックする。
            DB::rollback();
        }
    }
}


Routeファサード

▼ Routeファサードとは

ルーティング処理を提供する。

▼ ヘルスチェックへの対応

ALBやGlobal Acceleratorから『/healthcheck』に対してヘルスチェックを設定した上で、200ステータスを含むレスポンスを返信する。

Nginxでヘルスチェックを実装もできるが、アプリケーションの死活管理としては、Laravelに実装する方が適切である。

RouteServiceProviderも参照せよ。

*実装例*

以下のような構成があるとする。

webサーバーではヘルスチェックエンドポイントを設けず、appサーバー側にヘルスエンドポイント (例:/healthcheck) を設ける。

⬇⬆︎︎
⬇⬆︎︎
webサーバー #
⬇⬆︎︎
⬇⬆︎︎
appサーバー # /healthcheckで、00を返却する
<?php

# ヘルスチェックを受信するパス
Route::get("/healthcheck", function () {
    return response("success", 200);
});

middlewareメソッド

コントローラーへのルーティング時に実行するMiddlewareクラスを設定する。

引数として、App\Http\Kernel.phpファイルで定義されたMiddlewareクラスのエイリアスを設定する。

*実装例*

認証方法としてWebガードを使用する場合、authエイリアスを設定する。

<?php

use App\Http\Controllers\Foo\FooController;

// authエイリアスを設定する。
Route::middleware("auth")->group(function () {
    Route::get("/foo", [FooController::class, "getFoo"]);
    Route::get("/foo/{fooId}", [FooController::class, "index"]);
    Route::post("/foo", [FooController::class, "createFoo"]);
    Route::put("/foo/{fooId}", [FooController::class, "updateFoo"]);
    Route::delete("/foo/{fooId}", [FooController::class, "deleteFoo"]);
});

デフォルトでは、App\Http\Kernel.phpファイルにて、authエイリアスに\App\Http\Middleware\Authenticateクラスが紐付けられている。

<?php

namespace App\Http;

use App\Http\Middleware\BeforeMiddleware\FooIdConverterMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

    ...

    protected $routeMiddleware = [

        "auth" => \App\Http\Middleware\Authenticate::class,

    ];

    ...

}

一方で、認証方法としてAPIガードを使用する場合、auth:apiエイリアスを設定する。

<?php

// authエイリアスのMiddlewareクラスが使用される。
Route::middleware("auth:api")->group(function () {
    // 何らのルーティング
});

prefixメソッド

エンドポイントが共通として持つ最初のパスを、接頭辞として定義する。

*実装例*

各エンドポイントの最初の『foos』を接頭辞として定義する。

<?php

use App\Http\Controllers\Foo\FooController;

Route::prefix("foos")->group(function () {
    Route::get("/", [FooController::class, "getFoo"]);
    Route::get("/{fooId}", [FooController::class, "index"]);
    Route::post("/", [FooController::class, "createFoo"]);
    Route::put("/{fooId}", [FooController::class, "updateFoo"]);
    Route::delete("/{fooId}", [FooController::class, "deleteFoo"]);
});

whereメソッド、patternメソッド

パスパラメーターに対するバリデーションルールを正規表現で定義し、加えて実行する。

RouteServiceProviderのbootメソッドにて、patternメソッドで制約を設定することによって、ルーティング時にwhereを使用する必要がなくなる。

*実装例*

userIdの形式を『0〜9が1つ以上』に設定している。

<?php

use App\Http\Controllers\Foo\FooController;

Route::prefix("foos")->group(function () {
    Route::get("/", [FooController::class, "getFoo"]);
    Route::get("/{fooId}", [FooController::class, "index"])
        // バリデーションルール
        ->where("fooId", "[0-9]+");
    Route::post("/", [FooController::class, "createFoo"]);
    Route::put("/{fooId}", [FooController::class, "updateFoo"])
        ->where("fooId", "[0-9]+");
    Route::delete("/{fooId}", [FooController::class, "deleteFoo"])
        ->where("fooId", "[0-9]+");
});

または、RouteServiceProviderクラスにpatternメソッドを定義すると、各エンドポイントに対する正規表現を一括で実行できる。

*実装例*

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * ルーティングの設定ファイルをコールします。


     *
     * @return void
     */
    public function boot()
    {
        // バリデーションルールとして『0〜9が1つ以上』を定義する。
        Route::pattern('fooId', '[0-9]+');

        // 〜 中略 〜
    }
}

groupメソッド

複数のグループを組み合わせる場合、groupメソッドを使用する。

*実装例*

エンドポイントの接頭辞とミドルウェアの指定を定義する。

<?php

Route::group(["prefix" => "foo" , "middleware" => "auth"], (function () {
    Route::get("/", [FooController::class, "getFoo"]);
    Route::get("/{fooId}", [FooController::class, "index"]);
    Route::post("/", [FooController::class, "createFoo"]);
    Route::put("/{fooId}", [FooController::class, "updateFoo"]);
    Route::delete("/{fooId}", [FooController::class, "deleteFoo"]);
});


Storageファサード

▼ Storageファサードとは

ファイルの入出力処理を提供する。

▼ ローカルストレージ (非公開) の場合

ファイルを/storage/appディレクトリ配下に保管する。

このファイルは非公開であり、リクエストによってアクセスできない。

事前に、シンボリックリンクを作成する、また、filesystems.phpファイルに設定が必要である。

$ php artisan storage:link
return [

    "default" => env("FILESYSTEM_DRIVER", "local"),

     ...

    "disks" => [

        "local" => [
            "driver" => "local",
            "root"   => storage_path("app"),
        ],

     ...

    // シンボリックリンクの関係を定義
    "links" => [

        // 『/var/www/project/public/storage』から『/var/www/project/storage/app/public』へのリンク
        public_path("storage") => storage_path("app/public"),
    ],
];

*実装例*

Storageファサードのdiskメソッドを使用してlocalディスクを指定する。

file.txtファイルをstorage/app/file.txtとして保管する。

Storage::disk("local")->put("file.txt", "file.txt");

ただし、filesytems.phpファイルでデフォルトディスクはlocalになっているため、putメソッドを直接的に使用できる。

Storage::put("file.txt", "file.txt");

▼ ローカルストレージ (公開) の場合

ファイルをstorage/app/publicディレクトリ配下に保管する。

このファイルは公開であり、リクエストできる。

事前に、filesystems.phpファイルに設定が必要である。

return [

    "default" => env("FILESYSTEM_DRIVER", "local"),

     ...

    "disks" => [

        ...

        "public" => [
            "driver"     => "local",
            "root"       => storage_path("app/public"),
            "url"        => env("APP_URL") . "/storage",
            "visibility" => "public",
        ],

        ...

    ],
];

*実装例*

Storageファサードのdiskメソッドを使用してpublicディスクを指定する。

また、file.txtファイルをstorage/app/public/file.txtとして保管する。

Storage::disk("s3")->put("file.txt", "file.txt");

ただし、環境変数を使用して、filesytems.phpファイルでデフォルトディスクをs3に変更すると、putメソッドを直接的に使用できる。

FILESYSTEM_DRIVER=s3
Storage::put("file.txt", "file.txt");

*実装例*

<?php

namespace App\Http\Controllers;

use Storage;

class FileSystemPublicController extends Controller
{
    /**
     * ファイルをpublicディスクに保管する
     */
    public function putContentsInPublicDisk()
    {
        // 保存先をpublicに設定する。
        $disk = Storage::disk("public");

        // 保存先のファイルを読み込む
        $file_path = "/path/to/public/foo.jpg"
        $contents = file_get_contents($file_path);

        // 保存先パス (ディレクトリ+ファイル名)
        $saved_file_path = "/images/foo.jpg";

        // foo.jpgを『/images/foo.jpg』に保存
        // ルートディレクトリは『/storage/app/public』
        $disk->put($saved_file_path, $contents);
    }
}

▼ クラウドストレージの場合

ファイルをS3バケット内のディレクトリ配下に保管する。

環境変数を.envファイルに実装する必要がある。

filesystems.phpファイルから、指定された設定が選択される。

AWSアカウントの認証情報を環境変数として設定するか、またはS3アクセスポリシーをEC2やECSタスクに付与することにより、S3にリクエストを送信できるようになる。

事前に、filesystems.phpファイルに設定が必要である。

# S3アクセスポリシーをEC2やECSタスクに付与しても良い
AWS_ACCESS_KEY_ID=<アクセスキーID>
AWS_SECRET_ACCESS_KEY=<シークレットアクセスキー>
AWS_DEFAULT_REGION=ap-northeast-1

# 必須
AWS_BUCKET=<バケット名>
return [

    "default" => env("FILESYSTEM_DRIVER", "local"),

     ...

    "disks" => [

        ...

        "s3" => [
            "driver"   => "s3",
            "key"      => env("AWS_ACCESS_KEY_ID"),
            "secret"   => env("AWS_SECRET_ACCESS_KEY"),
            "region"   => env("AWS_DEFAULT_REGION"),
            "bucket"   => env("AWS_BUCKET"),
            "url"      => env("AWS_URL"),
            "endpoint" => env("AWS_ENDPOINT"),
        ],
    ],
];

*実装例*

Storageファサードのdiskメソッドを使用してs3ディスクを指定する。

また、file.txtファイルをS3バケットのルートにfile.txtとして保管する。

Storage::disk("s3")->put("file.txt", "file.txt");

他の実装方法として、環境変数を使用して、filesytems.phpファイルでデフォルトディスクをs3に変更すると、putメソッドを直接的に使用できる。

FILESYSTEM_DRIVER=s3
Storage::put("file.txt", "file.txt");


Validatorファサード

▼ Validatorファサードとは

バリデーション処理を提供する。

FormRequestクラスのvalidatedメソッドやvalidateメソッドの代わりとして、Validatorファサードを使用しても良い。

▼ Validatorクラス、failsメソッド

Validateファサードのmakeメソッドを使用して、ルールを定義する。

この時、第一引数で、バリデーションを実行するリクエストデータを渡す。

ルールに反すると、1つ目のルール名 (例:required) に基づき、validation.phpファイルから対応するエラーメッセージを自動的に選択する。

次に、failsメソッドを使用して、バリデーションでエラーが発生した場合の処理を定義する。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class FooController extends Controller
{
    /**
     * 新しいブログポストの保存
     *
     * @param Request $request
     */
    public function update(Request $request)
    {
        // ルールの定義
        $validator = Validator::make(
            $request->all(), [
            "title" => "required|unique:posts|max:255",
            "body"  => "required",
        ]);

        // バリデーション時にエラーが発生した場合
        if ($validator->fails()) {
            // 指定したWebページにリダイレクト
            // validatorを渡すことにより、エラーメッセージをViewに渡せる。
            return redirect("error")->withErrors($validator)
                ->withInput();
        }

        // 続きの処理
    }
}

validateメソッド

Validatorクラスのvalidateメソッドを使用すると、FormRequestクラスのvalidateメソッドと同様の処理が実行される。

バリデーションでエラーが発生した場合、Handlerクラスのinvalidメソッドがコールされ、元のWebページにリダイレクトされる。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class FooController extends Controller
{
    /**
     * 新しいブログポストの保存
     *
     * @param Request $request
     */
    public function update(Request $request)
    {
        // 元のWebページにリダイレクトする場合は、validateメソッドを使用する。
        $validator = Validator::make(
            $request->all(),
            [
                "title" => "required|unique:posts|max:255",
                "body"  => "required",
            ])->validate();

        // バリデーション時にエラーが発生した場合
        if ($validator->fails()) {
            // 指定したWebページにリダイレクト
            // validatorを渡すことにより、エラーメッセージをViewに渡せる。
            return redirect("error")->withErrors($validator)
                ->withInput();
        }

        // 続きの処理
    }
}


08-02. よく使用するグローバルヘルパー関数

ヘルパー関数

▼ ヘルパー関数とは

グローバルにコールできるLaravel専用のメソッドのこと。

基本的には、ヘルパー関数で実行される処理は、Facadeの内部で実行されるものと同じである。

どちらを使用するかは好みである。

▼ 一覧

以下リンクを参考にせよ。


authヘルパー

▼ AuthManagerインスタンスの返却

認証処理を持つAuthManagerクラスのインスタンスを返却する。

<?php

// Illuminate\Auth\AuthManager
$auth = auth();


configヘルパー

▼ 環境変数ファイルの読み出し

環境変数ファイル名とキー名をドットで指定し、事前に設定された値を出力する。

*実装例*

デフォルトで搭載されているapp.phpファイルのtimezoneキーの値を出力する。

<?php

$value = config("app.timezone");

▼ 独自環境変数ファイルの作成と読み出し

任意の名前のphp形式ファイルをconfigディレクトリ配下に作成しておく。

これは、configヘルパーで読み込める。

*実装例*

<?php

$requestUrl = config("api.foo.endpoint_url");
<?php

return [
    "foo" => [
        "endpoint_url" => env("ENDPOINT_URL", ""),
        "api_key"      => env("API_KEY"),
    ],
    "bar" => [
        "endpoint_url" => env("ENDPOINT_URL", ""),
        "api_key"      => env("API_KEY"),
    ]
];


bcryptヘルパー

<?php

$hash = bcrypt('foo'); // 『foo』をハッシュ化して、『$2y$10$ZkYG.whhdcogCCzbG.VlQ』としてDBで管理する。


redirectヘルパー


responseヘルパー

▼ JSON型データを含むレスポンス

返却されるResponseFactoryクラスのjsonメソッドにレンダリングしたいJSON型データを設定する。

responseヘルパーは初期値として200ステータスが設定されているが、viewメソッドやsetStatusCodeメソッドを使用して、明示的に設定しても良い。

*実装例*

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Response;

class FooController extends Controller
{
    public function index()
    {

        ...

        return response()->json([
            "name"  => "Abigail",
            "state" => "CA"
        ], 200);
    }
}

▼ Viewテンプレートのレスポンス

返却されるResponseFactoryクラスのviewメソッドに、レンダリングしたいデータ (テンプレート、array型データ、ステータスコードなど) を設定する。

また、ViewクラスのheaderメソッドにHTTPヘッダー値を設定する。

responseヘルパーは初期値として200ステータスが設定されているが、viewメソッドやsetStatusCodeメソッドを使用して、明示的に設定しても良い。

*実装例*

<?php

namespace App\Http\Controllers\Foo;

use App\Http\Controllers\Controller;
use Illuminate\Http\Response;

class FooController extends Controller
{
    public function index()
    {
        ...

        // データ、ステータスコード、ヘッダーなどを設定する場合
        return response()->view(
            "foo",
            $data,
            200
        )->header(
            "Content-Type",
            $type
        );
    }
}
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Response;

class FooController extends Controller
{
    public function index()
    {
        ...

        // ステータスコードのみ設定する場合
        return response()->view("foo")
            ->setStatusCode(200);
    }
}


routeヘルパー

▼ ルートエイリアスを基にURL作成

ルートにエイリアスがついている場合、エイリアスに応じてURLを作成する。

ドメインは自動的に補完される。

<?php

Route::get('/foo', [FooController::class, 'index'])->name('foos_index');

// https://example.com/foo
$url = route('foos_index');


path系ヘルパー

base_pathヘルパー

引数を設定しない場合、projectルートディレクトリの絶対パスを作成する。

また、projectルートディレクトリからの相対パスを引数として、絶対パスを作成する。

<?php

// /var/www/project
$path = base_path();

// /var/www/project/vendor/bin
$path = base_path("vendor/bin");

public_pathヘルパー

引数を設定しない場合、publicディレクトリの絶対パスを作成する。

また、publicディレクトリからの相対パスを引数として、絶対パスを作成する。

<?php

// /var/www/project/public
$path = public_path();

// /var/www/project/public/css/app.css
$path = public_path("css/app.css");

storage_pathヘルパー

引数を設定しない場合、storageディレクトリの絶対パスを作成する。

まあ、storageディレクトリからの相対パスを引数として、絶対パスを作成する。

<?php

// /var/www/project/storage
$path = storage_path();

// /var/www/project/storage/app/file.txt
$path = storage_path("app/file.txt");


urlヘルパー

▼ パスを基にURL作成

指定したパスに応じてURLを作成する。

ドメインは自動的に補完される。

<?php

// https://example.com/foo
$url = url('/foo');


09. Factory

初期値レコードの定義

▼ Fakerによるランダム値作成

Fakerは、レコードの値をランダムに作成する。

Farkerクラスは、プロパティにランダムなデータを保持している。

このプロパティを特に、Formattersという。

▼ Factoryによるレコード定義

*実装例*

<?php

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Foo;
use Illuminate\Database\Eloquent\Factories\Factory;

class FooFactory extends Factory
{
    /**
     * @var string
     */
    protected $model = Foo::class;

    /**
     * @return array
     */
    public function definition()
    {
        return [
            'name'          => $this->faker->name,
            'email_address' => $this->faker->unique()->safeEmail,
            'password'      => 'password',
        ];
    }
}

▼ HasFactoryトレイト

Factoryに対応するEloquentモデルで使用する必要がある。

class Foo
{
    use HasFactory;
}


初期ダミーデータの量産

▼ Seederによるダミーデータ量産

Factoryにおける定義を基にして、指定した数だけダミーデータを量産する。

*実装例*

FooSeederを定義し、50個のダミーユーザーデータを量産する。

<?php

use App\Models\Foo;
use Illuminate\Database\Seeder;

class FooSeeder extends Seeder
{
    private const NUM_TEST_DATA = 3;

    /**
     * @return void
     */
    public function run()
    {
        factory()->count(self::NUM_TEST_DATA)->create();
    }
}

また、BarSeederを定義し、50個のダミーユーザーデータを量産する。

<?php

use App\Models\Bar;
use Illuminate\Database\Seeder;

class BarSeeder extends Seeder
{
    private const NUM_TEST_DATA = 3;

    /**
     * @return void
     */
    public function run()
    {
        factory()->count(self::NUM_TEST_DATA)->create();
    }
}

DatabaseSeederにて、全てのSeederをまとめて実行する。

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * @return void
     */
    public function run()
    {
        // 開発環境用の初期データ
        if (app()->environment("local")) {
            $this->call([
                // ダミーデータ
                FooSeeder::class,
                BarSeeder::class
            ]);
        }

        // テスト環境用の初期データ
        if (app()->environment("tes")) {
            $this->call([
                // リアルデータ
            ]);
        }

        // ステージング環境用の初期データ
        if (app()->environment("stg")) {
            $this->call([
                // リアルデータ
            ]);
        }

        // 本番環境用の初期データ
        if (app()->environment("prd")) {
            $this->call([
                // リアルデータ
            ]);
        }
    }
}


10. HTTP|Controller


10-02. HTTP|FormRequest

クエリパラメーター/メッセージボディのバリデーション

▼ ルール定義 & バリデーション手動実行

同じくFormRequestクラスのvalidateメソッドを使用して、ルールを定義し、加えてバリデーションを実行する。

validatedメソッドと間違わないように注意する。

ルールに反すると、1つ目のルール名 (例:required) に基づき、validation.phpファイルから対応するエラーメッセージを自動的に選択する。

バリデーションでエラーが発生した場合、Handlerクラスのinvalidメソッドがコールされ、元のWebページにリダイレクトされる。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     */
    public function index(Request $request)
    {
        // クエリパラメーターのバリデーションを実行する。
        // エラーが発生した場合は元のWebページにリダイレクト
        $validated = $request->validate([
            "limit" => ["required", Rule::in([25, 50, 100])],
            "order" => ["required", Rule::in(["ascend", "descend"])],
        ]);

        // 続きの処理
    }

    /**
     * @param Request $request
     */
    public function update(Request $request)
    {
        // ルールの定義、バリデーションの実行
        // エラーが発生した場合は元のWebページにリダイレクト
        $validated = $request->validate([
            "title" => ["required", "string", "max:255"],
            "body"  => ["required", "string", "max:255"],
            "date"  => ["required", "date"],
        ]);

        // 続きの処理
    }
}

注意点として、ルールによっては、配列を使用せずとも定義できる。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     */
    public function update(Request $request)
    {
        // ルールの定義、メッセージボディのバリデーションを実行する。
        // エラーが発生した場合は元のWebページにリダイレクト
        $validated = $request->validate([
            "title" => "required|string|max:5255",
            "body"  => "required|string|max:255",
            "date"  => "required|date",
        ]);

        // 続きの処理
    }
}

▼ ルール定義 & バリデーション自動実行

Controllerで、FormRequestクラスを引数に指定すると、コントローラーのメソッドをコールする前にバリデーションを自動的に実行する。

そのため、コントローラーの中ではバリデーションを実行する必要はない。

代わりとして、ルールをFormRequestクラスのruleメソッドに定義する必要がある。

FormRequestクラスのvalidatedメソッドを使用して、バリデーション済みのデータを取得できる。

バリデーションでエラーが発生した場合、Handlerクラスのinvalidメソッドがコールされ、元のWebページにリダイレクトされる。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     */
    public function index(Request $request)
    {
        // クエリパラメーターのバリデーションを実行する。
        // エラーが発生した場合は元のWebページにリダイレクト
        $validated = $request->validated();

        // 続きの処理
    }

    /**
     * @param Request $request
     */
    public function update(Request $request)
    {
        // メッセージボディのバリデーションを実行する。
        // エラーが発生した場合は元のWebページにリダイレクト
        $validated = $request->validated();

        // 続きの処理
    }
}

FormRequestクラスのrulesメソッドを使用して、ルールを定義する。

ルールに反すると、1つ目のルール名 (例:required) に基づき、validation.phpファイルから対応するエラーメッセージを自動的に選択する。

*実装例*

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FooRequest extends FormRequest
{
    /**
     * ルールを返却します。


     *
     * @return array
     */
    public function rules()
    {
        // ルールの定義
        return [
            "title"  => ["required", "string", "max:255"],
            "body"   => ["required", "string", "max:255"],
            "type"   => ["required", Rule::in([1, 2, 3])],
            "author" => ["required", "string", new UppercaseRule()],
            "date"   => ["required", "date"],
        ];
    }
}


パスパラメーターのバリデーション

▼ ルールの定義 & バリデーション自動実行

Routeファサードのpatternメソッドまたはwhereメソッドで定義する。


エラーメッセージ

▼ 標準のエラーメッセージ

標準のバリデーションメッセージは、resources/lang/ja/validation.phpファイルで定義できる。

バリデーションルールの組み合わせによって、validation.phpファイルから自動的にメッセージが選択される。

例えばルールとして最大値を設定した場合は、データ型に合わせてメッセージが選択される。

日本語翻訳validation.phpファイルについては、以下のリンクを参考にせよ。

<?php

return [

    // 〜 中略 〜

    'required' => ':attributeは必須です',

    'string' => ':attribute は文字列のみ有効です',

    'max' => [
        'numeric' => ':attributeには、:max以下の数字を指定してください',
        'file'    => ':attributeには、:max kB以下のファイルを指定してください',
        'string'  => ':attributeは、:max文字以下で指定してください',
        'array'   => ':attributeは:max個以下指定してください',
    ],

    'date' => ':attribute を有効な日付形式にしてください',

    'attributes' => [
        'title' => 'タイトル',
        'body'  => '本文',
        'date'  => '作成日',
    ],

    // 〜 中略 〜

];

注意点として、言語設定を行わない場合、デフォルトでは/resources/lang/en/validation.phpファイルをバリデーションメッセージとして参照するため、app.phpファイルで言語を変更することと、日本語翻訳validation.phpファイルが必要である。

<?php

return [

    // 〜 中略 〜

    'locale' => 'ja'

    // 〜 中略 〜

];

▼ 画面上でのエラーメッセージ出力

バリデーションでエラーがあった場合、Handlerクラスのinvalidメソッドがコールされ、MessageBagクラスがViewに渡される。

選択されたバリデーションメッセージが配列型でMessageBagクラスに格納されている。

(
[title] => Array
(
[0] => タイトルの入力は必須です
[1] => タイトルは、最大255文字以下で指定してください
)

[body] => Array
(
[0] => 本文の入力は必須です
[1] => 本文は、最大255文字以下で指定してください
)
[data] => Array
(
[0] => 作成日の入力は必須です
[1] => 作成日を有効な日付形式にしてください
)
)


Rule

existsメソッド

指定されたテーブルのカラムに値が存在しているかを検証する。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FooRequest extends FormRequest
{
    /**
     * ルールを返却します。


     *
     * @return array
     */
    public function rules()
    {
        // ルールの定義
        return [
            "prefectureId" => ["nullable", "integer", Rule::exists("prefectures", "id")],
            "cityId"       => ["nullable", "integer", Rule::exists("cities", "id")]
        ];
    }
}

テーブルにカラム数が多い場合は、Where句をつけることにより、特定のカラムのみ検証もできる。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FooRequest extends FormRequest
{
    /**
     * ルールを返却します。


     *
     * @return array
     */
    public function rules()
    {
        // ルールの定義
        return [
            "prefectureId" => ["nullable", "integer", Rule::exists("prefectures", "id")],
            "cityId"       => ["nullable", "integer", Rule::exists("cities", "id")->whereNull("deleted_at")],
        ];
    }
}

inメソッド

決められた複数の値に合致する値であるか否かを検証する。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FooRequest extends FormRequest
{
    /**
     * ルールを返却します。


     *
     * @return array
     */
    public function rules()
    {
        // ルールの定義
        return [
            "type"  => ["required", Rule::in([1, 2, 3])],
        ];
    }
}

▼ 自前ルール/メッセージ

自前ルールを定義する場合は、Ruleクラスを継承したクラスを用意し、ruleメソッドの中でインスタンスを作成する。

自前Ruleクラスでは、passesメソッドでルールを定義する。

また、messagesメソッドでバリデーションメッセージを定義する。

validation.phpファイルでメッセージを定義し、これを参照しても良い。

*実装例*

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class UppercaseRule implements Rule
{
    /**
     * バリデーションの成功を判定
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return strtoupper($value) === $value;
    }

    /**
     * バリデーションエラーメッセージの取得
     *
     * @return string
     */
    public function message()
    {
        return 'The :attribute must be uppercase.';
        // return trans('validation.uppercase'); validation.phpファイルから参照する。
    }
}
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FooRequest extends FormRequest
{
    /**
     * ルールを返却します。


     *
     * @return array
     */
    public function rules()
    {
        // ルールの定義
        return [
            "author"  => ["required", "string", new UppercaseRule()]
        ];
    }
}


セッション

▼ セッション変数の取得

FormRequestクラスのsessionメソッドを使用して、セッション変数を取得する。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param  Request  $request
     * @param  int  $id
     */
    public function show(Request $request, int $id)
    {
        $session = $request->session()
            ->get("key");
    }
}

全てのセッション変数を取得もできる。

$session = $request->session()->all();

▼ フラッシュデータの設定

現在のセッションで、今回と次回のリクエストのみで有効な一時データを設定できる。

$request->session()
    ->flash("status", "Task was successful!");


認証

authorizeメソッド

ユーザーがリソースに対してCRUDを実行する権限を持っているかを、コントローラーのメソッドを実行する前に、判定する。

*実装例*

/**
 * ユーザーがこのリクエストの権限を持っているかを判断する
 *
 * @return bool
 */
public function authorize()
{
    $comment = Comment::find($this->route("comment"));

    return $comment&& $this->user()->can("update", $comment);
}

▼ Authファサード

ノート内の『Authファサード』の項目を参照せよ。


10-03. HTTP|Middleware

Middlewareの仕組み

▼ Middlewareの種類

ルーティング後にコントローラーメソッドの前にコールされるBeforeMiddleと、レスポンスの実行時にコールされるAfterMiddlewareがある

Laravelのミドルウェア

▼ BeforeMiddleware

ルーティング時のコントローラーメソッドのコール前に実行する処理を設定できる。

一連の処理を終えた後、FormRequestクラスを、次のMiddlewareクラスやControllerクラスに渡す必要がある。

これらのクラスはクロージャー (無名関数) として、next変数に格納されている。

*実装例*

<?php

namespace App\Http\Middleware;

use Closure;

class FooBeforeMiddleware
{
    /**
     * @param  Request  $request
     * @param  \Closure  $next
     */
    public function handle($request, Closure $next)
    {
        // 何らかの処理

        // 次のMiddlewareクラスやControllerクラスに、FormRequestクラスを渡す。
        return $next($request);
    }
}

▼ AfterMiddleware

コントローラーメソッドのレスポンスの実行後 (テンプレートのレンダリングを含む) に実行する処理を設定できる。

あらかじめ、FormRequestクラスを、前のMiddlewareクラスやControllerクラスから受け取る必要がある。

これらのクラスはクロージャー (無名関数) として、next変数に格納されている。

*実装例*

<?php

namespace App\Http\Middleware;

use Closure;

class FooAfterMiddleware
{
    /**
     * @param  Request  $request
     * @param  \Closure  $next
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // 何らかの処理

        // 前のMiddlewareクラスやControllerクラスから、FormRequestクラスを受け取る。
        return $response;
    }
}


標準のMiddleware

▼ EncryptCookies

レスポンス時に、Cookieヘッダーの全ての値を暗号化する。

暗号化したくない場合は、Cookieヘッダーのキー名をexceptプロパティに設定する。

▼ StartSession

セッションの開始の開始点になる。

また、同一セッションで一意なCSRFトークンを作成する。

CSRFトークンによるCSRFの防御については、以下のリンクを参考にせよ。

▼ VerifyCsrfToken

セッションデータに書かれたCSRFトークンと、リクエストボディに割り当てられたトークンを比較する。

セッションデータはstorage/framework/sessionsディレクトリ配下に配置されている。

一般的に、CSRFトークンはCookieヘッダーに割り当てることもできるが、Laravelではリクエストボディを使用する必要がある。


コール方法のカスタマイズ

▼ Kernel

Middlewareクラスをコールする時の方法をカスタマイズできる。

*実装例*

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * 全てのHTTPリクエストに適用するミドルウェアを定義します。


     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\Auth\TrustProxies::class,
        \App\Http\Middleware\Auth\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\Auth\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * エイリアスとミドルウェアグループを定義します。


     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
        ],

        'api' => [
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    /**
     * エイリアスと個別のミドルウェアを定義します。


     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth'                 => \App\Http\Middleware\Auth\Authenticate::class,
        'auth.basic'           => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings'             => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers'        => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can'                  => \Illuminate\Auth\Middleware\Authorize::class,
        'guest'                => \App\Http\Middleware\Auth\RedirectIfAuthenticated::class,
        'password.confirm'     => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed'               => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle'             => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified'             => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,

        // Fooミドルウェアクラス
        'foo' => \App\Http\Middleware\Before\FooMiddleware::class
    ];

    /**
     * ミドルウェアをコールする順番を定義します。


     *
     * @var string[]
     */
    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\Auth\Authenticate::class,
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
}


10-04. HTTP|Request

リクエストパラメーターの取得

▼ クエリパラメーター/メッセージボディ

クエリパラメーターとメッセージボディの両方を取得する。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     */
    public function update(Request $request)
    {
        $params = $request->all(); // 全てのパラメーターを連想配列で取得する。

        $foo = $request->input('foo'); // 指定したパラメーターの値を取得する。

        $qux = $request->input('foo.qux'); // ネストされたパラメーターの値を取得する。

        $params = $request->only(['foo', 'bar']); // 指定したパラメーターを連想配列で取得する。

        $params = $request->except(['baz']); // 指定したパラメータ以外を連想配列で取得する。

        $foo = $request->foo; // 指定したパラメーターの値を取得する。

        $foo = request('foo'); // 指定したパラメーターの値を取得する。
    }
}

▼ クエリパラメーター

クエリパラメーターを取得する。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     */
    public function index(Request $request)
    {
        $params = $request->query(); // 全てのパラメーターを連想配列で取得する。

        $foo = $request->query('foo'); // 指定したパラメーターの値を取得する。
    }
}

▼ パスパラメータ

パスパラメーターを取得する。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     */
    public function update(Request $request)
    {
        $params = $request->route(); // 全てのパラメーターを連想配列で取得する。

        $fooId = $request->route('fooId'); // 指定したパラメーターの値を取得する。

        $fooId = $request->route->parameter('fooId'); // 指定したパラメーターの値を取得する。
    }
}

代わりとして、コントローラーの第二引数にパスパラメーター名を記述することにより、パスパラメーターの値を取得しても良い。

*実装例*

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param Request $request
     * @param         $fooId
     */
    public function update(Request $request, $fooId)
    {

    }
}


バリデーション

Requestではなく、FormRequestを使用した方がバリデーションがおすすめである。

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    public function update(Request $request)
    {
        // リクエストのバリデーション
        $validated = $request->validate([
            'name'  => 'required',
            'email' => 'required|email',
        ]);

        // 〜 中略 〜
    }
}


11. Logging

ログの出力先

▼ 設定方法

環境変数を.envファイルに実装する。

logging.phpファイルから、指定された設定が選択される。

LOG_CHANNEL=<オプション名>

注意点として、storageディレクトリ配下にログファイルを作成するようなログチャンネルを設定した場合、phpがこのディレクトリへの認可スコープを持たない。

そのため、リクエストできるようにする必要がある。

権限を変更したファイルは差分としてGitに認識されるため、これを共有すればチーム内で権限変更を共有できる。

# Failed to open stream: Permission denied
$ chmod -R 777 /var/www/foo/storage

▼ PHP-FPMのログについて

LaravelとPHP-FPMのプロセスはそれぞれ独立しているため、Laravelのログの出力先を変更しても、PHP-FPMのログの出力先は変更されない。

stackキー

他の単一/複数のチャンネルを利用するチャンネル。

<?php

return [

    ...

    "default"  => env("LOG_CHANNEL", "stack"),
    "channels" => [
        "stack" => [
            "driver"            => "stack",
            // 複数チャンネルを設定できる。
            // (例) ["single", "stack"]
            "channels"          => ["single"],
            "ignore_exceptions" => false,
        ],

        ...

    ]
];

singleキー

全てのログを/storage/logs/laravel.logファイルに対して出力する。

<?php

return [

    ...

    "default"  => env("LOG_CHANNEL", "stack"),
    "channels" => [
        "daily" => [
            "driver" => "daily",
            "path"   => storage_path("logs/laravel.log"),
            "level"  => env("LOG_LEVEL", "debug"),
            "days"   => 14,
        ],

        ...

    ]
];

dailyキー

全てのログを/storage/logs/laravel-<日付>.logファイルに対して出力する。

return [

    ...

    "default"  => env("LOG_CHANNEL", "stack"),
    "channels" => [
        "stderr" => [
            "driver"    => "monolog",
            "handler"   => StreamHandler::class,
            "formatter" => env("LOG_STDERR_FORMATTER"),
            "with"      => [
                "stream" => "php://stderr",
            ],
        ],

        ...

    ]
];

stderrキー

全てのログを標準エラー出力に対して出力する。

Docker上でLaravelを稼働させる場合は、作成されるログファイルでコンテナのサイズが肥大化することを防ぐために、これを選択する。

注意点として、自前カスタマイズとして、streamキーをstdout変更すれば、標準出力にログを出力もできる。

return [

    ...

    "default"  => env("LOG_CHANNEL", "stack"),
    "channels" => [
        "stderr" => [
            "driver"    => "monolog",
            "handler"   => StreamHandler::class,
            "formatter" => env("LOG_STDERR_FORMATTER"),
            "with"      => [
                "stream" => "php://stderr",
            ],
        ],

        ...

    ]
];


ログの出力

errorメソッド

エラーメッセージを定義する時、sprintfメソッドを使用すると便利である。

*実装例*

外部のAPIに対してリクエストを送信し、データを取得する。

取得したJSON型データを、クライアントにレスポンスする。

この時、リクエスト処理のために、Guzzleパッケージを使用している。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Response;

class FooController extends Controller
{
    public function index()
    {
        $client = new Client();
        $requestUrl = config("api.foo.endpoint_url");

        try {

            $response = $client->request(
                "GET",
                $requestUrl,
                [
                    "headers" => [
                        "Content-Type" => "application/json",
                        "X-API-Key"    => "api.foo.api_key",
                    ]
                ]
            );

            // JSONをクライアントにレスポンス
            return $response->getBody()
                ->getContents();

        } catch (GuzzleException $e) {

            Log::error(sprintf(
                    "%s : %s at %s line %s",
                    get_class($e),
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine())
            );

            return response()->json(
                [],
                $e->getCode()
            );
        }
    }
}
<?php

return [
    "foo" => [
        "endpoint_url" => env("ENDPOINT_URL", ""),
        "api_key"      => env("API_KEY"),
    ],
    "bar" => [
        "endpoint_url" => env("ENDPOINT_URL", ""),
        "api_key"      => env("API_KEY"),
    ]
];

infoメソッド


12. Migration

テーブルの作成/削除

upメソッド、downメソッド

コマンドによるDBマイグレーション時にコールされる。

upメソッドでテーブル、カラム、インデックスのCREATEを実行する。

downメソッドでupメソッドの結果をロールバックする。

*実装例*

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateFooTable extends Migration
{
    /**
     * @return void
     */
    public function up()
    {
        Schema::create("foos", function (Blueprint $table) {

            $table->bigIncrements("foo_id")->comment("ID");
            $table->string("name")->comment("名前");

            // MigrationMacroServiceProviderのメソッドを使用する。
            $table->systemColumns();

            // deleted_atカラムを追加する。
            $table->softDeletes();
        });
    }

    /**
     * @return void
     */
    public function down()
    {
        Schema::drop("foos");
    }
}


カラムの追加/変更/削除

▼ なし

指定したカラムを追加する。

*実装例*

カラムを追加するためだけにDBマイグレーションファイルを作成する。

$ php artisan make:migration add_column --table=foos

追加したいカラムのみを定義する。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddColumn extends Migration
{
    /**
     * @return void
     */
    public function up()
    {
        Schema::table('foos', function (Blueprint $table) {
            $table->string('foo_name');
        });
    }
}

DBマイグレーションを実行すると、指定したテーブルのカラムが追加される。

実行後は、作成したDBマイグレーションファイルを削除する。

$ php artisan migrate

renameColumnメソッド

指定したカラムの名前を変更する。

*実装例*

カラム名を変更するためだけにDBマイグレーションファイルを作成する。

$ php artisan make:migration rename_column --table=foos

テーブルのカラム名を定義し、renameColumnメソッドをコールする。

変更後でも、ロールバックできるように、downメソッドも定義しておく。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class RenameColumn extends Migration
{
    /**
     * @return void
     */
    public function up()
    {
        Schema::table('foos', function (Blueprint $table) {
            $table->renameColumn('id', 'foo_id');
        });
    }

    /**
     * @return void
     */
    public function down()
    {
        // データ型の変更後でも、ロールバックできるようにしておく。
        Schema::table('foos', function (Blueprint $table) {
            $table->renameColumn('foo_id', 'foo_id');
        });
    }
}

DBマイグレーションを実行すると、指定したテーブルのカラム名が変更される。

実行後は、作成したDBマイグレーションファイルを削除する。

$ php artisan migrate

changeメソッド

指定したカラムのデータ型を変更する。

*実装例*

データ型を変更するためだけにDBマイグレーションファイルを作成する。

$ php artisan make:migration change_column_data_type --table=foos

テーブルのカラムのデータ型を定義し、changeメソッドをコールする。

変更後でも、ロールバックできるように、downメソッドも定義しておく。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class ChangeColumnDataType extends Migration
{
    /**
     * @return void
     */
    public function up()
    {
        Schema::table('foos', function (Blueprint $table) {
            $table->integer('bar')->change();
        });
    }

    /**
     * @return void
     */
    public function down()
    {
        // データ型の変更後でも、ロールバックできるようにしておく。
        Schema::table('foos', function (Blueprint $table) {
            $table->string('bar')->change();
        });
    }
}

DBマイグレーションを実行すると、指定したテーブルのカラムのデータ型が変更される。

実行後は、作成したDBマイグレーションファイルを削除する。

$ php artisan migrate

dropColumnメソッド

指定したカラムを削除する。

*実装例*

カラムを削除するためだけにDBマイグレーションファイルを作成する。

$ php artisan make:migration drop_column --table=foos

削除するカラムをdropColumnメソッドで指定する。

変更後でも、ロールバックできるように、downメソッドも定義しておく。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class DropColumn extends Migration
{
    /**
     * @return void
     */
    public function up()
    {
        Schema::table('foos', function (Blueprint $table) {
            $table->dropColumn('foo_name');
        });
    }

    /**
     * @return void
     */
    public function down()
    {
        Schema::table('foos', function (Blueprint $table) {
            $table->string('foo_name');
        });
    }
}

DBマイグレーションを実行すると、指定したテーブルのカラムが追加される。

実行後は、作成したDBマイグレーションファイルを削除する。

$ php artisan migrate


よく使用するカラムタイプ

bigIncrementsメソッド

自動増分ありのinteger型カラムを作成する。

プライマリーキーとするIDカラムのために使用する。

自動増分のカラムは1個のテーブルに1つしか定義できず、他のIDカラムはunsignedBigIntegerメソッドを使用して定義する。

*実装例*

Schema::create("foos", function (Blueprint $table) {

    ...

    $table->bigIncrements("foo_id");

    ...

});

unsignedBigIntegerメソッド

自動増分なしのinteger型カラムを作成する。

プライマリーキーではないIDカラムのために使用する。

Schema::create("foos", function (Blueprint $table) {

    ...

    $table->bigIncrements("foo_id");
    $table->unsignedBigInteger("bar_id");

    ...

});

stringメソッド

VARCHAR型カラムを作成する。

*実装例*

Schema::create("foos", function (Blueprint $table) {

    ...

    $table->string("name");

    ...

});

timestampメソッド

TIMESTAMP型カラムを作成する。

*実装例*

Schema::create("foos", function (Blueprint $table) {

    ...

    $table->timestamp("created_at");

    ...
});


13. Notification

artisanコマンド



通知内容

▼ Notification

通知内容を定義する。

viaメソッドで受信チャンネルを定義する。

この時、Laravelがデフォルトで用意しているチャンネル (Mail、SMS、Slackチャンネル、Databaseチャンネル) 以外に送信したい場合、Channelクラスを定義する必要がある。

複数の値を設定した場合は、それぞれに通信が送信される。

toMailメソッド、toSmsメソッド、toSlackメソッド、toArrayメソッド、を使用して、Laravelの標準のチャンネルに渡す通知内容を定義できる。

*実装例*

<?php

namespace App\Notifications;

use App\Models\User;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\AwsSnsChannel;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class TfaTokenNotification extends Notification
{
    /**
     * @param $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        // 受信チャンネルを選択します。
    }

    /**
     * @param $notifiable
     * @return string
     */
    public function toSms($notifiable)
    {
        // SMSのメッセージ内容を返却します。
    }

    /**
     * @param $notifiable
     * @return MailMessage
     */
    public function toMail($notifiable)
    {
        // Emailのメッセージ内容を返却します。
    }

    /**
     * @param $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        // DBへの保存方法を返却します。
    }
}

▼ Eメール通知内容の定義

MailMessageクラスのメソッドを使用して、Eメール通知の内容を作成する。

markdownメソッドを使用することにより、マークダウン形式で定義できる。

<?php

namespace App\Notifications;

use App\Models\User;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\AwsSnsChannel;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class TfaTokenNotification extends Notification
{
    /**
     * @param $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return [
            $notifiable->prefers_sms ? [AwsSnsChannel::class] : [EmailChannel::class], // SMSでない場合は、Eメール通知とします。
            'database'
        ];
    }

    /**
     * @param $notifiable
     * @return MailMessage
     */
    public function toMail($notifiable)
    {
        // Emailのメッセージ内容を返却します。
        return (new MailMessage())->subject("コードを送信いたしました。")
            ->markdown("template.mail", [
                "tfa_token" => $notifiable->tfaToken()
            ]);
    }
}
@component("mail::message") 認証コード『{ $tfa_token }}』を入力して下さい。<br />

+++++++++++++++++++++++++++++++++++++<br />
本アドレスは送信専用です。 ご返信頂いてもお答えできませんので、ご了承ください。
@endcomponent

▼ SMS通知内容の定義

<?php

namespace App\Notifications;

use App\Models\User;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\AwsSnsChannel;
use Illuminate\Notifications\Notification;

class TfaTokenNotification extends Notification
{
    /**
     * @param $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return [
            $notifiable->prefers_sms ? [AwsSnsChannel::class] : [EmailChannel::class], // SMSの場合は、AWS-SNSを使用します。
            'database'
        ];
    }

    /**
     * @param $notifiable
     * @return string
     */
    public function toSms($notifiable)
    {
        // SMSのメッセージ内容を返却します。
        return view("template.sms", [
            "subject"   => "コードを送信いたしました。",
            "tfa_token" => $notifiable->tfaToken()
        ]);
    }
}

▼ Slack通知内容の定義

▼ DB通知内容の定義

配列でDBに保管する内容を定義する。

<?php

namespace App\Notifications;

use App\Models\User;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\AwsSnsChannel;
use Illuminate\Notifications\Notification;

class TfaTokenNotification extends Notification
{
    /**
     * @param $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return [
            $notifiable->prefers_sms ? [AwsSnsChannel::class] : [EmailChannel::class],
            'database' // DB受信チャンネル
        ];
    }

    /**
     * @param $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        // notificationsテーブルのdataカラムに、JSONで保存されます。
        return [
            "tfa_token" => $notifiable->tfaToken(),
        ];
    }
}


受信チャンネル (通知方法)

▼ Channel

Laravelがデフォルトで用意しているチャンネル以外に送信したい場合、ユーザー定義の受信チャンネルを定義する。

これは、Notificationクラスのviaメソッドで使用される。

*実装例*

AWS SNSを受信チャンネルとする。

AWSから配布されているパッケージが必要である。

$ composer require aws/aws-sdk-php-laravel
<?php

namespace App\Notifications\Channels;

use Aws\Sns\SnsClient;
use Aws\Exception\AwsException;
use Illuminate\Notifications\Notification;

class AwsSnsChannel
{
    /**
     * @param SnsClient $awsSnsClient
     */
    public function __construct(SnsClient $awsSnsClient)
    {
        $this->awsSnsClient = $awsSnsClient;
    }

    /**
     * @param              $notifiable
     * @param Notification $notification
     */
    public function send($notifiable, Notification $notification)
    {
        try {
            $message = $notification->toSms($notifiable);

            // AWS SNSにメッセージを送信します。
            $this->awsSnsClient->publish([
                "Message"     => $message,
                "PhoneNumber" => $this->toE164nizeInJapan(
                    $notifiable->phoneNumber()
                ),
            ]);
        } catch (AwsException $e) {

            Log::error(sprintf(
                    "%s : %s at %s line %s",
                    get_class($e),
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine())
            );

            throw new AwsException($e->getMessage());
        }
    }

    /**
     * @param string
     * @return string
     */
    private function toE164nizeInJapan(string $phoneNumeber): string
    {
        // E.164形式の日本電話番号を返却します。
        return "+81" . substr($phoneNumeber, 1);
    }
}


通知対象モデル

▼ Notifiableトレイトのnotifyメソッド

通知対象となるモデルを定義する。

Notifiableトレイトを継承する。

これにより、notifyメソッドを使用できるようになる。

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}

通知先のクラスからnotifyメソッドをコールし、任意のNotificationクラスを渡す。

これにより、通知処理が実行される。

<?php

$user->notify(new FooNotification());

▼ Notificationファサード

通知対象となるモデルを定義する。

Notifiableトレイトを継承する。

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}

Notificationファサードに通知先のモデルと通知クラスを渡す。

<?php

Notification::send($users, new FooNotification());

▼ オンデマンド通知

オンデマンド通知を使用すると、通知対象となるモデルがNotificableトレイトに依存せずに通知を実行できる。

<?php

Notification::route('mail', $user->email_address)
            ->route('nexmo', $user->phone_number)
            ->route('slack', $slackMessage->usl)
            ->notify(new FooMotification());


14. Resource

レスポンスデータ作成前のデータ型変換

▼ データ型変換の必要性

EloquentモデルをJSON型データとしてレスポンスする時に、一旦、配列データに変換する必要がある。

▼ 単一のEloquentモデルの配列化

単一のEloquentモデルを配列に変換する。

ResourceクラスのtoArrayメソッドにて、this変数は自身ではなく、Resourceクラス名につくEloquentモデル名になる。

また、this変数からゲッターを経由せずに直接的にプロパティにアクセスできる。

Controllerにて、ResouceクラスにEloquentモデルを渡すようにする。

LaravelはレスポンスのJSON型データを作成するために、まずtoArrayメソッドにより配列化し、加えてこれをJSON型データに変換する。

*実装例*

Fooクラスからデータを取り出し、配列化する。

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class FooJsonResource extends JsonResource
{
    /**
     * オブジェクトを配列に変換します。


     *
     * @param  Request
     * @return array
     */
    public function toArray($request)
    {
        return [
            "id"       => $this->id,
            "name"     => $this->name,
        ];
    }
}
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * クライアントにデータを返却します。


     *
     * @param  Request  $request
     * @return Response
     */
    public function index(Request $request)
    {
        // ここに、EloquentモデルをDBから取得する処理

        // Eloquentモデルを渡す。
        return new FooResource($foo);
    }
}

▼ 複数のEloquentモデル (Collection型) の配列化

複数のEloquentモデル (Collection型) を配列に変換する。

// ここに実装例


15. Routing

api.phpファイル

▼ Middlewareの適用

APIのエンドポイントとして働くルーティング処理を実装する。

実装したルーティング処理時には、KernelクラスのmiddlewareGroupsプロパティのapiキーで設定したミドルウェアが実行される。

APIのエンドポイントは外部公開する必要があるため、webキーと比較して、セキュリティのためのミドルウェアが設定されていない。

<?php

namespace App\Http;

use App\Http\Middleware\BeforeMiddleware\FooIdConverterMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    // 〜 中略 〜

    /**
     * The application"s route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [

        // 〜 中略 〜

        "api" => [
            "throttle:60,1",
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    // 〜 中略 〜
}


web.phpファイル

▼ Middlewareの適用

API以外のルーティング処理を実装する。

実装したルーティング処理時には、KernelクラスのmiddlewareGroupsプロパティのwebキーで設定したミドルウェアが実行される。

API以外のルーティングは外部公開する必要がないため、apiキーと比較して、セキュリティのためのミドルウェアが多く設定されている。

例えば、CSRF対策のためのVerifyCsrfTokenクラスがある。

<?php

namespace App\Http;

use App\Http\Middleware\BeforeMiddleware\FooIdConverterMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    // 〜 中略 〜

    /**
     * @var array
     */
    protected $middlewareGroups = [
        "web" => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        // 〜 中略 〜

    ];

    // 〜 中略 〜
}


guest.phpファイル

ヘルスチェックなど、API認証が不要なルーティング処理を実装する。


暗黙のモデル結合

▼ コントローラー使用時

ルーティング時に使用するパラメーター名とコントローラーのメソッドの引数型と変数名が同じであり、かつパラメーターに数値が割り当てられた場合、その数値をIDとするEloquentモデルが自動的にインジェクションされる。

*実装例*

ルーティング時に、パスパラメーター名をuserとしておく。

<?php

Route::get('/users/{user}', 'UserController@index');

かつ、コントローラーのメソッドの引数型/変数名をUser/$userとする。

または。

この時、『/users/1』に対してリクエストを送信すると、ユーザーIDが1のユーザーがDBから読み出され、コントローラーにインジェクションされる。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;

class UserController extends Controller
{
    /**
     * @param User $user
     */
    public function index(User $user)
    {
        $id = $user->id; // パスパラメーターのidに紐付くユーザーが自動的に渡されている。
    }
}


16. Security

CSRF対策

▼ アプリケーション側の対応

セッション開始時にCSRFトークンが作成される。

Bladeを使用してサーバ側のCSRFトークンを取り出し、inputタグのhidden属性にCSRFトークンを割り当て送信する。

<form method="POST" action="/profile">@csrf ...</form>

Bladeを使用しない場合、セッション開始時のレスポンスのSet-CookieにCSRFトークンが割り当てられるため、これを取り出してX-CSRF-TOKENヘッダーやX-XSRF-TOKENヘッダーに割り当てるようにする。リクエストのたびに異なるCSRFトークンがレスポンスされ、これを次のリクエストで使用する必要がある。

▼ HTTPクライアントツール側の対応

PostmanなどのHTTPクライアントツールをフロントエンドの代わりに使用する場合は、レスポンスで返信されるCSRFトークを扱えない、そこで、各リクエストで事前にルートパスのエンドポイントをコールし、CSRFトークンをPostmanの環境変数に保管するようなスクリプトを設定しておくと良い。

if (pm.request.method == "GET") {
  return true;
}

return pm.sendRequest("http://127.0.0.1:8000", (error, response, {cookies}) => {
  if (error) {
    console.error(error);
    return false;
  }

  const xsrfTokenHeader = cookies.one("XSRF-TOKEN");

  if (!xsrfTokenHeader) {
    console.log("トークンがありません");
    return false;
  }

  // laravelによってエンコードされたトークンをデコードする。
  const xsrfToken = decodeURIComponent(xsrfTokenHeader["value"]);
  // 環境変数を挿入するために、該当する実行環境名をCollection全体に適用しておく必要がある。
  pm.environment.set("XSRF_TOKEN", xsrfToken);
  console.log(xsrfToken);
  return true;
});

▼ XSS対策

▼ 常時HTTPS化


17. Seeder

初期データの定義

▼ DBファサードによる定義

DBファサードを使用して、初期データを定義する。

<?php

use Illuminate\Database\Seeder;
use App\Constants\ExecutorConstant;

class ProductsSeeder extends Seeder
{
    /**
     * Seederを実行します。


     *
     * @return void
     */
    public function run()
    {
        DB::table("products")->insert([
                "product_name" => "シャープペンシル",
                "price"        => 300,
                "product_type" => 1,
                "created_by"   => ExecutorConstant::ARTISAN_COMMAND,
                "updated_by"   => ExecutorConstant::ARTISAN_COMMAND,
                "created_at"   => NOW(),
                "updated_at"   => NOW(),
                "deleted_at"   => NULL
            ],
            [
                "product_name" => "ノート",
                "price"        => 200,
                "product_type" => 2,
                "created_by"   => ExecutorConstant::ARTISAN_COMMAND,
                "updated_by"   => ExecutorConstant::ARTISAN_COMMAND,
                "created_at"   => NOW(),
                "updated_at"   => NOW(),
                "deleted_at"   => NULL
            ],
            [
                "product_name" => "消しゴム",
                "price"        => 100,
                "product_type" => 3,
                "created_by"   => ExecutorConstant::ARTISAN_COMMAND,
                "updated_by"   => ExecutorConstant::ARTISAN_COMMAND,
                "created_at"   => NOW(),
                "updated_at"   => NOW(),
                "deleted_at"   => NULL
            ],

            ...

        ]);
    }
}

実行者名は、定数として管理しておくと良い。

<?php

namespace App\Constants;

/**
 * 実行者定数クラス
 */
class ExecutorConstant
{
    /**
     * Artisanコマンド
     */
    public const ARTISAN_COMMAND = "Artisan Command";

    /**
     * スタッフ
     */
    public const STAFF = "Staff";

    /**
     * ゲスト
     */
    public const GUEST = "Guest";
}

▼ CSVファイルによる定義

CSVファイルを使用して、初期データを定義する。

DBファサードを使用するよりも。

大サイズのデータを定義しやすい。

この時、LOAD DATA LOCAL INFILE文を使用すると、高速で処理できる。

<?php

use Illuminate\Database\Seeder;
use App\Constants\ExecutorConstant;

class ProductsSeeder extends Seeder
{
    /**
     * Seederを実行します。


     *
     * @return void
     */
    public function run()
    {
        // 〜 中略 〜

        $this->importCsv();

        // 〜 中略 〜
    }

    /**
     * CSVを読み込んでDBにデータを保存します。


     */
    private function importCsv(): void
    {
        foreach ($this->tables as $table) {

            // S3に保存してあるCSVファイルを読み込む。
            $csv = \Storage::get(migrations/csv/ . $table . '.csv');

            // 一時CSVファイルに書き込む。
            file_put_contents('/tmp/csv', $csv);

            // 一時ファイルを使用して、DBにCSVファイルの中身を書き込む。
            \DB::statement(
                "LOAD DATA LOCAL INFILE '/tmp/csv'
                INTO TABLE {$table}
                FIELDS terminated by ','
                ENCLOSED BY '\"'
                ESCAPED BY '\\\'
                LINES TERMINATED BY '\n'
                IGNORE 1 LINES"
            );
        }
    }
}


Seederの実行

DatabaseSeederにて、全てのSeederをまとめて実行する。

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seederを実行します。


     *
     * @return void
     */
    public function run()
    {
        // 開発環境用の初期データ
        if (App::environment("local")) {
            $this->call([
                // ダミーデータ
            ]);
        }

        // テスト環境用の初期データ
        if (app()->environment("tes")) {
            $this->call([
                // リアルデータ
            ]);
        }

        // ステージング環境用の初期データ
        if (App::environment("stg")) {
            $this->call([
                // リアルデータ
                ProductsSeeder::class
            ]);
        }

        // 本番環境用の初期データ
        if (App::environment("prd")) {
            $this->call([
                // リアルデータ
                ProductsSeeder::class
            ]);
        }
    }
}


18. ServiceProvider

ServiceProvider

▼ ServiceProviderの用途

用途の種類 説明
AppServiceProvider ・ServiceContainerへのクラスのバインド (登録)
・ServiceContainerからのインスタンスのリゾルブ (作成)
MacroServiceProvider ServiceContainerへのメソッドのバインド (登録)
RouteServiceProvider
(app.phpweb.phpも使用)
ルーティングとコントローラーの対応関係の定義
EventServiceProvider EventListenerとEventhandler関数の対応関係の定義

▼ ServiceProviderのコール

クラスの名前空間を、config/app.phpファイルのproviders配列に登録すると、アプリケーションの起動時にServiceProviderをコールできる。

そのため、ServiceContainerへのクラスのバインドが自動的に完了する。

*実装例*

<?php

"providers" => [

    // 複数のServiceProviderが実装されている

    App\Providers\ComposerServiceProvider::class,
],


ServiceContainer

▼ ServiceContainer、バインド、リゾルブとは

ServiceContainer、バインド、リゾルブについては、以下のリンクを参考にせよ。

<?php

namespace Illuminate\Contracts\Container;

use Closure;
use Psr\Container\ContainerInterface;

interface Container extends ContainerInterface
{
    /**
     * 通常のバインディングとして、自身にバインドする。


     * 第二引数は、クロージャー、もしくはクラス名前空間
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @param  bool  $shared
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false);

    /**
     * singletonとして、自身にバインドする。


     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @return void
     */
    public function singleton($abstract, $concrete = null);
}


AppServiceProvider

▼ 単一のクラスをバインド/リゾルブ

AppSeriveProviderにて、ServiceContainerにクラスをバインドすることによって、ServiceContainerがインスタンスをリゾルブできるようになる。

これにより、メソッドの引数でクラスを指定しさえすれば、そのクラスのインスタンスが渡されるため、自動的に依存オブジェクト注入が実行されたことになる。

Laravelでは、クラスはServiceContainerに自動的にバインドされており、引数でクラスを指定するのみでインスタンスが作成されるため、以下の実装を実行する必要はない。

ただし、混合型の場合は引数の型を指定できないため、リゾルブは実行できない。

*実装例*

バインドする。

注意点として、Laravelでは不要である。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Models\Bar;
use App\Models\Baz;
use App\Models\Foo;

class FooServiceProvider extends ServiceProvider
{
    /**
     * コンテナにバインド
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(Foo::class, function ($app) {
            return new Foo(new Bar, new Baz);
        });
    }
}

引数の型を元に、クラスのインスタンスがリゾルブされる。

<?php

class Qux
{
    /**
     * @param Foo $foo
     */
    public function method(Foo $foo) // リゾルブされる。
    {
        $foo->bar;
        $foo->baz;
    }
}

引数の型を指定しない場合は、手動で渡す必要がある。

<?php

use App\Models\Foo;

class Qux
{
    /**
     * @param Foo $foo
     */
    public function __construct($foo) // 引数の型を指定しない場合、リゾルブされない。
    {
        $foo->bar;
        $foo->baz;
    }
}

$foo = new Foo();
$qux = new Qux($foo); // 手動で渡す

混合型の場合は、引数の型を指定できないため、リゾルブを実行できない。

<?php

use App\Models\Foo1;
use App\Models\Foo2;
use App\Models\Foo3;

class Qux
{
    /**
     * @param Foo1|Foo2|Foo3 $mixed
     */
    public function __construct($mixed) // 混合型では引数の型を指定できない
    {
        $mixed->bar;
        $mixed->baz;
    }
}

$foo1 = new Foo1();
$foo2 = new Foo2();
$foo3 = new Foo3();

$qux = new Qux($foo1);

▼ 複数のクラスをバインド/リゾルブ

メソッドの引数でクラスを指定しさえすれば、そのクラスのインスタンスが渡されるため、自動的に依存オブジェクト注入が実行されたことになる。

*実装例*

<?php

namespace App\Providers;

use App\Models\Foo\Entities;
use App\Models\Bar;
use App\Models\Baz;
use Illuminate\Support\ServiceProvider;

class FoosServiceProvider extends ServiceProvider
{
     /**
     * 各registerメソッドをコール
     *
     * @return void
     */
    public function register()
    {
        $this->registerFoo();
        $this->registerBar();
        $this->registerBaz();
    }

    /**
    * 1つ目のクラスをバインド
    */
    private function registerFoo()
    {
        $this->app->bind(Foo::class, function ($app) {
            return new Foo();
        });
    }

    /**
    * 2つ目のクラスをバインド
    */
    private function registerBar()
    {
        $this->app->bind(Bar::class, function ($app) {
            return new Bar();
        });
    }

    /**
    * 3つ目のクラスをバインド
    */
    private function registerBaz()
    {
        $this->app->bind(Baz::class, function ($app) {
            return new Baz();
        });
    }
}

▼ インターフェースをバインドし、実装クラスをリゾルブ

Laravelではクラスが自動的にバインドされ、これのインスタンスがリゾルブされる、しかし、バインドされたクラスとは別のクラスのインスタンスをリゾルブしたい場合は、ServiceProviderにそれを定義すれば、自動的なバインドを上書きできる。

これを使用して、インターフェースをバインドし、実装クラスをリゾルブ可能にする。

この方法は、上位レイヤーが抽象に依存することが必要な場面 (例:依存性逆転の原則) で役立つ。

*実装例*

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\FooRepository as FooRepositoryInterface;
use App\Infrastructure\FooRepository;

class FooServiceProvider extends ServiceProvider
{
    /**
     * コンテナにバインド
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            "App\Domain\Foo\Repositories\FooRepositoryInterface", // インターフェース
            "App\Infrastructure\Doo\Repositories\FooRepository" // 実装クラス
        );
    }
}
<?php

class Interactor
{
    /**
     * @var FooRepositoryIF
     */
    private FooRepositoryIF $fooRepository;

    /**
     * @param FooRepositoryIF $fooRepository
     */
    public function __constructor(FooRepositoryIF $fooRepository) // リゾルブされる。
    {
        $this->fooRepository = $fooRepository;
    }
}

makeメソッド

引数の型でリゾルブを実行する以外に、makeメソッドも使用できる。

makeメソッドの引数にクラスの名前空間を渡すことにより、インスタンスがリゾルブされる。

*実装例*

<?php

class Foo
{
    public function method()
    {
        return "foo";
    }
}

// Fooクラスをリゾルブし、そのままmethodをコール
$result = app()->make(Foo::class)
    ->method();

// Fooクラスをリゾルブ
$foo = App::make(Foo::class);
$result = $foo->method();

registerメソッドとbootメソッドの違い

Laravelのライフサイクルで、ServiceContainerへのクラスのバインドの時には、まずServiceProviderのregisterメソッドが実行され、その後にbootメソッドが実行される。

そのため、ServiceProviderが他のServiceProviderをコールするような処理を実装したいとき、これはbootメソッドに実装することが適している。


MigrationMacroServiceProvider

複数のテーブルに共通のカラムを作成するDBマイグレーション処理を提供する。

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\ServiceProvider;

/**
 * マイグレーションマクロサービスプロバイダクラス
 */
class MigrationMacroServiceProvider extends ServiceProvider
{
    /**
     * サービスコンテナにDBマイグレーションメソッドをバインドします。


     *
     * @return void
     */
    public function register()
    {
        Blueprint::macro("systemColumns", function () {
            $this->string("created_by")
                ->comment("レコードの作成者")
                ->nullable();
            $this->string("updated_by")
                ->comment("レコードの最終更新者")
                ->nullable();
            $this->timestamp("created_at")
                ->comment("レコードの作成日")
                ->nullable();
            $this->timestamp("updated_at")
                ->comment("レコードの最終更新日")
                ->nullable();
            $this->timestamp("deleted_at")
                ->comment("レコードの削除日")
                ->nullable();
        });

        Blueprint::macro("dropSystemColumns", function () {
            $this->dropColumn(
                "created_by",
                "updated_by",
                "created_at",
                "updated_at",
                "deleted_at"
            );
        });
    }
}

DBマイグレーションファイルにて、定義したsystemColumnメソッドをコールする。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateFooTable extends Migration
{
    /**
     * マイグレート
     *
     * @return void
     */
    public function up()
    {
        Schema::create("foos", function (Blueprint $table) {
            $table->bigIncrements("foo_id")
                ->comment("ID");
            $table->string("name")
                ->comment("名前");

            // MigrationMacroServiceProviderのメソッドを使用する。
            $table->systemColumns();

            // deleted_atカラムを追加する。
            $table->softDeletes();
        });
    }

    /**
     * ロールバック
     *
     * @return void
     */
    public function down()
    {
        Schema::drop("foos");
    }
}

最後に、app.phpファイルにて、MigrationMacroServiceProviderを新しく読み込む。

<?php

return [

    ...

    'providers' => [
        // マクロサービスプロバイダー
        App\Providers\MigrationMacroServiceProvider::class,
    ],

    ...

];


RouteServiceProvider

▼ 全てのルーティングへの処理

ルーティングの設定ファイルをコールする。

また、全てのルーティングに適用する処理を定義する。

*実装例*

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * ルーティングの設定ファイルをコールします。


     *
     * @return void
     */
    public function boot()
    {
        // ヘルスチェック
        Route::prefix('api')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/healthcheck.php'));

        // API
        Route::prefix('api')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));
    }
}

▼ リクエスト数制限

1分間当たりに許容するリクエスト数とその制限名をconfigureRateLimitingメソッドで定義する。

加えて、Throttleミドルウェアに制限名を渡し、指定したルートにリクエストの上限数を適用させる、もし制限を超えた場合、configureRateLimitingメソッドによって、429ステータスでレスポンスが返信される。

*実装例*

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * @var array
     */
    protected $middlewareGroups = [

        'web' => [
        ],

        'api' => [
            // throttleミドルウェアを適用する。
            'throttle:limit_per_minute',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    /**
     * @return void
     */
    public function configureRateLimiting()
    {
        RateLimiter::for('limit_per_minute', function (Request $request) {
            // 一分間当たり1000リクエストまでを許可する。
            return Limit::perMinute(1000);
        });
    }
}


EventServiceProvider

▼ EventとListenerの登録

EventとListenerの対応関係を定義する。

注意点として、Eventを発火させてListenerを実行する方法は、Eventコンポーネントを参考にせよ。

<?php

namespace App\Providers;

use App\Events\UpdatedModelEvent;
use App\Listeners\UpdatedModelListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * イベントとリスナーの対応関係を配列で定義します。


     * [イベント => リスナー]
     *
     * @var array
     */
    protected $listen = [
        // Eloquentモデル更新イベント
        UpdatedModelEvent::class => [
            UpdatedModelListener::class,
        ],
    ];

    /**
     * @return void
     */
    public function boot()
    {
    }
}


19. Session

セッションの操作

▼ 設定ファイル

<?php

use Illuminate\Support\Str;

return [

    'driver' => env('SESSION_DRIVER', 'file'),

    'lifetime' => env('SESSION_LIFETIME', 120),

    'expire_on_close' => false,

    'encrypt' => false,

    'files' => storage_path('framework/sessions'),

    'connection' => env('SESSION_CONNECTION', null),

    'table' => 'sessions',

    'store' => env('SESSION_STORE', null),

    'lottery' => [2, 100],

    'cookie' => env(
        'SESSION_COOKIE',
        Str::slug(env('APP_NAME', 'laravel'), '_') . '_session'
    ),

    'path'      => '/',

    // Set-Cookieヘッダーのdomain属性に値を割り当てる。
    'domain'    => env('SESSION_DOMAIN', null),

    // Set-Cookieヘッダーのsecure属性を有効化する。
    'secure'    => env('SESSION_SECURE_COOKIE', false),

    // Set-CookieヘッダーのHttpOnly属性を有効化する。
    'http_only' => true,

    // Set-CookieヘッダーのsameSite属性に値を割り当てる。nullの場合、Laxとなる。
    'same_site' => null,
];

▼ よく使用する操作メソッド

FormRequestクラスのsessionメソッドはStoreクラスを返却する。

このクラスのメソッドを使用して、セッションを操作できる。

メソッド名 説明
get セッションのキー名を指定して、1個の値を取得する。
all セッションの全ての値を取得する。
forget セッションのキー名を指定して、値を取得する。キー名を配列で渡して、複数を削除することも可能。
flush セッションの全ての値を取得する。
pull セッションのキー名を指定して、1個の値を取得し、取得後に削除する。
has セッションのキー名を指定して、値が存在しているかを検証する。nullfalseとして判定する。

*実装例*

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class FooController extends Controller
{
    /**
     * @param  Request  $request
     * @param  int  $id
     * @return Response
     */
    public function index(Request $request, $id)
    {
        // getメソッド
        $data = $request->session()->get("foo");

        // allメソッド
        $data = $request->session()->all();

        // forgetメソッド
        $request->session()->forget("foo");
        $request->session()->forget(["foo", "bar"]);

        // flush
        $request->session()->flush();

        // pullメソッド
        $data = $request->session()->pull("foo");

        // hasメソッド
        if ($request->session()->has("foo")) {

        }
    }
}

▼ セッションデータがStoreクラスに至るまで

全てを追うことは難しいため、StartSessionクラスのhandleメソッドが実行されるところから始めるものとする。

ここで、handleStatefulRequestメソッドの中のstartSessionメソッドが実行される。

これにより、Storeクラスのstartメソッド、loadSessionメソッド、readFromHandlerメソッドが実行され、SessionHandlerInterfaceの実装クラスのreadメソッドが実行される。

readメソッドは、storage/framework/sessionsにあるセッションデータに書き込まれたセッションを読み出し、attributeプロパティに格納する。

Sessionクラスのメソッドは、attributeプロパティを使用して、セッションを操作する。

最終的に,handleStatefulRequestでは、saveSessionメソッドの中のsaveメソッドが実行され、セッションデータに新しい値が書き込まれる。


20. Views

データの出力

▼ データの出力

Controllerクラスから返却されたデータは、{{ 変数名 }}で取得できる。

`

*実装例*

<html>
  <body>
    <h1>Hello!! {{ $data }}</h1>
  </body>
</html>

▼ バリデーションメッセージの出力

バリデーションでエラーが発生した場合、バリデーションでエラーがあった場合、Handlerクラスのinvalidメソッドがコールされ、MessageBagクラスがViewに渡される。

MessageBagクラスは、Blade上でerrors変数に格納されており、各メソッドをコールしてエラーメッセージを出力できる。

*実装例*

MessageBagクラスのallメソッドで、全てのエラーメッセージを出力する。

<!-- /resources/views/foo/create.blade.php -->

<h1>ポスト作成</h1>

@if ($errors->any())
<div>
  @foreach ($errors->all() as $error)
  <p class="alert alert-danger">{{ $error }}</p>
  @endforeach
</div>
@endif @isset ($status)
<div class="complete">
  <p>登録が完了しました。</p>
</div>
@endisset

<!-- ポスト作成フォーム -->
.errors {
  /* 何らかのデザイン */
}

.complete {
  /* 何らかのデザイン */
}


要素の共通化

@include (サブビュー)

読み込んだファイル全体を出力する。

読み込むファイルに対して、変数を渡すこともできる。

@extentdsとの使い分けとして、親子関係のないテンプレートの間で使用するのが良い。

両者は、PHPでいうextends (クラスチェーン) とrequire (単なる読み出し) の関係に近い。

*実装例*

<div>
  @include("shared.errors")
  <form><!-- フォームの内容 --></form>
</div>


要素の継承

@yield@extends@section@endsection

子テンプレートのレンダリング時に、子テンプレートで新しく定義したHTMLの要素を、親テンプレートの指定した場所に出力する。親テンプレートにて、@yield("foo")を定義する。

*実装例*

<!-- 親テンプレート -->

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>アプリケーション</title>
  </head>
  <body>
    <h2>タイトル</h2>
    @yield("content")
  </body>
</html>

これを子テンプレートで@extendsで継承すると、レンダリング時に、子テンプレートの@section("foo")-@endsectionで定義した要素が、親テンプレートの@yieidメソッド部分に出力される。

*実装例*

<!-- 子テンプレート -->

@extends("layouts.parent") @section("content")
<p>子テンプレートのレンダリング時に、yieldに出力される要素</p>
@endsection

補足として、子テンプレートは、レンダリング時に以下の様に出力される。

*実装例*

<!-- 子テンプレート -->

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>アプリケーション</title>
  </head>
  <body>
    <h2>タイトル</h2>
    <p>子テンプレートのレンダリング時に、yieldに出力される要素</p>
  </body>
</html>

@section@show@extends@parent

子テンプレートのレンダリング時に、親テンプレートと子テンプレートそれぞれで新しく定義したHTMLの要素を、親テンプレートの指定した場所に出力する。親テンプレートにて、@section-@showで要素を定義する。

*実装例*

<!-- 親テンプレート -->

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>アプリケーション</title>
  </head>
  <body>
    <h2>タイトル</h2>
    @section("sidebar")
    <p>親テンプレートのサイドバーとなる要素</p>
    @show
  </body>
</html>

子テンプレートの@sectionにて、@parentを使用する。

親テンプレートと子テンプレートそれぞれの要素が出力される。

*実装例*

<!-- 子テンプレート -->

@extends("layouts.app") @section("sidebar") @parent
<p>子テンレプートのサイドバーに追加される要素</p>
@endsection

補足として、子テンプレートは、レンダリング時に以下の様に出力される。

*実装例*

<!-- 子テンプレート -->

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>アプリケーション</title>
  </head>
  <body>
    <h2>タイトル</h2>
    <p>親テンプレートのサイドバーとなる要素</p>
    <p>子テンレプートのサイドバーに追加される要素</p>
  </body>
</html>

@stack@push

子テンプレートのレンダリング時に、.cssファイルと.jsファイルを動的に出力する場合に使用する。親テンプレートにて、@stack("foo")を定義する。これを継承した子テンプレートのレンダリング時に、@push("foo")-@endpushで定義した要素が、@stackメソッド部分に出力される。

*実装例*

<!-- 親テンプレート -->

<head>
  <!-- Headの内容 -->

  @stack("scripts")
</head>
<!-- 子テンプレート -->

@push("scripts")
<script src="/foo.js"></script>
@endpush


Twigとの互換

▼ Bladeで実装した場合

*実装例*

<!-- 親テンプレート -->

<!doctype html>
<html lang="">
  <head>
    <title>@yield("title")</title>
  </head>
  <body>
    @section("sidebar") 親テンプレートのサイドバーとなる要素 @show

    <div class="container">@yield("content")</div>
  </body>
</html>
<!-- 子テンプレート -->

@extends("layouts.master") @section("title",
"子テンプレートのタイトルになる要素") @section("sidebar") @parent
<p>子テンレプートのサイドバーに追加される要素</p>
@endsection @section("content")
<p>子テンプレートのコンテンツになる要素</p>
@endsection

▼ Twigで実装した場合

*実装例*

<!-- 親テンプレート -->

<!doctype html>
<html lang="">
  <head>
    <title>{% block title %}{% endblock %}</title>
  </head>
  <body>
    {% block sidebar %}
    <!-- @section("sidebar") に相当 -->
    親テンプレートのサイドバーとなる要素 {% endblock %}

    <div class="container">
      {% block content %}<!-- @yield("content") に相当 -->
      {% endblock %}
    </div>
  </body>
</html>
<!-- 子テンプレート -->

{% extends "layouts.master" %}
<!-- @extends("layouts.master") に相当 -->

{% block title %}
<!-- @section("title", "Page Title") に相当 -->
子テンプレートのタイトルになる要素 {% endblock %} {% block sidebar %}
<!-- @section("sidebar") に相当 -->
{{ parent() }}
<!-- @parent に相当 -->
<p>子テンレプートのサイドバーに追加される要素</p>
{% endblock %} {% block content %}
<!-- @section("content") に相当 -->
<p>子テンプレートのコンテンツになる要素</p>
{% endblock %}