mcpField
L
laravel-models

laravel-models

github.com/leeovery開発4338 回インストール31 週間前に更新
リポジトリ

laravel-models について

laravel-modelsは、Laravel Eloquentモデルの規約をまとめた開発スキルです。Laravel開発者向けに、リレーション、キャスト、カスタムクエリビルダー、ライフサイクルフックの構成を示し、ビジネスロジックはActionsに分ける方針を説明します。

主な機能

  1. 01カスタムクエリビルダーの指針
  2. 02リレーションとキャストのパターン
  3. 03アクセサーとミューテーター
  4. 04オブザーバーによるフック処理
  5. 05ルートバインディングと論理削除

活用シーン

  1. 01新しいEloquentモデルの規約策定
  2. 02関連レコードと型付き属性の設計
  3. 03モデルからビジネスロジックを分離
  4. 04ルートバインディングと論理削除の設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/leeovery/claude-laravel --skill laravel-models

インストール後、Claude Code で/laravel-modelsを入力して読み込まれたか確認します。

同梱ファイルreferences/HasUuid.phpreferences/query-builders.md

詳細

Laravel モデル

モデルは、データベーステーブルとドメインエンティティを表します。

関連ガイド:

  • クエリビルダー - カスタムクエリビルダー(スコープではありません)
  • アクション - アクションにビジネスロジックを記述します
  • DTO - モデルの JSON カラムを DTO にキャストします

基本方針

モデルでは、次のようにします。

  • ローカルスコープではなく、カスタムクエリビルダーを使用します。クエリビルダーを参照してください。
  • リレーションを定義します
  • キャストを定義します
  • シンプルなアクセサー/ミューテーターを含めます
  • ビジネスロジックは含めません(ビジネスロジックはアクションに記述します)

モデルの基本構造

<?php

declare(strict_types=1);

namespace App\Models;

use App\Builders\OrderBuilder;
use App\Enums\OrderStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Order extends Model
{
    use HasFactory;

    protected function casts(): array
    {
        return [
            'status' => OrderStatus::class,
            'total' => 'integer',
        ];
    }

    // Custom Query Builder
    public function newEloquentBuilder($query): OrderBuilder
    {
        return new OrderBuilder($query);
    }

    // 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
    ];
}

利用可能なキャスト:

  • 'integer'、'real'、'float'、'double'
  • 'string'、'boolean'
  • 'array'、'json'、'object'、'collection'
  • 'date'、'datetime'、'immutable_date'、'immutable_datetime'
  • 'timestamp'
  • 'encrypted'、'encrypted:array'、'encrypted:collection'、'encrypted:json'、'encrypted:object'
  • カスタムキャストクラス
  • Enum クラス
  • DTO クラス

リレーション

BelongsTo
public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

public function customer(): BelongsTo
{
    return $this->belongsTo(Customer::class, 'customer_id', 'id');
}
HasMany
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

public function items(): HasMany
{
    return $this->hasMany(OrderItem::class);
}
HasOne
public function profile(): HasOne
{
    return $this->hasOne(UserProfile::class);
}
BelongsToMany
public function roles(): BelongsToMany
{
    return $this->belongsToMany(Role::class)
        ->withTimestamps()
        ->withPivot('assigned_at');
}
HasManyThrough
public function deployments(): HasManyThrough
{
    return $this->hasManyThrough(Deployment::class, Environment::class);
}
MorphTo / MorphMany
// MorphTo
public function commentable(): MorphTo
{
    return $this->morphTo();
}

// MorphMany
public function comments(): MorphMany
{
    return $this->morphMany(Comment::class, 'commentable');
}

アクセサーとミューテーター

アクセサー(取得)
use Illuminate\Database\Eloquent\Casts\Attribute;

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn () => "{$this->first_name} {$this->last_name}",
    );
}

// Usage
$user->full_name; // "John Doe"
ミューテーター(設定)
protected function password(): Attribute
{
    return Attribute::make(
        set: fn (string $value) => bcrypt($value),
    );
}

// Usage
$user->password = 'secret'; // Automatically hashed
取得と設定の両方
protected function email(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => strtolower($value),
        set: fn (string $value) => strtolower(trim($value)),
    );
}

モデルメソッド

シンプルなヘルパーメソッドは使用できます。

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 配列を管理する必要がありません
  • 柔軟性:すべての属性を一括代入できます
  • 信頼性:フォームリクエストとアクションで適切にバリデーションを行えば、一括代入の保護は不要です
  • モデルの簡潔さ:定型コードを減らせます

重要: アクション/モデルに渡す前に、必ずフォームリクエストで入力をバリデーションしてください。

タイムスタンプ

// Disable timestamps
public $timestamps = false;

// Custom timestamp columns
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'updated_date';

ソフトデリート

use Illuminate\Database\Eloquent\SoftDeletes;

class Order extends Model
{
    use SoftDeletes;
}

使用方法:

$order->delete();      // Soft delete
$order->forceDelete(); // Permanent delete
$order->restore();     // Restore

Order::withTrashed()->find($id);
Order::onlyTrashed()->get();

コレクション

クエリ結果はコレクションとして返されます。

$orders = Order::all(); // Illuminate\Database\Eloquent\Collection

$orders->filter(fn($order) => $order->isPending());
$orders->map(fn($order) => $order->total);
$orders->sum('total');

モデルの構成

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);
});

まとめ

モデルでは、次のようにします。

  • AppServiceProvider で Model::unguard() を使い、グローバルにガードを解除します
  • 構造(キャスト、リレーション)を定義します
  • スコープではなく、カスタムクエリビルダーを使用します
  • シンプルなヘルパーメソッドを使用します
  • ライフサイクルフックにはオブザーバーを使用します

モデルでは、次のことをしないでください。

  • $fillable または $guarded プロパティを使用する
  • ビジネスロジックを含める(アクションを使用してください)
  • 複雑なメソッドを持たせる(アクションを使用してください)
  • ローカルスコープを使用する(カスタムビルダーを使用してください)

コメント