【保存版】Laravel Fillableの完全ガイド:セキュアなモデル実装の7つのベストプラクティス

Laravel Fillableとは:Mass Assignmentの基礎知識

Laravelでモデルを扱う際に避けて通れない重要な概念が「Fillable」です。本記事では、Mass Assignment脆弱性の観点から、なぜFillableが必要なのか、そしてどのように使用すべきかを詳しく解説していきます。

Mass Assignmentの脆弱性から学ぶFillableの重要性

Mass Assignment脆弱性とは、ユーザーが予期せぬデータをモデルに一括代入できてしまう脆弱性です。この問題が実際にどのような影響を及ぼすのか、具体例で見てみましょう。

// 脆弱性のある例
class User extends Model
{
    // Fillableの設定がない場合、全てのカラムが代入可能
}

// コントローラーでの処理
public function store(Request $request)
{
    $user = User::create($request->all());
    return redirect()->route('users.index');
}

上記のコードでは、以下のような問題が発生する可能性があります:

  1. 悪意のあるユーザーが管理者権限を付与
POST /users
{
    "name": "攻撃者",
    "email": "attacker@example.com",
    "is_admin": 1  // 本来設定できないはずの管理者フラグ
}
  1. 重要な情報の改ざん
POST /users
{
    "balance": 999999,  // 残高を不正に操作
    "verified": true    // 認証状態を不正に変更
}

このような脆弱性を防ぐために、Laravelは$fillableプロパティを提供しています。

// 適切な実装例
class User extends Model
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
}

GuardedとFillableの違いを理解する

Laravelでは、Mass Assignment保護のために$fillable$guardedという2つの方法を提供しています。この2つは相反する概念で、以下のような特徴があります:

項目FillableGuarded
定義の意味ホワイトリスト(許可するカラムを指定)ブラックリスト(禁止するカラムを指定)
デフォルト状態指定したカラムのみ代入可能指定したカラム以外を代入可能
セキュリティ観点より安全(明示的な許可が必要)より危険(うっかり漏れる可能性)
コード例protected $fillable = ['name'];protected $guarded = ['id'];

セキュリティのベストプラクティスとしては、$fillableの使用が推奨されます。その理由は:

  1. 明示的な許可が必要なため、新しいカラムを追加した際の安全性が高い
  2. 意図しないカラムの更新を確実に防げる
  3. コードレビューで許可されているカラムが一目で分かる
// 推奨される実装パターン
class Article extends Model
{
    protected $fillable = [
        'title',
        'content',
        'published_at',
    ];

    // システムで管理する値は$fillableに含めない
    // - id
    // - created_at
    // - updated_at
    // - deleted_at
}

基礎知識として重要なのは、$fillableはあくまでMass Assignmentの制御であり、個別の代入は制限されないという点です:

$article = new Article();
$article->status = 'draft';  // $fillableに含まれていなくても代入可能
$article->save();

このような基本的な理解の上に立って、次節では具体的な設定方法とベストプラクティスを見ていきます。

Fillableプロパティの正しい設定方法

プロパティを保護するための基本的な書き方

Fillableプロパティの設定には、いくつかの重要なパターンと注意点があります。以下に、基本的な実装パターンとその説明を示します。

class Product extends Model
{
    /**
     * Mass Assignmentで代入を許可する属性
     * 
     * @var array<int, string>
     */
    protected $fillable = [
        'name',           // 商品名
        'description',    // 商品説明
        'price',         // 価格
        'stock',         // 在庫数
        'category_id',   // カテゴリID
    ];

    // 以下のような重要な属性は$fillableに含めない
    // - id: 主キー
    // - created_at: 作成日時
    // - updated_at: 更新日時
    // - deleted_at: 削除日時
    // - total_sales: 売上集計値
}

設定時の重要なポイント:

  1. 型アノテーションの明記
  • PHPDocで@var array<int, string>を指定
  • 静的解析ツールでのチェックが可能に
  1. 命名規則の統一
  • スネークケースを使用(Laravelの規約に準拠)
  • テーブルのカラム名と一致させる
  1. コメントの活用
  • 各属性の意味を明記
  • 除外した属性の理由を記載

複数のプロパティを一括で設定する効率的な方法

大規模なモデルや、状況によって許可する属性が変わる場合の効率的な設定方法を紹介します。

class Order extends Model
{
    /**
     * 基本の許可属性
     */
    private array $baseFields = [
        'customer_name',
        'email',
        'phone',
    ];

    /**
     * 配送関連の属性
     */
    private array $shippingFields = [
        'shipping_address',
        'shipping_method',
        'tracking_number',
    ];

    /**
     * 支払い関連の属性
     */
    private array $paymentFields = [
        'payment_method',
        'payment_status',
    ];

    /**
     * コンストラクタでFillableを動的に設定
     */
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);

        // 基本フィールドは常に許可
        $allowedFields = $this->baseFields;

        // 状況に応じて追加のフィールドを許可
        if (config('app.enable_shipping')) {
            $allowedFields = array_merge($allowedFields, $this->shippingFields);
        }

        if (config('app.enable_payment')) {
            $allowedFields = array_merge($allowedFields, $this->paymentFields);
        }

        $this->fillable = $allowedFields;
    }

    /**
     * 特定の状況で一時的にFillableを変更
     */
    public function temporarilyFillable(array $attributes, callable $callback)
    {
        $originalFillable = $this->fillable;
        $this->fillable = array_merge($this->fillable, $attributes);

        try {
            $result = $callback($this);
            $this->fillable = $originalFillable;
            return $result;
        } catch (\Exception $e) {
            $this->fillable = $originalFillable;
            throw $e;
        }
    }
}

効率的な設定のためのテクニック:

  1. 属性のグループ化
   // コントローラーでの使用例
   $order = new Order();
   $order->temporarilyFillable(['special_note'], function($order) use ($request) {
       return $order->create($request->all());
   });
  1. 環境による制御
   // config/app.php
   return [
       'enable_shipping' => env('ENABLE_SHIPPING', true),
       'enable_payment' => env('ENABLE_PAYMENT', true),
   ];
  1. バッチ処理での一時的な許可
   // バッチ処理での例
   $order->temporarilyFillable(['imported_at', 'import_batch_id'], function($order) {
       return $order->updateFromBatch($batchData);
   });

これらの設定方法を適切に組み合わせることで、柔軟かつセキュアなモデル実装が可能になります。次のセクションでは、これらの基本を踏まえた上での具体的なベストプラクティスを見ていきます。

セキュアなモデル実装のベストプラクティス

セキュアで保守性の高いLaravelモデルを実装するための7つのベストプラクティスを、具体的なコード例と共に解説します。

ステップ 1: 必要なプロパティだけを許可する

最小権限の原則に基づき、必要最小限のプロパティのみを許可します。

class Article extends Model
{
    /**
     * 許可する属性を機能ごとにグループ化
     */
    private const CONTENT_FIELDS = [
        'title',
        'body',
        'summary',
    ];

    private const META_FIELDS = [
        'slug',
        'published_at',
        'featured_image',
    ];

    /**
     * ユーザーの役割に応じて許可する属性を制御
     */
    public function getFillableFields(): array
    {
        $fields = self::CONTENT_FIELDS;

        if (auth()->user()->can('manage_article_meta')) {
            $fields = array_merge($fields, self::META_FIELDS);
        }

        return $fields;
    }

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
        $this->fillable = $this->getFillableFields();
    }
}

ステップ 2: センシティブなプロパティを特定する

セキュリティ上重要な属性を明確に定義し、保護します。

class User extends Model
{
    /**
     * 決して一括代入を許可してはいけない属性
     */
    private const SENSITIVE_FIELDS = [
        'password_hash',
        'remember_token',
        'api_key',
        'is_admin',
        'email_verified_at',
    ];

    /**
     * センシティブな属性のバリデーション
     */
    protected static function boot()
    {
        parent::boot();

        static::saving(function ($user) {
            $changed = array_intersect(
                array_keys($user->getDirty()),
                self::SENSITIVE_FIELDS
            );

            if (!empty($changed)) {
                \Log::warning('Attempt to modify sensitive fields: ' . implode(', ', $changed));
                throw new \RuntimeException('Cannot modify sensitive fields directly');
            }
        });
    }

    /**
     * センシティブな属性を安全に更新するメソッド
     */
    public function updatePassword(string $newPassword): void
    {
        $this->password_hash = Hash::make($newPassword);
        $this->save();
    }
}

ステップ 3: 動的なFillableプロパティの実現

状況に応じて柔軟にFillableを制御する実装パターンです。

class Product extends Model
{
    use HasFactory;

    /**
     * 動的なFillable制御のためのトレイト
     */
    trait DynamicFillable
    {
        private array $originalFillable = [];

        public function initializeDynamicFillable()
        {
            $this->originalFillable = $this->fillable;
        }

        public function setContextFillable(string $context)
        {
            $this->fillable = match($context) {
                'admin' => array_merge(
                    $this->originalFillable,
                    ['price', 'stock', 'status']
                ),
                'api' => array_merge(
                    $this->originalFillable,
                    ['external_id', 'api_status']
                ),
                default => $this->originalFillable,
            };
        }
    }
}

ステップ 4: バリデーションとの組み合わせ

Fillableとバリデーションを連携させ、データの整合性を保護します。

class ProjectRequest extends FormRequest
{
    /**
     * バリデーションルールとFillableの同期を維持
     */
    public function rules(): array
    {
        return array_fill_keys(
            (new Project)->getFillable(),
            'required|string|max:255'
        ) + [
            'start_date' => 'required|date',
            'end_date' => 'required|date|after:start_date',
            'budget' => 'required|numeric|min:0',
        ];
    }

    /**
     * カスタムバリデータとの連携
     */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            $project = new Project($this->validated());
            if (!$project->isValidBudgetAllocation()) {
                $validator->errors()->add(
                    'budget',
                    'Invalid budget allocation'
                );
            }
        });
    }
}

ステップ 5: リレーション時の注意点

リレーションを含むモデルでのFillable設定について解説します。

class Post extends Model
{
    protected $fillable = [
        'title',
        'content',
    ];

    /**
     * ネストされた属性の制御
     */
    protected $with = ['tags'];

    /**
     * リレーション更新時の制御
     */
    public function syncTags(array $tags)
    {
        return $this->tags()->sync(
            collect($tags)
                ->filter(fn($tag) => auth()->user()->can('attach_tag', $tag))
                ->pluck('id')
        );
    }

    /**
     * セーフなリレーション更新
     */
    public function safeUpdate(array $attributes)
    {
        DB::transaction(function () use ($attributes) {
            // 基本属性の更新
            $this->fill($attributes)->save();

            // リレーションの更新
            if (isset($attributes['tags'])) {
                $this->syncTags($attributes['tags']);
            }
        });
    }
}

ステップ 6: ポリシーとの連携

Fillableの制御をポリシーと連携させ、より細かな権限管理を実現します。

class ArticlePolicy
{
    /**
     * 属性レベルでの権限チェック
     */
    public function updateField(User $user, Article $article, string $field): bool
    {
        return match($field) {
            'status' => $user->hasRole('editor'),
            'published_at' => $user->hasRole('publisher'),
            default => $user->can('edit_article', $article),
        };
    }
}

class Article extends Model
{
    /**
     * ポリシーを考慮したFillable制御
     */
    public function getFillableForUser(User $user): array
    {
        return collect($this->fillable)
            ->filter(fn($field) => $user->can('updateField', [$this, $field]))
            ->toArray();
    }

    /**
     * セキュアな更新処理
     */
    public function secureUpdate(array $attributes, User $user)
    {
        $fillable = $this->getFillableForUser($user);
        $this->fillable = $fillable;

        return $this->update($attributes);
    }
}

ステップ 7: 正しいテストの実装

Fillableの設定を確実にテストし、セキュリティを担保します。

class ArticleTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Fillableの基本テスト
     */
    public function test_fillable_attributes_are_mass_assignable()
    {
        $article = Article::factory()->create();
        $newData = ['title' => 'New Title', 'content' => 'New Content'];

        $article->fill($newData);
        $this->assertEquals('New Title', $article->title);
    }

    /**
     * センシティブな属性の保護テスト
     */
    public function test_sensitive_attributes_are_protected()
    {
        $article = Article::factory()->create();

        $this->expectException(\Illuminate\Database\Eloquent\MassAssignmentException::class);

        $article->fill(['published_status' => 'approved']);
    }

    /**
     * 動的Fillableのテスト
     */
    public function test_dynamic_fillable_changes_based_on_context()
    {
        $article = Article::factory()->create();
        $user = User::factory()->create(['role' => 'editor']);

        $this->actingAs($user);

        $fillable = $article->getFillableForUser($user);
        $this->assertContains('status', $fillable);

        $regularUser = User::factory()->create(['role' => 'user']);
        $this->actingAs($regularUser);

        $fillable = $article->getFillableForUser($regularUser);
        $this->assertNotContains('status', $fillable);
    }
}

これらの7つのベストプラクティスを適切に組み合わせることで、セキュアで保守性の高いモデル実装が可能になります。次のセクションでは、これらのプラクティスを実際のユースケースに適用する方法を見ていきます。

Fillableのユースケース別実装パターン

実際のプロジェクトで遭遇する代表的なユースケースにおける、Fillableの実装パターンを解説します。

管理画面での柔軟なプロパティ設定

管理画面では、一般ユーザー向けのインターフェースよりも多くの属性を編集可能にする必要があります。

class Product extends Model
{
    use HasFactory;

    /**
     * 基本の編集可能フィールド
     */
    protected $baseFillable = [
        'name',
        'description',
        'price',
        'stock',
    ];

    /**
     * 管理者用の追加フィールド
     */
    protected $adminFillable = [
        'cost_price',
        'supplier_id',
        'tax_category',
        'featured_rank',
        'internal_notes',
    ];

    /**
     * コンストラクタでの初期化
     */
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
        $this->initializeFillable();
    }

    /**
     * 権限に基づいてFillableを初期化
     */
    protected function initializeFillable(): void
    {
        $this->fillable = $this->baseFillable;

        if ($this->isAdminPanel()) {
            $this->fillable = array_merge(
                $this->fillable,
                $this->adminFillable
            );
        }
    }

    /**
     * 管理画面からのアクセスかを判定
     */
    protected function isAdminPanel(): bool
    {
        return request()->segment(1) === 'admin' && 
               auth()->user()?->can('access-admin');
    }

    /**
     * 管理画面用のForm Request
     */
    public function adminUpdateRules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'price' => 'required|numeric|min:0',
            'cost_price' => 'required|numeric|min:0|lt:price',
            'supplier_id' => 'required|exists:suppliers,id',
            'tax_category' => 'required|in:standard,reduced,zero',
            'featured_rank' => 'nullable|integer|min:0',
            'internal_notes' => 'nullable|string',
        ];
    }
}

APIエンドポイントでの利用方法

APIでは、バージョンやクライアントの種類によって異なるFillable設定が必要になることがあります。

class User extends Model
{
    /**
     * API用のトレイト
     */
    trait ApiVersionedFillable
    {
        /**
         * APIバージョン別の許可フィールド
         */
        protected $apiFillable = [
            'v1' => [
                'name',
                'email',
                'preferences',
            ],
            'v2' => [
                'name',
                'email',
                'preferences',
                'notification_settings',
                'timezone',
            ]
        ];

        /**
         * クライアント種別別の追加フィールド
         */
        protected $clientSpecificFields = [
            'mobile' => [
                'device_token',
                'app_version',
            ],
            'web' => [
                'theme_preference',
                'layout_settings',
            ]
        ];

        /**
         * APIコンテキストでのFillable設定
         */
        public function setApiFillable(string $version, string $client = 'web'): void
        {
            $this->fillable = array_merge(
                $this->apiFillable[$version] ?? [],
                $this->clientSpecificFields[$client] ?? []
            );
        }
    }

    /**
     * API用のリソースクラス
     */
    class UserApiResource extends JsonResource
    {
        public function toArray($request): array
        {
            $version = $request->route('version');
            $fields = $this->apiFillable[$version] ?? [];

            return collect(parent::toArray($request))
                ->only($fields)
                ->toArray();
        }
    }
}

複数環境でのプロパティ管理

開発・ステージング・本番環境で異なるFillable設定が必要な場合の実装パターンです。

class Configuration extends Model
{
    /**
     * 環境別の設定管理
     */
    protected $environmentSettings = [
        'local' => [
            'fillable' => [
                'app_name',
                'debug_mode',
                'mail_settings',
                'test_accounts',
                'mock_services',
            ],
            'validation' => [
                'strict' => false,
                'log_level' => 'debug',
            ]
        ],
        'staging' => [
            'fillable' => [
                'app_name',
                'mail_settings',
                'feature_flags',
                'monitoring_settings',
            ],
            'validation' => [
                'strict' => true,
                'log_level' => 'info',
            ]
        ],
        'production' => [
            'fillable' => [
                'app_name',
                'mail_settings',
                'monitoring_settings',
            ],
            'validation' => [
                'strict' => true,
                'log_level' => 'error',
            ]
        ]
    ];

    /**
     * 環境に応じたFillable初期化
     */
    public function initializeEnvironmentFillable(): void
    {
        $env = app()->environment();
        $settings = $this->environmentSettings[$env] ?? 
                   $this->environmentSettings['production'];

        $this->fillable = $settings['fillable'];

        // 環境固有のバリデーション設定
        Config::set('validation.strict', $settings['validation']['strict']);
        Log::setDefaultDriver($settings['validation']['log_level']);
    }

    /**
     * 環境チェック用のミドルウェア
     */
    class EnvironmentConfigurationMiddleware
    {
        public function handle($request, Closure $next)
        {
            if (app()->environment('production')) {
                $sensitiveParams = ['debug_mode', 'test_accounts'];

                if ($request->hasAny($sensitiveParams)) {
                    Log::warning('Attempt to modify sensitive configuration in production', [
                        'ip' => $request->ip(),
                        'user' => auth()->user()->id ?? null,
                        'params' => $request->only($sensitiveParams)
                    ]);

                    abort(403, 'Cannot modify sensitive settings in production');
                }
            }

            return $next($request);
        }
    }
}

これらのパターンは、実際のプロジェクトでの要件に応じて適切にカスタマイズして使用できます。次のセクションでは、Fillable関連でよく遭遇するトラブルとその解決方法について解説します。

よくあるFillable関連のトラブルシューティング

Mass Assignment例外の原因と対処法

Mass Assignment例外は開発中によく遭遇する問題の一つです。主な原因と解決方法を解説します。

1. MassAssignmentException の基本的な対処

// エラーの例
Illuminate\Database\Eloquent\MassAssignmentException: Add [status] to fillable property to allow mass assignment.

// 原因となるコード
class Article extends Model
{
    protected $fillable = ['title', 'content'];
}

$article->update($request->all());  // statusフィールドを含むリクエスト

解決方法1: デバッグモードでの詳細確認

class Article extends Model
{
    /**
     * Mass Assignment例外をデバッグしやすくする
     */
    public function fill(array $attributes)
    {
        try {
            return parent::fill($attributes);
        } catch (\Illuminate\Database\Eloquent\MassAssignmentException $e) {
            logger()->debug('Mass Assignment Debug', [
                'attempted_fields' => array_keys($attributes),
                'fillable_fields' => $this->fillable,
                'guarded_fields' => $this->guarded
            ]);
            throw $e;
        }
    }
}

解決方法2: 安全な属性フィルタリング

class ArticleController extends Controller
{
    public function update(Request $request, Article $article)
    {
        // fillableな属性のみを抽出
        $fillableData = $request->only($article->getFillable());

        // 安全に更新
        $article->update($fillableData);
    }
}

2. ネストされたリレーションでの問題

// エラーパターン
class Post extends Model
{
    protected $fillable = ['title', 'content'];

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

// このように一括更新しようとするとエラー
$post->update([
    'title' => 'New Title',
    'comments' => [
        ['body' => 'New Comment']
    ]
]);

解決方法: カスタムミューテータの使用

class Post extends Model
{
    protected $fillable = ['title', 'content'];

    /**
     * コメントの安全な一括更新
     */
    public function setCommentsAttribute(array $comments)
    {
        // トランザクション内で安全に更新
        DB::transaction(function () use ($comments) {
            // 既存のコメントを取得
            $existingComments = $this->comments->keyBy('id');

            foreach ($comments as $comment) {
                if (isset($comment['id'])) {
                    // 既存コメントの更新
                    $existingComment = $existingComments->get($comment['id']);
                    if ($existingComment) {
                        $existingComment->update(
                            Arr::only($comment, ['body'])
                        );
                    }
                } else {
                    // 新規コメントの作成
                    $this->comments()->create(
                        Arr::only($comment, ['body'])
                    );
                }
            }
        });
    }
}

予期せぬ属性更新の防止方法

データの整合性を保つため、予期せぬ属性更新を防ぐ方法を紹介します。

1. 更新前の属性検証

class Order extends Model
{
    /**
     * 更新前の属性検証
     */
    protected static function boot()
    {
        parent::boot();

        static::updating(function ($order) {
            $dirty = $order->getDirty();

            // ステータス遷移の検証
            if (isset($dirty['status'])) {
                if (!$order->isValidStatusTransition($dirty['status'])) {
                    throw new \InvalidArgumentException(
                        "Invalid status transition from {$order->status} to {$dirty['status']}"
                    );
                }
            }

            // 金額変更の検証
            if (isset($dirty['amount']) && $order->isPaid()) {
                throw new \InvalidArgumentException(
                    "Cannot modify amount of paid order"
                );
            }

            return true;
        });
    }

    /**
     * ステータス遷移の検証
     */
    private function isValidStatusTransition(string $newStatus): bool
    {
        $allowedTransitions = [
            'pending' => ['processing', 'cancelled'],
            'processing' => ['completed', 'failed'],
            'completed' => [],
            'failed' => ['pending'],
            'cancelled' => []
        ];

        return in_array($newStatus, $allowedTransitions[$this->status] ?? []);
    }
}

2. 属性変更の監査ログ

class AuditableFillable extends Model
{
    /**
     * Fillableな属性変更の監査
     */
    public function fill(array $attributes)
    {
        $before = $this->getAttributes();

        parent::fill($attributes);

        $after = $this->getAttributes();
        $changes = array_diff_assoc($after, $before);

        if (!empty($changes)) {
            $this->logAttributeChanges($changes);
        }

        return $this;
    }

    /**
     * 属性変更のログ記録
     */
    protected function logAttributeChanges(array $changes): void
    {
        ActivityLog::create([
            'model_type' => get_class($this),
            'model_id' => $this->getKey(),
            'changes' => $changes,
            'user_id' => auth()->id(),
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent()
        ]);
    }
}

これらのトラブルシューティング手法を適切に実装することで、より安全で保守性の高いアプリケーション開発が可能になります。次のセクションでは、Fillableのより高度な活用テクニックについて解説します。

Fillableの実践的な活用テクニック

条件付きFillableの実装方法

状況に応じて動的にFillableを制御する高度な実装テクニックを紹介します。

class DynamicModel extends Model
{
    /**
     * 条件付きFillable用のトレイト
     */
    trait ConditionalFillable
    {
        /**
         * コンテキスト別のFillable定義
         */
        protected array $contextFillable = [
            'default' => [
                'name',
                'description',
            ],
            'admin' => [
                'internal_code',
                'priority_rank',
            ],
            'api' => [
                'external_id',
                'sync_status',
            ]
        ];

        /**
         * 条件に基づくFillable拡張
         */
        protected array $conditionalFillable = [
            'is_premium' => [
                'premium_features',
                'subscription_details',
            ],
            'has_special_permission' => [
                'restricted_fields',
                'special_notes',
            ]
        ];

        /**
         * 動的なFillable制御
         */
        public function initializeFillable(string $context = 'default'): void
        {
            // 基本のFillable設定
            $this->fillable = $this->contextFillable[$context] ?? 
                             $this->contextFillable['default'];

            // 条件に基づく拡張
            foreach ($this->conditionalFillable as $condition => $fields) {
                if ($this->$condition) {
                    $this->fillable = array_merge($this->fillable, $fields);
                }
            }

            // カスタムポリシーの適用
            $this->applyCustomFillablePolicy();
        }

        /**
         * カスタムポリシーの適用
         */
        protected function applyCustomFillablePolicy(): void
        {
            $user = auth()->user();

            if ($user && method_exists($this, 'getFillableForUser')) {
                $this->fillable = array_intersect(
                    $this->fillable,
                    $this->getFillableForUser($user)
                );
            }
        }
    }

    /**
     * 使用例
     */
    class Product extends DynamicModel
    {
        use ConditionalFillable;

        public function __construct(array $attributes = [])
        {
            parent::__construct($attributes);

            // コンテキストに基づいて初期化
            $context = $this->determineContext();
            $this->initializeFillable($context);
        }

        /**
         * コンテキストの判定
         */
        protected function determineContext(): string
        {
            if (request()->is('api/*')) {
                return 'api';
            }

            if (auth()->user()?->isAdmin()) {
                return 'admin';
            }

            return 'default';
        }
    }
}

グローバルスコープとの併用

Fillableとグローバルスコープを組み合わせた高度な実装パターンです。

/**
 * ソフトデリート + マルチテナント環境での実装例
 */
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        if (auth()->check()) {
            $builder->where('tenant_id', auth()->user()->tenant_id);
        }
    }
}

class TenantAwareFillable extends Model
{
    use SoftDeletes;

    protected static function boot()
    {
        parent::boot();
        static::addGlobalScope(new TenantScope);
    }

    /**
     * テナント別のFillable制御
     */
    protected function initializeTenantFillable()
    {
        if (auth()->check()) {
            $tenant = auth()->user()->tenant;

            // テナント設定に基づくFillable制御
            $this->fillable = array_merge(
                $this->fillable,
                $tenant->allowed_fields ?? []
            );

            // テナント固有の制限の適用
            if ($tenant->restrictions) {
                $this->fillable = array_diff(
                    $this->fillable,
                    $tenant->restrictions
                );
            }
        }
    }

    /**
     * イベントベースのFillable制御
     */
    protected static function booted()
    {
        static::saving(function ($model) {
            $model->tenant_id = auth()->user()->tenant_id;
        });

        static::updating(function ($model) {
            if ($model->isDirty('tenant_id')) {
                throw new \RuntimeException(
                    'Cannot change tenant_id'
                );
            }
        });
    }

    /**
     * 一括更新の拡張
     */
    public function fill(array $attributes)
    {
        // テナントIDの自動設定
        if (!isset($attributes['tenant_id'])) {
            $attributes['tenant_id'] = auth()->user()->tenant_id;
        }

        // テナント固有のバリデーション
        $this->validateTenantAttributes($attributes);

        return parent::fill($attributes);
    }

    /**
     * テナント固有のバリデーション
     */
    protected function validateTenantAttributes(array $attributes)
    {
        $tenant = auth()->user()->tenant;

        // テナント固有のバリデーションルール
        $rules = $tenant->validation_rules ?? [];

        if (!empty($rules)) {
            $validator = Validator::make($attributes, $rules);

            if ($validator->fails()) {
                throw new ValidationException($validator);
            }
        }
    }

    /**
     * カスタムクエリスコープ
     */
    public function scopeWithTenantContext($query)
    {
        return $query->with(['tenant' => function ($q) {
            $q->select(['id', 'name', 'settings']);
        }]);
    }
}

/**
 * 使用例
 */
class Document extends TenantAwareFillable
{
    protected $fillable = [
        'title',
        'content',
        'status',
    ];

    public function tenant()
    {
        return $this->belongsTo(Tenant::class);
    }

    /**
     * テナント固有の処理
     */
    public function processWithTenantContext()
    {
        $tenant = $this->tenant;

        if ($tenant->hasFeature('document_versioning')) {
            $this->createVersion();
        }

        if ($tenant->hasFeature('document_approval')) {
            $this->initiateApprovalFlow();
        }
    }
}

これらの高度なテクニックを使用することで、より柔軟で保守性の高いアプリケーションを構築できます。最後に、これまでの内容をまとめるセクションに移りましょう。

まとめ:セキュアで保守性の高いテクニック設計のために

Fillableベストプラクティスの実装チェックリスト

以下のチェックリストを使用して、Fillableの実装が適切に行われているか確認できます。

セキュリティ関連チェック項目

/**
 * セキュリティチェックリスト実装例
 */
trait FillableSecurityCheck
{
    public function runSecurityCheck(): array
    {
        $results = [];

        // 1. センシティブな属性の保護
        $sensitiveFields = ['password', 'remember_token', 'api_key'];
        $exposedFields = array_intersect($this->getFillable(), $sensitiveFields);

        if (!empty($exposedFields)) {
            $results['sensitive_fields'] = [
                'status' => 'warning',
                'message' => 'Sensitive fields found in fillable: ' . implode(', ', $exposedFields)
            ];
        }

        // 2. Mass Assignment脆弱性チェック
        if (empty($this->fillable) && empty($this->guarded)) {
            $results['mass_assignment'] = [
                'status' => 'error',
                'message' => 'Neither fillable nor guarded is set'
            ];
        }

        return $results;
    }
}

実装チェックリスト

  1. 基本設定の確認
  • [ ] すべてのモデルで$fillableまたは$guardedが明示的に設定されている
  • [ ] センシティブな属性が$fillableから除外されている
  • [ ] 主キーやタイムスタンプが適切に保護されている
  • [ ] 属性の型がPHPDocで明記されている
  1. セキュリティ対策
  • [ ] Mass Assignment脆弱性への対策が実装されている
  • [ ] バリデーションと連携している
  • [ ] ポリシーによる権限制御が実装されている
  • [ ] 更新履歴のログが残るようになっている
  1. 保守性の確認
  • [ ] コードにドキュメントが付与されている
  • [ ] テストが実装されている
  • [ ] 属性の変更が追跡可能になっている
  • [ ] 環境別の設定が適切に管理されている
  1. パフォーマンス最適化
  • [ ] 不要な属性のロードを避けている
  • [ ] N+1問題に対処している
  • [ ] キャッシュを適切に利用している
  • [ ] クエリの最適化が行われている

学習リソースの紹介

公式ドキュメント

  • Laravel公式ドキュメント: Eloquentモデル
  // 例:公式ドキュメントのサンプルコード
  use Illuminate\Database\Eloquent\Model;

  class Article extends Model
  {
      /**
       * @link https://laravel.com/docs/eloquent
       */
      protected $fillable = [
          'title',
          'body',
      ];
  }

セキュリティガイドライン

/**
 * セキュリティベストプラクティス
 * @see https://owasp.org/www-project-top-ten/
 */
class SecureModel extends Model
{
    use FillableSecurityCheck;

    protected function bootSecurityFeatures()
    {
        // 1. 入力値の検証
        static::saving(function ($model) {
            $model->validateSensitiveData();
        });

        // 2. 変更の監査
        static::updated(function ($model) {
            $model->auditChanges();
        });
    }
}

実装パターン集

  1. 基本パターン
   protected $fillable = ['name', 'email'];
  1. 動的パターン
   public function getFillable()
   {
       return array_merge(
           parent::getFillable(),
           $this->getContextualFillable()
       );
   }
  1. ポリシー連携パターン
   protected static function booted()
   {
       static::saving(function ($model) {
           if (! auth()->user()->can('update', $model)) {
               throw new AuthorizationException;
           }
       });
   }

開発ツール

  • Laravel Debugbar: クエリと属性のデバッグ
  • PHPStan: 静的解析によるエラー検出
  • Laravel IDE Helper: IDE補完の強化

最後に

Fillableの適切な実装は、アプリケーションのセキュリティと保守性を大きく左右します。本記事で紹介したベストプラクティスとパターンを参考に、プロジェクトに最適な実装を選択してください。

また、セキュリティは常に進化していくものです。定期的に実装を見直し、新しい脆弱性や対策について情報をキャッチアップすることを推奨します。

// 継続的な改善のためのコメント例
/**
 * @todo 定期的なセキュリティレビュー
 * @todo パフォーマンス最適化
 * @todo テストカバレッジの向上
 */

この記事が、より安全で保守性の高いLaravelアプリケーションの開発の一助となれば幸いです。