目次
概要
タイピング大戦争(仮)のサーバーサイドを開発する。
- ユーザーやギルドなどの管理画面
下記で環境構築しておくこと。
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 migrateScore、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モデル)
- 項目の追加
- リレーションの設定を追加
Season、Stageモデルが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