ユーザごとのAPIをまとめたLaravelパッケージ開発手順

がっつりパッケージ開発とかじゃなくてモジュール再利用したい場合とかに便利そうなのでメモ。
参考: Laravel JP Conference で Laravel のパッケージ開発に関する登壇をしてきました。

Laravelのインストール

Laravel 6.0 インストール

$ composer global require laravel/installer

パッケージ用ディレクトリの作成

管理画面向けAPI(admin)、ユーザ画面向けAPI(front)、共通ロジック(common)という構成を例にします。

composer.json の作成(common)

package/luftgarden/common/composer.json

{
    "name": "luftgarden/common",
    "minimum-stability": "dev",
    "require": {
        "barryvdh/laravel-cors": "^0.11.0",
        "nicolaslopezj/searchable": "1.*",
        "intervention/image": "^2.4",
        "aws/aws-sdk-php-laravel": "~3.0",
        "barryvdh/laravel-dompdf": "^0.8.0",
        "azuyalabs/yasumi": "2.0.*",
        "sentry/sentry-laravel": "1.3.0",
        "itsgoingd/clockwork": "^4.0.0",
        "spatie/laravel-backup": "^6.0.0"
    },
    "extra": {
        "laravel": {
            "providers": [
                "LuftGarden\\Common\\CommonServiceProvider",
                "Barryvdh\\Cors\\ServiceProvider",
                "Intervention\\Image\\ImageServiceProvider",
                "Aws\\Laravel\\AwsServiceProvider",
                "Barryvdh\\DomPDF\\ServiceProvider",
                "Sentry\\Laravel\\ServiceProvider",
                "Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
            ],
            "aliases": {
                "LuftCommon": "LuftGarden\\Common\\CommonFacade",
                "Image": "Intervention\\Image\\Facades\\Image",
                "AWS": "Aws\\Laravel\\AwsFacade",
                "PDF": "Barryvdh\\DomPDF\\Facade",
                "Sentry": "Sentry\\Laravel\\Facade",
                "Clockwork": "Clockwork\\Support\\Laravel\\Facade"
            },
            "dont-discover": []
        }
    },
    "autoload": {
        "psr-4": {
            "LuftGarden\\Common\\": "./src"
        }
    }
}

今回作成するパッケージ用に CommonServiceProviderCommonFacade を指定しているのがポイント。
あとは autoload にも namespace を指定しておく。

require するパッケージは必要に応じて調整する。

親側でパッケージを入れる際に dev でインストールする(後述)ので、何かしらライブラリをインストールする場合は不安定なバージョンが入らないように明示的にバージョン指定しておいたほうがいい。

extra.laravel 配下にサービスプロバイダとファサードを指定しておけば Auto Discovery が働いてくれる。
(laravel-backupとか指定無しで大丈夫なので必須ではないかもです)

CommonServiceProvider の作成

common 用のサービスプロバイダを作成する。
※以下の例はコピペでは動かないので適当に調整してください

package/luftgarden/common/CommonServiceProvider.php

<?php

namespace LuftGarden\Common;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
use LuftGarden\Common\Console\Commands\SampleCommand;
use LuftGarden\Common\Guard\CustomTokenGuard;
use LuftGarden\Common\Model\User;
use LuftGarden\Common\Observers\UserObserver;

class CommonServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton( 'CommonService', function () {
            return new CommonService();
        } );
    }

    /**
     * Bootstrap services.
     *
     * @param \Illuminate\Routing\Router $router
     * @return void
     */
    public function boot( \Illuminate\Routing\Router $router )
    {
        /**
         * Add custom Guard
         */
        $this->app['auth']->extend( 'custom_token', function ( $app, $name, array $config ) {
            return new CustomTokenGuard( Auth::createUserProvider( $config['provider'] ), $app['request'] );
        } );

        /**
         * Define Gates
         */
        $this->defineGates();

        /**
         * Middleware alias name
         */
        $router->aliasMiddleware(
            'sample.middleware',
            \LuftGarden\Common\Http\Middleware\SampleMiddleware::class
        );

        /**
         * Front API custom middleware group
         */
        $router->middlewareGroup( 'front', [
            'throttle:60,1',
            'bindings',
            \Barryvdh\Cors\HandleCors::class,
        ] );

        /**
         * Admin API custom middleware group
         *
         * @see \App\Http\Kernel.php $middlewareGroups['api']
         */
        $router->middlewareGroup( 'admin', [
            'throttle:60,1',
            'bindings',
            \Barryvdh\Cors\HandleCors::class,
        ] );

        /**
         * Configs
         */
        $this->mergeConfigFrom( __DIR__ . '/config/common.php', 'common' );

        /**
         * Common routes / Migrations
         */
        $this->loadRoutesFrom( __DIR__ . '/routes/common.php' );
        $this->loadMigrationsFrom( __DIR__ . '/database/migrations' );

        /**
         * Commands
         */
        if ( $this->app->runningInConsole() ) {
            $this->commands( [
                SampleCommand::class,
            ] );
        }

        /**
         * Schedules
         */
        $this->app->booted( function () {
            $schedule = $this->app->make( Schedule::class );

            $schedule->command( 'sample:command' )->everyFiveMinutes();
        } );

        /**
         * Observer
         *
         * @see https://readouble.com/laravel/6.0/ja/eloquent.html#observers
         */
        User::observe( UserObserver::class );
    }

    /**
     * Gateの定義追加
     */
    private function defineGates()
    {
        /**
         * データ所有者の一致
         */
        Gate::define( 'possessor', function ( $user, $target_user ) {
            return $user->id === $target_user->id;
        } );
    }
}

register メソッドはシングルトンでサービスクラスを用意しているだけ。
重要なのは boot メソッドのほう。コメント毎に説明していきます。

Add custom Guard

カスタムガードの登録。通常のLaravel認証を使うなら必要ない。
カスタムガードの作り方自体は以下の記事とかを参考に。
[Laravel] Token認証で利用するキー(api_token)の名前を変更する

Define Gates

ゲートの登録。
ここだけメソッド化しているけど好きにしてください。

Middleware alias name

ミドルウェアクラスのエイリアス名を登録。

Front (Admin) API custom middleware group

Laravel標準のAPIミドルウェアに laravel-cors を加えただけ。
throttleが足りなければ適当に調整する。
Laravel 6.0 認可

Configs

コンフィグの登録。
複数登録して細分化しておけば捗る(適当)。
Laravel 6.0 設定

Commands

コマンドの登録。
runningInConsole() で判定するのがポイント。

Schedules

タスクスケジュールの登録。
もちろん前項で登録したコマンドはここで割り当てることができる。

Observer

オブザーバの登録。
論理削除と物理削除で処理を変えたいときは Model::isForceDeleting() を使えばいい。

オブザーバのドキュメントは場所が分かり辛いので頑張って探す。
Laravel 6.0 Eloquent:利用の開始 | オブザーバ

CommonFacade と CommonService の作成

作成必須ではない(作成しない場合は composer.jsonCommonServiceProvider.php から対象コードを削除しておく)。

/package/luftgarden/common/CommonFacade.php

<?php

namespace LuftGarden\Common;

use Illuminate\Support\Facades\Facade;

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

/package/luftgarden/common/CommonService.php

<?php

namespace LuftGarden\Common;

class CommonService
{
    public function getClass()
    {
        return __CLASS__;
    }
}

common パッケージのインストール

ここまで作業が終わったら親側でパッケージをインストールしてみる。

composer.json の編集

作成した common パッケージを親側(Laravelルート)の composer.json に追加する。

composer.json

{
    //...中略...

    "repositories": [
        {
            "type": "path",
            "url": "package/luftgarden/common"
        }
    ],
}

パッケージのインストールと動作確認

$ composer require luftgarden/common:*@dev
$ php artisan serve

composer.json の作成(admin)

次は admin 用の composer.json を作成する。

/package/luftgarden/admin/composer.json

{
    "name": "luftgarden/admin",
    "minimum-stability": "dev",
    "require": {},
    "extra": {
        "laravel": {
            "providers": [
                "LuftGarden\\Admin\\AdminServiceProvider"
            ],
            "aliases": {
                "LuftAdmin": "LuftGarden\\Admin\\AdminFacade"
            },
            "dont-discover": []
        }
    },
    "autoload": {
        "psr-4": {
            "LuftGarden\\Admin\\": "./src"
        }
    }
}

AdminServiceProvider の作成

admin 用のサービスプロバイダを作成する。

package/luftgarden/admin/AdminServiceProvider.php

<?php

namespace LuftGarden\Admin;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

class AdminServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton( 'AdminService', function () {
            return new AdminService();
        } );

        $this->map();
    }

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

    /**
     * Define the routes for the application.
     *
     * @return void
     */
    public function map()
    {
        $this->mapAdminRoutes();
    }

    /**
     * Define the admin API routes for the application.
     *
     * @return void
     */
    protected function mapAdminRoutes()
    {
        Route::prefix( 'api/v1/admin' )
             ->middleware( 'admin' )
             ->name( 'admin.' )
             ->group( __DIR__ . '/routes/admin.php' );
    }
}

AdminService の登録と map() メソッドでルーティング登録してるだけなのでシンプル。

middleware( 'admin' ) とすることで、CommonServiceProvider.php で定義したAdmin用のミドルウェアグループをルート全体に適用している。また、 name( 'admin.' ) とすれば各ルートにプリフィックス付けられて便利。

AdminFacade と AdminService の作成

こちらも common と同様に作成必須ではない。

/package/luftgarden/admin/AdminFacade.php

<?php

namespace LuftGarden\Admin;

use Illuminate\Support\Facades\Facade;

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

/package/luftgarden/admin/AdminService.php

<?php

namespace LuftGarden\Admin;

class AdminService
{
    public function getClass()
    {
        return __CLASS__;
    }
}

admin パッケージのインストール

最後に common パッケージと同様の手順でインストールする。

composer.json の編集

作成した admin パッケージを親側(Laravelルート)の composer.json に追加する。

composer.json

{
    //...中略...

    "repositories": [
        {
            "type": "path",
            "url": "package/luftgarden/common"
        },
        {
            "type": "path",
            "url": "package/luftgarden/admin"
        }
    ],
}

パッケージのインストールと動作確認

$ composer require luftgarden/admin:*@dev
$ php artisan serve

おわりに

front も admin と同様の手順で作成できます。
最終的には以下のようなディレクトリ/ファイル構成になります(見づらい)。