Filamentで開発1(管理画面の作成)

概要

タイピング大戦争(仮)のサーバーサイドを開発する。

  • ユーザーやギルドなどの管理画面

下記で環境構築しておくこと。

Userモデル

Userモデルの作成

デフォルトで作成されているので。必要なし。

管理画面に項目を追加

ユーザモデルは新規登録時のメールアドレスのユニーク制限やパスワードのハッシュ化等、色々と考えるべきことが多いらしい。下記を参考にした。

リソースの作成

php artisan make:filament-resource User

以下のファイルが作成される。

  • app/Filament/Resources/UserResource.php
  • app/Filament/Resources/UserResource/Pages/CreateUser.php
  • app/Filament/Resources/UserResource/Pages/EditUser.php
  • app/Filament/Resources/UserResource/Pages/ListUsers.php

リソースファイルの編集

作成されたFilamentのリソースファイルを下記のように変更する。

use Illuminate\Support\Facades\Hash; // 追加

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                //名前
                Forms\Components\TextInput::make('name')->required()->label('名前'),

                //メアド:編集保存時にメアドのユニーク制限でエラーにならないようにignoreRecordをtrueにする
                Forms\Components\TextInput::make('email')->required()->label('メールアドレス')
                    ->unique(ignoreRecord: true),

                //パスワード:パスワードのハッシュ化や、編集時に都度再入力を求められないようにdehydrateを使う
                Forms\Components\TextInput::make('password')
                    ->password()
                    ->dehydrateStateUsing(fn ($state) => Hash::make($state))
                    ->dehydrated(fn ($state) => filled($state))
                    ->required(fn (string $context): bool => $context === 'create')
                    ->same('password_confirmation')
                    ->label('パスワード'),

                //パスワード確認フィールド
                Forms\Components\TextInput::make('password_confirmation')
                    ->password()
                    ->required(fn (string $context): bool => $context === 'create')
                    ->dehydrated(false)
                    ->label('パスワード確認')
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
            Tables\Columns\TextColumn::make('name')->label('名前'),
            Tables\Columns\TextColumn::make('email')->label('メールアドレス'),
            ])
            // ~~~~~中略~~~~~
    }

Guildモデル

Guildモデルの作成

マイグレーションファイルとモデルファイルの作成

php artisan make:model -m Guild

以下のファイルが生成される

  • app\Models\Guild.php
  • database\migrations\2023_08_06_053348_create_guilds_table.php

マイグレーションファイルの編集

    public function up(): void
    {
        Schema::create('guilds', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique(); // ギルド名が一意であることを保証
            $table->integer('level')->default(1); // 初期値は1
            $table->timestamps();
        });
    }

マイグレーション実行

php artisan migrate

モデルファイルの編集(Guildモデル)

  • nameとlevelを追加
  • リレーションの設定を追加
    GuildモデルがUserモデルと1対多(hasMany)の関係にあることを定義
class Guild extends Model
{
    use HasFactory;

    // フィールド定義
    protected $fillable = ['name', 'level'];

    // リレーション
    public function users()
    {
        return $this->hasMany(User::class);
    }
}

モデルファイルの編集(Userモデル)

  • リレーションの設定を追加
    UserモデルがGuildモデルと1対1(belongsTo)の関係にあることを定義
class User extends Authenticatable
{
    // ~~~~~中略~~~~~

    protected $fillable = [
        'name',
        'email',
        'password',
        'guild_id',
    ];

    // ~~~~~中略~~~~~
    
    // リレーション
    public function guild()
    {
        return $this->belongsTo(Guild::class);
    }
}

管理画面に項目を追加

リソースの作成

php artisan make:filament-resource Guild

下記のファイルが作成される。

  • app\Filament\Resources\GuildResource.php
  • app\Filament\Resources\GuildResource\Pages\CreateGuild.php
  • app\Filament\Resources\GuildResource\Pages\EditGuild.php
  • app\Filament\Resources\GuildResource\Pages\ListGuilds.php

リソースファイルの編集(Guild)

class GuildResource extends Resource
{
    protected static ?string $model = Guild::class;

    protected static ?string $navigationIcon = 'heroicon-o-collection';

    protected static ?string $modelLabel = 'ギルド'; //モデル名を追加(日本語)

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                // ここに編集したい項目を追加する
                Forms\Components\TextInput::make('name')
                    ->required()
                    ->label('ギルド名')
                    ->hint("ギルド名の入力"),
                Forms\Components\TextInput::make('level')
                    ->numeric()
                    ->minValue(1)
                    ->maxValue(100)
                    ->required()
                    ->label('レベル')
                    ->hint("レベルの入力"),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                // ここに表示したい項目を追加する
                Tables\Columns\TextColumn::make('name')
                    ->searchable()
                    ->sortable()
                    ->label('ギルド名'),
                Tables\Columns\TextColumn::make('level')
                    ->sortable()
                    ->label('ギルドレベル'),
            ])

        // ~~~~~中略~~~~~

    }

リソースファイルの編集(User)

リレーションするのでUser側も修正が必要になる。

// ~~~~~中略~~~~~
use App\Models\Guild;
// ~~~~~中略~~~~~

    public static function form(Form $form): Form
    {
        // 全てのギルドを取得
        $guilds = Guild::all()->pluck('name', 'id')->toArray();

        return $form
            ->schema([
                // ギルド
                Forms\Components\Select::make('guild_id')
                    ->required()
                    ->options($guilds)
                    ->label('ギルド'),
                // ~~~~~中略~~~~~
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('name')->label('名前'),
                Tables\Columns\TextColumn::make('email')->label('メールアドレス'),
                Tables\Columns\TextColumn::make('guild.name')->label('ギルド'),
            ])
        // ~~~~~中略~~~~~
    }

マイグレーションファイルの作成(ユーザーテーブル修正用)

userテーブルにguild_idを追加するためにマイグレーションファイルを作成。

php artisan make:migration add_column_to_User_table --table=users

マイグレーションファイルの編集

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->foreignId('guild_id')
                ->nullable() // すでに作成済みのユーザーがあってもエラーにならないように
                ->constrained('guilds')
                ->onDelete('set null') // 所属ギルドが消えたら自動でnullに替わる
                ->unique();
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropForeign(['guild_id']); // 外部キーの制約を削除
            $table->dropColumn('guild_id');    // ギルドIDのカラムを削除
        });
    }
};

マイグレーション実行

php artisan migrate

Score、Season、Stageモデル

モデルの作成

マイグレーションファイルとモデルファイルの作成

ScoreはSeasonやStageを参照するためマイグレーションの順番を最後にしたいので、最後に作成している。

php artisan make:model -m Season
php artisan make:model -m Stage
php artisan make:model -m Score

以下のファイルが生成される

  • app\Models\Season.php
  • app\Models\Stage.php
  • app\Models\Score.php
  • database\migrations\2023_08_15_003220_create_seasons_table.php
  • database\migrations\2023_08_15_003222_create_stages_table.php
  • database\migrations\2023_08_15_003223_create_scores_table.php

マイグレーションファイルの編集

    public function up(): void
    {
        Schema::create('seasons', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique(); // シーズン名が一意であることを保証
            $table->date('start_date')->nullable();
            $table->date('end_date')->nullable();
            $table->timestamps();
        });
    }
    public function up(): void
    {
        Schema::create('stages', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique(); // ステージ名が一意であることを保証
            $table->timestamps();
        });
    }
    public function up(): void
    {
        Schema::create('scores', function (Blueprint $table) {
            $table->id();
            $table->foreignId('season_id')->nullable()->constrained('seasons')->onDelete('set null');
            $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null');
            $table->foreignId('guild_id')->nullable()->constrained('guilds')->onDelete('set null');
            $table->foreignId('stage_id')->nullable()->constrained('stages')->onDelete('set null');
            $table->integer('score');
            $table->integer('correct_count');
            $table->integer('incorrect_count');
            $table->integer('time_span');
            $table->ipAddress('ip_address');
            $table->timestamps();
        });
    }

マイグレーション実行

php artisan migrate

モデルファイルの編集(Season、Stageモデル)

  • 項目の追加
  • リレーションの設定を追加
    SeasonStageモデルがScoreモデルと1対多(hasMany)の関係にあることを定義
    // フィールド定義
    protected $fillable = ['name', 'start_date', 'end_date'];

    // リレーション
    public function users()
    {
        return $this->hasMany(Score::class);
    }
    // フィールド定義
    protected $fillable = ['name'];

    // リレーション
    public function users()
    {
        return $this->hasMany(Score::class);
    }

モデルファイルの編集(Scoreモデル)

  • リレーションの設定を追加
    UserモデルがGuildモデルと1対1(belongsTo)の関係にあることを定義
    // フィールド定義
    protected $fillable = [
        'season_id',
        'user_id',
        'guild_id',
        'stage_id',
        'score',
        'correct_count',
        'incorrect_count',
        'time_span',
        'ip_address'
    ];

    // リレーション
    public function season()
    {
        return $this->belongsTo(Season::class);
    }
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    public function guild()
    {
        return $this->belongsTo(Guild::class);
    }
    public function stage()
    {
        return $this->belongsTo(Stage::class);
    }

管理画面に項目を追加

リソースの作成

php artisan make:filament-resource Season
php artisan make:filament-resource Stage
php artisan make:filament-resource Score

下記のファイルが作成される。

  • app\Filament\Resources\SeasonResource.php
  • app\Filament\Resources\StageResource.php
  • app\Filament\Resources\ScoreResource.php
  • app\Filament\Resources\SeasonResource\Pages\CreateSeason.php
  • app\Filament\Resources\SeasonResource\Pages\EditSeason.php
  • app\Filament\Resources\SeasonResource\Pages\ListSeasons.php
  • app\Filament\Resources\StageResource\Pages\CreateStage.php
  • app\Filament\Resources\StageResource\Pages\EditStage.php
  • app\Filament\Resources\StageResource\Pages\ListStages.php
  • app\Filament\Resources\ScoreResource\Pages\CreateScore.php
  • app\Filament\Resources\ScoreResource\Pages\EditScore.php
  • app\Filament\Resources\ScoreResource\Pages\ListScores.php

リソースファイルの編集(Season)

class SeasonResource extends Resource
{
    // ~~~~~中略~~~~~
    protected static ?string $navigationIcon = 'heroicon-o-square-3-stack-3d';
    protected static ?string $modelLabel = 'シーズン';
    protected static ?string $navigationGroup = '管理';
    protected static ?int $navigationSort = 4;

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\TextInput::make('name')
                    ->required()
                    ->label('ステージ名')
                    ->hint("ステージ名の入力"),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('id')
                    ->searchable()
                    ->sortable()
                    ->label('id'),
                Tables\Columns\TextColumn::make('name')
                    ->searchable()
                    ->sortable()
                    ->label('ステージ名'),
            ])

リソースファイルの編集(Stage)

class StageResource extends Resource
{
    // ~~~~~中略~~~~~
    protected static ?string $navigationIcon = 'heroicon-o-flag';
    protected static ?string $modelLabel = 'ステージ';
    protected static ?string $navigationGroup = '管理';
    protected static ?int $navigationSort = 5;

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\TextInput::make('name')
                    ->required()
                    ->label('ステージ名')
                    ->hint("ステージ名の入力"),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('id')
                    ->searchable()
                    ->sortable()
                    ->label('id'),
                Tables\Columns\TextColumn::make('name')
                    ->searchable()
                    ->sortable()
                    ->label('ステージ名'),
            ])

リソースファイルの編集(Score)

// ~~~~~中略~~~~~
use App\Models\User;
use App\Models\Season;
use App\Models\Stage;
// ~~~~~中略~~~~~

class StageResource extends Resource
{
    // ~~~~~中略~~~~~
    protected static ?string $model = Score::class;
    protected static ?string $navigationIcon = 'heroicon-o-square-3-stack-3d';
    protected static ?string $modelLabel = 'スコア';

    public static function form(Form $form): Form
    {
        // 項目取得
        $users = User::all()->pluck('name', 'id')->toArray();
        $seasons = Season::all()->pluck('name', 'id')->toArray();
        $stages = Stage::all()->pluck('name', 'id')->toArray();

        return $form
            ->schema([
                Forms\Components\Select::make('user_id')
                    ->required()
                    ->options($users)
                    ->label('ユーザー'),
                Forms\Components\TextInput::make('score')
                    ->required()
                    ->label('スコア')
                    ->hint("スコアの入力"),
                Forms\Components\Select::make('season_id')
                    ->options($seasons)
                    ->label('シーズン'),
                Forms\Components\Select::make('stage_id')
                    ->options($stages)
                    ->label('ステージ'),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('user.name')->label('名前'),
                Tables\Columns\TextColumn::make('score')->label('スコア'),
                Tables\Columns\TextColumn::make('guild.name')->label('ギルド'),
                Tables\Columns\TextColumn::make('season.name')->label('シーズン'),
                Tables\Columns\TextColumn::make('stage.name')->label('ステージ'),
                Tables\Columns\TextColumn::make('created_at')->label('記録時刻'),
            ])

こまかいカスタム

Filamentメニューの日本語化

爆速CRUD管理画面作成パッケージ:Filamentの使い方 (zenn.dev)

    'locale' => 'ja',

データ作成/更新時にリダイレクト先を変えたい

データ作成/更新時にリダイレクト先を変えたい

データ作成時のリダイレクト先変更方法

class CreatePost extends CreateRecord
{

    // ~~~~~中略~~~~~

    // 完了後にリストに戻る
    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }
}

データ更新時のリダイレクト先変更方法

class EditPost extends EditRecord
{

    // ~~~~~中略~~~~~

    // 完了後にリストに戻る
    protected function getRedirectUrl(): string
    {
        return $this->getResource()::getUrl('index');
    }
}

表示されているモデル名を好きな名前に変えたい

表示されているモデル名を好きな名前に変えたい

表示されているモデル名を好きな名前に変えたい

class PostResource extends Resource
{
    protected static ?string $model = Post::class;

    protected static ?string $navigationIcon = 'heroicon-o-collection';

    protected static ?string $modelLabel = 'ブログ記事'; //ここを追加

    public static function form(Form $form): Form

コメントする