Laravel × Dockerで始める認証・API開発:FortifyとArraytoXmlを活用した実装と工夫

# 動機
今回携わったシステム開発プロジェクトで多くの学びがあり、考えたことや工夫を記事として残そうと思い、本記事を作成しています。

※この後、コード例が出てきますが開発環境、コード共に類似環境であり本番ではありません。
# 前置き
去年9月から要件定義を開始し、今年の6月末に納入予定の建設業界の中規模業務システムのプロジェクトに携わっています。メンバーは私を含め3名で半年ほどかけて要件定義、設計、実装を進めてきました。
以下の3点を今回実装しました。

1. 認証機能(Fortify)
カスタムログインレイアウトでログイン、ログアウトボタンでログアウトする
2. 一覧・検索機能
部品リストの表示、検索を行う
3. API連携機能
自社パッケージへデータをxmlまたはjsonで提供する

現時点では実装フェーズはほぼ終了し、テストに入りつつあります。システム構成は以下になります。
- フロントエンド:JavaScript
- バックエンドLaravel12
- ローカル:Docker

# 開発環境
php8.4,mysql8.4,apache(httpd:2.4)の条件でローカル用のdockerを作成しました。以下の環境で/service直下のlaravelで開発を進めました。

https://github.com/kai-phoenix/docker_laravel_apache/blob/main/docker/apache/Dockerfile

# 技術選定
本プロジェクトでは、認証機能とAPI連携機能についていくつかの選択肢を比較検討しました。
具体的には認証機能はLaravel fortify,breeze,sanctum,UI,API連携機能についてはSimpleXMLElement,ArraytoXmlの選択肢がありました。
最終的に認証機能にLaravel fortify,API連携機能にはArraytoXmlを使用する形にしました。以下に主な理由を示します。

- Laravel Fortify
    - Laravelのオフィシャルパッケージであり、laravel12との相性が良く後の拡張性も高い
    - ログインフォームのカスタマイズが可能で独自要件への対応が容易
- ArraytoXml
    - phpの配列からxmlを簡潔かつ動的に生成できる
    - xmlを直書きする形式に対し配列を扱うので繰り返し処理などの実行処理の実装工数を削減できる
    - 直近もメンテナンスされており今後の保守性が高いと判断

## 認証機能(Fortify)
### 主な実装の流れ
1. LaravelディレクトリでFortifyのインストール
    ```bash:command
    composer require laravel/fortify
    ```
    <br>
2. 設定ファイルの編集
config/fortify.phpなどでログインに使用するカラムやパスの設定をする
    ```php:fortify.php
    /*途中省略*/
    /*ログインに使用するusernameに使用するカラムの設定*/
    'username' => 'example_id',
    /*パスワードリセット後に遷移するURLの設定*/
    'home' => '/example'
    /*途中省略*/
    ```
    <br>
3. app/Providers/FortifyServiceProvider.phpに認証機能を実装
    ```php:FortifyServiceProvider.php
    <?php
    namespace App\Providers;
    use Illuminate\Http\Request;
    use Laravel\Fortify\Fortify;
    use App\Models\User;
    use Illuminate\Support\ServiceProvider;
    use Illuminate\Support\Facades\Hash;
    use Laravel\Fortify\Contracts\LoginResponse;
    use Laravel\Fortify\Contracts\LogoutResponse;
    use Illuminate\Validation\ValidationException;
    /*途中省略*/
    class FortifyServiceProvider extends ServiceProvider
    {
        public function register():void
        {
            //ログイン時のリダイレクト先の設定
            $this->app->instance(LoginResponse::class,
                new class implements LoginResponse {
                    public function toResponse($request) {
                        return redirect('/example');
                    }
                }
            );
            //ログアウト時のリダイレクト先の設定
            $this->app->instance(LogoutResponse::class,
                new class implements LogoutResponse {
                    public function toResponse($request) {
                        return redirect('/login');
                    }
                }
            );
        }
        public function boot():void
        {
            // ログイン処理
            Fortify::authenticateUsing(function(Request $request) {
            // 例: カラム名 'sample_id' を使用してユーザーを検索
                $user = User::where('sample_id', $request->sample_id)->first();
                if($user && Hash::check($request->password,$user->password))
                {
                    //ログイン成功時の処理
                    return $user;
                }
                else
                {
                   // 認証に失敗した場合はエラーメッセージを返す
                    throw ValidationException::withMessages([
                        'error' => 'エラー文です。',
                    ]); 
                }
            });
        }
    }
    ```
## API機能(ArraytoXml)
### 主な実装の流れ
1. LaravelディレクトリでArraytoXmlのインストール
    ```bash:command
    composer require spatie/array-to-xml
    ```
    <br>
2. 使用したいControllerで以下のようなイメージで実装
    ```php:SampleController.php
    <?php
    namespace App\Http\Controllers;
    use Spatie\ArrayToXml\ArrayToXml;
    /*途中省略*/
    public function sample()
    {
        $xml_array = [
            'user' => [
                'id' => 1,
                'name' => 'John Doe',
            ]
        ];
        $xml = ArrayToXml::convert(
                array: $xml_array,
                rootElement: 'sample',
                xmlEncoding: 'utf-8',
                xmlVersion: '1.0',
                domProperties:[
                    'formatOutput'=>true,
                ]
            );
        return response($xml, 200)->header('Content-Type', 'application/xml');
    }
    ```
    この書き方だとXML出力は以下のようになる
    ```xml:sample.xml
    <?xml version="1.0"?>
    <sample>
        <user>
            <id>1</id>
            <name>John Doe</name>
        </user>
    </sample>
    ```
    <br>
    設定などは以下Qiita記事を参考にしました。
    
https://qiita.com/NUKKOSPECIAL/items/d00f54412c8e1b98179f
    
# 工夫した課題
## 一覧・検索機能のパフォーマンスチューニング
1. 検索画面の表示速度をあげるためN+1問題を回避する対策を実施

    - EagerLoading(with)を活用し、カテゴリなどの関連テーブルからデータを取得時に必要な情報を一括で読み込むように記述した
         ```php:SampleModel.php(一部)
        User::with('samples')->get();
        ```
        <br>
2. 可読性や再利用性を向上させる方法の採用
    - 検索やソートロジックなどをModelのscopeに記述した
        ```php:Model/Sample.php
        <?php
            use Illuminate\Database\Eloquent\Builder;
            
            /*途中省略*/
            
            // グループ用スコープ
            public function scopeGrouped(Builder $query,array $columns= ['sample']):Builder
            {
                foreach($columns as $column)
                {
                    $query->groupBy($column);
                }
                return $query;
            }
        ```
        これを行うことでコントローラー側は
        ```php:exampleController.php(一部)
        Sample::select()->grouped($group_columns)
        ```
        のように共通化でき、シンプルに記述できた。

# 感想
LaravelFortifyを初めて導入しましたが、公式パッケージなだけあり、認証機能をスムーズに記述できました。
ArraytoXmlは非常に便利でEloquentの配列の結果をそのままxmlで出力できるため、JSONと同じような感覚で開発が進められました。
今回技術選定から機能を実装しましたがシステムの規模・用途や今後メンテナンスされそうかなどを念頭に置きつつscopeやN+1問題も工夫しながら開発でき、保守性や拡張性をちょっと意識できたかなと思います。

# 近況
現在はテストフェーズに入っており、総合テストを並行しながら不具合修正や追加要望の対応を行っています。

# 参考書籍及びURL

https://readouble.com/laravel/12.x/ja/fortify.html#customizing-the-authentication-pipeline

https://qiita.com/NUKKOSPECIAL/items/d00f54412c8e1b98179f

https://github.com/spatie/array-to-xml