【Laravel】TALL stackなFilamentを使って爆速で管理画面を構築する

# はじめに
TALL stackという技術スタックをご存知でしょうか？僕は最近知りました。

  - Tailwind
  - Alpine.js
  - Laravel
  - Livewire

の頭文字を取って**TALL**。
_Laravelコミュニティのメンバーによって作られたフルスタック開発ソリューション_（引用：[tallstack.dev](https://tallstack.dev/)）だそうです。
> Laravelを学び、Tailwindで簡単にスタイルを作り、Laravel風のLivewireコンポーネントを書き、Alpine.jsをひとふりすれば、フルスタックのリアクティブでインタラクティブな創造のプラットフォームができあがります。

（引用：[tallstack.dev](https://tallstack.dev/)）
本記事ではこの技術スタックを採用している[Filament](https://filamentadmin.com/)というライブラリを利用してリアクティブでインタラクティブな管理画面を爆速で構築してみます。

# 開発環境
全てローカルで動作させます。
Laravel new ○○（アプリ名）でベーシックなLaravelがインストールされていることが前提です。

- PHP 8.00
- MySQL 5.7.28
- Laravel Framework 8.65

ドメイン名は「filament.local」とします。

参考にする公式ドキュメントは[こちら](https://filamentadmin.com/docs/1.x/admin/getting-started)です。
ではやっていきましょう。

## インストール
基本は公式のGet Startedに沿ってインストールしていくだけです。

```
composer require filament/filament ← コマンド実行
~~~省略~~~
php artisan migrate ← コマンド実行
Migration table created successfully.
Migrating: 0000_00_00_000000_create_filament_users_table
Migrated:  0000_00_00_000000_create_filament_users_table (72.41ms)
Migrating: 0000_00_00_000001_create_filament_password_resets_table
Migrated:  0000_00_00_000001_create_filament_password_resets_table (174.52ms)
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (374.07ms)
~~~省略~~~
```
見て分かる通り、デフォルトのユーザーテーブル以外にfilament_usersテーブルが作成されます。
完了したら管理画面を操作するための管理者アカウントを作成します。
下記コマンドを実行すると対話形式でどのようなアカウントを作成するか指定することができます。
今回の場合は下記のように設定しました。

> 名前：Admin
メールアドレス：example@example.com
パスワード：password
管理者にするかどうか：yes

一番最後の質問はfilamentのgithub repoにスターをするかどうかなのでお好みでyes, noを選びましょう。

```
php artisan make:filament-user

 Name:
 > Admin

 Email:
 > example@example.com

 Password:
 > 

 Would you like this user to be an administrator? (yes/no) [yes]:
 > yes

Success! example@example.com may now log in at http://app.test/admin/login.

 Would you like to show some love by starring the repo? (yes/no) [yes]:
 > 

```
完了後、ブラウザからhttp://filament.local/admin/ にアクセスするとログイン画面が表示されます。

![スクリーンショット 2021-12-02 14.18.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115590/aa9a0524-0d91-7c08-c007-2844dd7c2816.png)

ログイン後はこんな感じ。まだ何もないのでシンプルですね。
![スクリーンショット 2021-12-02 14.18.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115590/5c8725bf-9beb-b544-67c7-70348383699c.png)

filamentは独自の認証機構を持っているので、すでに既存のLaravelでUsersテーブルを持っていて認証機構を実装していたとしても、競合せずに爆速で管理画面を構築することができます。

以上、filamentを使った爆速管理画面構築でした！

・・・としてもいいのですが、これだけだとあまりにも内容が薄いのでいくつか手を加えてみます。

## User関連のメニュー実装
管理画面左のメニューにはまだ「dashboard」しかありません。ここに「users」のようなメニューを実装してユーザー関連の操作（一覧、編集、削除）機能実装してみます。

### デモデータを用意
### 下準備
現状Usersテーブルには何もデータがないのでデモ用のデータをseederで作成します。
デフォルトの10人だと寂しいので100人に変更しました。

```database/seeders/DatabaseSeeders.php
class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        \App\Models\User::factory(100)->create();
    }
}
```
完了したらシーダー実行。

```
php artisan db:seed
Database seeding completed successfully.
```

完了するとUsersテーブルに100人のデモユーザーができているはずです。
これで下準備完了です。

### UsersResourceの作成
filamentの管理画面で操作するためのResourceを作成します。これはapp/Models/のUserと紐づけられます。

```
php artisan make:filament-resource User
Successfully created UserResource!
```
このコマンドでapp/Filament/Resourcesディレクトリ内に

- UserResource.php
- UserResource/Pages/CreateUser.php
- UserResource/Pages/EditUser.php
- UserResource/Pages/ListUser.php

の計4ファイルが自動生成されます。

```
アプリ名/　<-　Laravelのルートディレクトリ
　├ app/
   └ Console
~~~省略~~~
   ├ Filament/
   │ ~~~省略~~~
   │  ├ Resources/
   │  │  ├ UserResource/
   │  │  │  └ Pages/
   │  │  │   ├ CreateUser.php
   │  │  │   ├ EditUser.php
   │  │  │   └ ListUser.php
   │  │  └ UserResource.php
   │  │
~~~省略~~~
```
ブラウザからhttp://filament.local/admin/resource/user にアクセスすると一覧画面らしきものが表示されますが、まだ2ステップほど手を加える必要があります。
![スクリーンショット 2021-12-02 14.56.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115590/ad2f50f5-2e0a-6aed-2519-9011a8337ef7.png)

## Tables
一覧画面にユーザーの情報を表示するにはUserResouce.phpのtableメソッドに追記する必要があります。

```app/Filament/Resouces/UserResource.php
<?php

namespace App\Filament\Resources;

use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers;
use App\Filament\Roles;
use Filament\Resources\Forms\Components;
use Filament\Resources\Forms\Form;
use Filament\Resources\Resource;
use Filament\Resources\Tables\Columns;
use Filament\Resources\Tables\Columns\Column;
use Filament\Resources\Tables\Filter;
use Filament\Resources\Tables\Table;

class UserResource extends Resource
{
・・・
    public static function table(Table $table)
    {
        return $table
            ->columns([
                Columns\Text::make('id')->primary(),
                Columns\Text::make('name')
                    ->searchable(),
                Columns\Text::make('email')
                    ->url(fn ($user) => "mailto:{$user->email}")
                    ->searchable(),
                Columns\Text::make('created_at'),
                Columns\Text::make('updated_at')
            ])
            ->filters([
                //
            ]);
    }
・・・
```
表示したいものをColumns\Textで記述していきます。
デフォルトのテーブル定義に従ってID, 名前, メアド（クリックでメーラー起動）, 作成日, 更新日を表示しました。
Columns\Text以外にもColumns\ImageやColumns\Iconなどもありますがここでは割愛。
searchable()をつけると管理画面上で検索できるようになります。
そのほか色々便利なメソッドやFilter機能などもありますので公式ドキュメントの[Building Tables](https://filamentadmin.com/docs/1.x/admin/tables)を参考ください。

ここまで完了すると一気に管理画面ぽくなります。ここまでの状態でユーザー削除もできるようになっています。
![スクリーンショット 2021-12-02 15.11.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115590/315c4e50-9a13-4ff6-6742-0649976bb679.png)

### Forms
あと1ステップです。一覧画面はできましたが、新規作成や編集がまだできません。
新規作成や編集するためのフォームをUserResouce.phpのformメソッドに記述していきましょう。

```app/Filament/Resouces/UserResource.php
<?php

namespace App\Filament\Resources;

use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers;
use App\Filament\Roles;
use Filament\Resources\Forms\Components;
use Filament\Resources\Forms\Form;
use Filament\Resources\Resource;
use Filament\Resources\Tables\Columns;
use Filament\Resources\Tables\Columns\Column;
use Filament\Resources\Tables\Filter;
use Filament\Resources\Tables\Table;

class UserResource extends Resource
{
・・・
    public static function form(Form $form)
    {
        return $form
            ->schema([
                Components\Grid::make([
                    Components\TextInput::make('name')
                        ->label('Name')
                        ->disableAutocomplete()
                        ->required(),
                    Components\TextInput::make('email')
                        ->label('Email')
                        ->email()
                        ->disableAutocomplete()
                        ->required()
                        ->unique(static::getModel(), 'email', true),
                ]),
                Components\Fieldset::make('filament::resources/user-resource.form.password.fieldset.label.edit', [
                    Components\TextInput::make('password')
                        ->label('filament::resources/user-resource.form.password.fields.password.label')
                        ->password()
                        ->autocomplete('new-password')
                        ->confirmed()
                        ->minLength(8)
                        ->only(Pages\CreateUser::class, fn ($field) => $field->required()),
                    Components\TextInput::make('passwordConfirmation')
                        ->label('confirm password')
                        ->password()
                        ->autocomplete('new-password')
                        ->only(Pages\CreateUser::class, fn ($field) => $field->required())
                        ->only([
                            EditAccount::class,
                            Pages\EditUser::class,
                        ], fn ($field) => $field->requiredWith('password')),
                ])->only(
                    Pages\CreateUser::class,
                    fn ($fieldset) => $fieldset->label('filament::resources/user-resource.form.password.fieldset.label.create'),
                ),
            ]);
    }
・・・
```

こうすることで

- ユーザーの新規作成画面
![スクリーンショット 2021-12-14 16.54.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115590/c007af70-a3a6-3a10-2d37-a89d4b29d995.png)

- ユーザーの編集画面
![スクリーンショット 2021-12-14 16.56.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115590/01c43154-d324-1d15-9e70-a6609025746f.png)

これでユーザー関連のCRUDが実装できました🎉

が、実はまだ2つの問題があります。

- ユーザーの新規作成時にパスワードがHash化されずにDBにインサートされてしまう。
- ユーザーの編集画面でパスワードを変えようとしてもバリデーションが通らない

ライブラリのissueやstackoverflowで色々検索してみたものの、特に問題として上がっていないようなので僕の実装が間違っている可能性もありますが、特に1つ目のパスワードが平文で保存されてしまうのは致命的なので回避策を一つ記載しておきます。Filament使われる方はご参考ください。

### ユーザー新規作成時にpasswordをHash化してDBに登録するHotfix
CreateUser.phpにbeforeCreateメソッドを追加します。

```app/Filament/Resources/UserResource/Page/CreateUser.php
<?php

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;


class CreateUser extends CreateRecord
{
    public static $resource = UserResource::class;

    protected function beforeCreate()
    {
        $this->record['password'] = Hash::make($this->record['password']);
        unset($this->record['passwordConfirmation']);
    }
・・・
}
```
CreateUserの継承元であるCreateRecordのcreateメソッドにはフック可能な関数が以下の4つ用意されています。

- beforeValidate（フォームのバリデーション前）
- afterValidate（フォームのバリデーション後）
- beforeCreate（モデルの保存前）
- afterCreate（モデルの保存後）

このうち、beforeCreateで保存される前にパスワードをHash化したものに差し替えることでDBに暗号化されたpasswordが保存されるようにしています。

### ユーザー編集画面でパスワードのバリデーションが通るようにするHotfix
EditUser.phpにbeforeValidateメソッドとbeforeCreateメソッドを追記します。

```app/Filament/Resources/UserResource/Page/EditUser.php
<?php

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Hash;


class EditUser extends EditRecord
{
    public static $resource = UserResource::class;

    public function beforeValidate()
    {
        $this->record->makeVisible(["password"]);
    }

    public function beforeSave()
    {
        if ($this->record->password) {
            $this->record->password = Hash::make($this->record->password);
        } else {
            unset($this->record->password);
        }

        unset($this->record->passwordConfirmation);
        $this->record->makeHidden(["password"]);
    }
}
```
passwordがhiddenだとどうやらバリデーションがコケるようなので、passwordを一旦Visibleにして保存前にhiddenに戻しています。なぜバリデーションが通らないのかは調査不足なのですが、とりあえずこれで動いているのでよしとします。もしご存知の方いらっしゃったらコメントいただけますと嬉しいです。

# 最後に
とりあえず爆速で管理画面を構築して、基本的なCRUDを実装しました。そのほかにも色々と機能があるので触ってみてください。

以上、HappyなLaravelライフを🎉