🍣

Laravel + Livewireで「1チェックボックス = 1カラム」設計時の「いずれか1つ必須」バリデーション実装術♡

に公開

はじめに

ねぇねぇ、チェックボックスをいっぱい並べたフォームで「どれか1つは選べよ!」って要件、めっちゃあるよね?
特にLivewireで「1チェックボックス=1カラム」っていうキモいDB設計の場合、バリデーションどうすんの?って頭抱えちゃうでしょ?
今回はその悩みを秒速でぶっ飛ばす、required_without_allの裏ワザテクを教えてあげる💁‍♀️

DB構造の例

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    -- 色の選択肢(1チェックボックス = 1カラム)
    color_red BOOLEAN DEFAULT FALSE,
    color_blue BOOLEAN DEFAULT FALSE,
    color_green BOOLEAN DEFAULT FALSE,
    color_yellow BOOLEAN DEFAULT FALSE,
    color_unknown BOOLEAN DEFAULT FALSE, -- 「不明」オプション
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);

…見ただけでゲンナリだよね? 🙄

Livewireでの実装課題

Livewireはフォーム要素とプロパティがガチで1対1。だからこんな感じ:

class ProductForm extends Component
{
    public bool $color_red     = false;
    public bool $color_blue    = false;
    public bool $color_green   = false;
    public bool $color_yellow  = false;
    public bool $color_unknown = false;
}
<input type="checkbox" wire:model="color_red"> 赤  
<input type="checkbox" wire:model="color_blue"> 青  
<input type="checkbox" wire:model="color_green"> 緑  
<input type="checkbox" wire:model="color_yellow"> 黄  
<input type="checkbox" wire:model="color_unknown"> 不明  

…これで「どれか1つは必須!」ってどうやるの?って話よ。

よくある実装パターン(ダメな例)

$rules = [
    'colors' => ['required_without_all:color_red,color_blue,color_green,color_yellow'],
];
// ❌ Error: No property found for validation: [colors]

「colors」なんてプロパティねーよ!ってエラー出るだけ。Livewireのプロパティ名ミスると全滅。

メスガキの裏技的解決方法

基本の発想

一番先頭のチェックボックス(この例ならcolor_red)にだけ「他の全部がfalseのときは必須!」ってルールかますの。

namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;

class ProductForm extends Form
{
    #[Validate('boolean')]
    public bool $color_red     = false;

    #[Validate('boolean')]
    public bool $color_blue    = false;

    #[Validate('boolean')]
    public bool $color_green   = false;

    #[Validate('boolean')]
    public bool $color_yellow  = false;

    #[Validate('boolean')]
    public bool $color_unknown = false; // 「不明」

    public function rules(): array
    {
        return [
            // color_redに対して、他の色が全部選ばれてなければ必須!
            'color_red'   => [
                'boolean',
                'required_without_all:color_blue,color_green,color_yellow,color_unknown'
            ],
            'color_blue'  => ['boolean'],
            'color_green' => ['boolean'],
            'color_yellow'=> ['boolean'],
            'color_unknown'=> ['boolean'],
        ];
    }
}

あとは普通にValidateしてDBにポイっと保存すればOK👌

Enum使うともっとイケてる例

enum ProductColor: int
{
    case Red    = 1;
    case Blue   = 2;
    case Green  = 3;
    case Yellow = 4;

    public function column(): string
    {
        return match($this) {
            self::Red    => 'color_red',
            self::Blue   => 'color_blue',
            self::Green  => 'color_green',
            self::Yellow => 'color_yellow',
        };
    }
}

class ProductForm extends Form
{
    public bool $color_red     = false;
    public bool $color_blue    = false;
    public bool $color_green   = false;
    public bool $color_yellow  = false;
    public bool $color_unknown = false;

    public function rules(): array
    {
        $rules = [];
        // 各色にbooleanルール
        foreach (ProductColor::cases() as $color) {
            $rules[$color->column()] = ['boolean'];
        }

        // 最初のフィールドだけrequired_without_all
        $others = collect(ProductColor::cases())
            ->filter(fn($c) => $c->column() !== 'color_red')
            ->map->column()
            ->push('color_unknown')
            ->join(',');

        $rules['color_red'][] = "required_without_all:{$others}";

        return $rules;
    }
}

これでカッコよくコンパクトに書けちゃうわよ😘

Bladeビューでの見た目例

<div class="form-group">
    <label>色を選んでちょーだい(いずれか1つ必須)</label>
    <div class="checkbox-group">
        <label><input type="checkbox" wire:model="color_red"> 赤</label>
        <label><input type="checkbox" wire:model="color_blue"> 青</label>
        <label><input type="checkbox" wire:model="color_green"> 緑</label>
        <label><input type="checkbox" wire:model="color_yellow"> 黄</label>
        <label><input type="checkbox" wire:model="color_unknown"> 不明</label>
    </div>
    @error('color_red')
        <span class="error">ねぇ、どれか1つは選んでよね?</span>
    @enderror
</div>

メリット・デメリット

✔ メリット

  • 余計なパッケージ不要!
  • Laravel標準機能だけで完結♥
  • コードシンプルでわかりやすい

✖ デメリット

  • エラーメッセージがcolor_redにしか紐づかない
  • ちょっとトリッキーだから、一瞬「え?」ってなる

正統派!カスタムルール作る方法

まあ、ガチ勢はカスタムRule作るんだけど…そこまで大げさにしたくないなら無視でOK。

// app/Rules/AtLeastOneRequired.php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;

class AtLeastOneRequired implements Rule
{
    private array $fields;

    public function __construct(array $fields)
    {
        $this->fields = $fields;
    }

    public function passes($attribute, $value)
    {
        foreach ($this->fields as $field) {
            if (request()->input($field)) {
                return true;
            }
        }
        return false;
    }

    public function message()
    {
        return '少なくとも1つは選択してよね。';
    }
}

おわりに

どう?このrequired_without_allテク、めちゃ便利でしょ?
小規模プロジェクトとか、とにかくサクッと実装したいときには超オススメ😘
もう面倒なカスタムルール書かなくていいんだから、サクッと使っちゃいなさい✨

参考

Discussion