laravel-models について
laravel-modelsは、Laravel開発者向けのEloquentモデル設計スキルです。モデルの責務やキャスト、リレーション、ルートバインディングを扱い、ビジネスロジックをActionsに分離します。
主な機能
- 01モデルの責務と構成
- 02カスタムクエリビルダーとリレーション
- 03型安全な属性キャスト
- 04オブザーバーと再利用可能なTrait
- 05ルートモデルバインディングの指針
活用シーン
- 01アプリケーションのEloquentモデル設計
- 02ビジネスロジックの配置先の判断
- 03キャストとモデルのライフサイクルフックの設定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ leeovery/ agentic-skills --skill laravel-modelsインストール後、Claude Code で/laravel-modelsを入力して読み込まれたか確認します。
同梱ファイルreferences/HasUuid.phpreferences/query-builders.md
詳細
Laravel モデル
モデルはデータベースのテーブルとドメインエンティティを表します。
関連ガイド:
基本方針
モデルでは、次のようにします。
- ローカルスコープではなくカスタムクエリビルダーを使用します。クエリビルダーを参照してください
- リレーションシップを定義します
- キャストを定義します
- シンプルなアクセサー/ミューテーターを記述します
- ビジネスロジックは記述しません(ビジネスロジックはアクションに記述します)
- 利用可能な場合は、プロパティ/メソッドよりもPHP 属性を優先します(
#[UseEloquentBuilder]は Laravel 12 以降、#[Table]、#[ObservedBy]、#[UsePolicy]、#[UseFactory]などは Laravel 13 以降)
基本的なモデル構造
<?php
declare(strict_types=1);
namespace App\Models;
use App\Builders\OrderBuilder;
use App\Enums\OrderStatus;
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[UseEloquentBuilder(OrderBuilder::class)]
class Order extends Model
{
use HasFactory;
protected function casts(): array
{
return [
'status' => OrderStatus::class,
'total' => 'integer',
];
}
// Relationships
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
}
キャスト
型安全性を確保するため、キャストを定義します。
protected function casts(): array
{
return [
'status' => OrderStatus::class, // Enum
'total' => 'integer', // Integer
'is_paid' => 'boolean', // Boolean
'metadata' => OrderMetadataData::class, // DTO
'completed_at' => 'datetime', // Carbon
'tags' => 'array', // JSON array
];
}
モデルのメソッド
シンプルなヘルパーメソッドは使用できます。
class Order extends Model
{
public function isPending(): bool
{
return $this->status === OrderStatus::Pending;
}
public function isCompleted(): bool
{
return $this->status === OrderStatus::Completed;
}
public function canBeCancelled(): bool
{
return $this->isPending() || $this->status === OrderStatus::Processing;
}
}
ただし、ビジネスロジックは記述しません。
// ❌ Bad - business logic in model
class Order extends Model
{
public function cancel(): void
{
DB::transaction(function () {
$this->update(['status' => OrderStatus::Cancelled]);
$this->refundPayment();
$this->notifyCustomer();
});
}
}
// ✅ Good - business logic in action
class CancelOrderAction
{
public function __invoke(Order $order): Order
{
return DB::transaction(function () use ($order) {
$order->update(['status' => OrderStatus::Cancelled]);
resolve(RefundPaymentAction::class)($order);
resolve(NotifyCustomerAction::class)($order);
return $order;
});
}
}
モデルオブザーバー
モデルのライフサイクルフックには、次のようにします。
<?php
declare(strict_types=1);
namespace App\Observers;
use App\Models\Order;
use Illuminate\Support\Str;
class OrderObserver
{
public function creating(Order $order): void
{
if (! $order->uuid) {
$order->uuid = Str::uuid();
}
}
public function created(Order $order): void
{
// Dispatch event, queue job, etc.
}
public function updating(Order $order): void
{
// Before update
}
public function updated(Order $order): void
{
// After update
}
public function deleted(Order $order): void
{
// After delete
}
}
AppServiceProvider に登録します。
use App\Models\Order;
use App\Observers\OrderObserver;
public function boot(): void
{
Order::observe(OrderObserver::class);
}
モデルの関心事(トレイト)
再利用可能な振る舞いを抽出します。
モデルで使用します。
class Order extends Model
{
use HasUuid;
}
ルートモデルバインディング
暗黙的なバインディング
// Route
Route::get('/orders/{order}', [OrderController::class, 'show']);
// Controller - automatically receives Order model
public function show(Order $order) { }
カスタムキー
Route::get('/orders/{order:uuid}', [OrderController::class, 'show']);
カスタム解決
public function resolveRouteBinding($value, $field = null)
{
return $this->where($field ?? 'id', $value)
->where('is_active', true)
->firstOrFail();
}
一括代入の保護
すべてのモデルで、デフォルトではガードを解除します。
AppServiceProvider の設定
AppServiceProvider::boot() メソッドで、Model::unguard() を呼び出します。
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::unguard();
}
}
モデルの設定
モデルで $fillable または $guarded プロパティを使用しないでください。
// ✅ Good - no fillable/guarded
class Order extends Model
{
protected function casts(): array
{
return [
'status' => OrderStatus::class,
];
}
}
// ❌ Bad - don't use fillable
class Order extends Model
{
protected $fillable = ['name', 'email'];
}
// ❌ Bad - don't use guarded
class Order extends Model
{
protected $guarded = [];
}
ガードを解除する理由
- シンプルさ:fillable/guarded 配列を保守する必要がありません
- 柔軟性:すべての属性を一括代入できます
- 信頼性:Form Request とアクションで適切にバリデーションすれば、一括代入の保護は冗長です
- よりすっきりしたモデル:定型コードを減らせます
重要: アクション/モデルに渡す前に、必ず Form Request で入力をバリデーションしてください。
モデルの構成
app/Models/
├── Order.php
├── User.php
├── Concerns/
│ ├── HasUuid.php
│ ├── BelongsToTenant.php
│ └── Searchable.php
└── Contracts/
└── Searchable.php
モデルのテスト
it('can mass assign attributes', function () {
$order = Order::create([
'user_id' => 1,
'status' => 'pending',
'total' => 1000,
'notes' => 'Test order',
]);
expect($order->user_id)->toBe(1)
->and($order->total)->toBe(1000);
});
it('casts status to enum', function () {
$order = Order::factory()->create(['status' => 'pending']);
expect($order->status)->toBeInstanceOf(OrderStatus::class);
});
it('has user relationship', function () {
$order = Order::factory()->create();
expect($order->user)->toBeInstanceOf(User::class);
});
コメント
Markdown 対応