illuminatech/model-rules

Laravel 模型验证规则的集合,用于检查模型是否存在或是否唯一

1.0.2 2024-03-25 11:37 UTC

This package is auto-updated.

Last update: 2024-08-25 12:31:48 UTC


README

Laravel 模型验证规则


此扩展提供了一套验证规则,用于检查 Laravel 中模型是否存在或是否唯一。

有关许可信息,请检查 LICENSE 文件。

Latest Stable Version Total Downloads Build Status

安装

安装此扩展的首选方式是通过 composer

运行

php composer.phar require --prefer-dist illuminatech/model-rules

或者将以下内容添加到您的 composer.json 文件的 "require" 部分中。

"illuminatech/model-rules": "*"

使用方法

此扩展提供了一套验证规则,用于检查 Laravel 中模型是否存在或是否唯一。它有两个主要用途

  • 定义 Eloquent 模型存在/唯一验证的流畅接口,
  • 消除 Eloquent 模型操作中的冗余数据库查询。

假设我们有一个数据库存储了一些按类别分组的条目。当创建一个新的 HTTP 请求处理器以保存新条目时,我们需要检查给定的类别记录是否存在,并将其关联到新条目。有一个已知的建议,即使用关系方法,如 associate() 在对象级别处理关系实例化。然而,如果我们遵循它并结合标准验证,我们的程序会执行冗余的 SQL 查询。例如

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Item;
use Illuminate\Http\Request;

class ItemController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $this->validate($request, [
            'name' => [
                'required',
                'string',
            ],
            'category_id' => [
                'required',
                'integer',
                'exists:categories,id', // executes SQL query 'SELECT ... FROM categories WHERE id =...'
            ],
        ]);
        
        $item = new Item();
        $item->name = $validatedData['name'];
        
        $category = Category::query()->find($validatedData['category_id']); // executes another SQL query 'SELECT ... FROM categories WHERE id =...'
        $item->category()->associate($category);
        
        $item->save();
        // ...
    }
}

要解决这个问题,您可以使用 Illuminatech\ModelRules\Exists 验证规则。在验证过程中,它会“记住”最后一次查询的模型实例,您可以使用 getModel() 方法检索它。例如

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminatech\ModelRules\Exists;

class ItemController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $this->validate($request, [
            'name' => [
                'required',
                'string',
            ],
            'category_id' => [
                'required',
                'integer',
                $categoryRule = Exists::new(Category::class), // executes SQL query 'SELECT ... FROM categories WHERE id =...' only once
            ],
        ]);
        
        $item = new Item();
        $item->name = $validatedData['name'];
        
        $category = $categoryRule->getModel(); // returns model fetched during validation without extra SQL query
        $item->category()->associate($category);
        
        $item->save();
        // ...
    }
}

您可以使用 Illuminatech\ModelRules\Unique 为唯一模型属性设置验证。例如

<?php

namespace App\Http\Controllers;

use App\Models\Item;
use Illuminate\Http\Request;
use Illuminatech\ModelRules\Unique;

class ItemController extends Controller
{
    public function update(Request $request, Item $item)
    {
        $validatedData = $this->validate($request, [
            'name' => [
                'required',
                'string',
            ],
            'slug' => [
                'required',
                'string',
                Unique::new(Item::class, 'slug') // check 'slug' value is unique throughout the items
                    ->ignore($item), // exclude current record from validation
            ],
        ]);
        
        $item = new Item();
        $item->name = $validatedData['name'];
        $item->slug = $validatedData['slug'];
        $item->save();
        // ...
    }
}

自定义数据库查询

您可以直接将查询构建器实例传递给模型验证规则。这允许您指定任何自定义搜索条件或使用关系查询。例如

<?php

namespace App\Http\Controllers;

use App\Models\RefundRequest;
use App\Models\RefundReason;
use Illuminate\Http\Request;
use Illuminatech\ModelRules\Exists;

class RefundRequestController extends Controller
{
    public function store(Request $request)
    {
        $user = $request->user();
        
        $validatedData = $this->validate($request, [
            'reason_id' => [
                'required',
                'integer',
                $reasonRule = Exists::new(RefundReason::query()->withoutTrashed()), // custom query condition
            ],
            'order_id' => [
                'required',
                'integer',
                $orderRule = Exists::new($user->orders()), // use a relation, e.g. `Order::query()->where('user_id', $user->id)`
            ],
        ]);
        
        $refundRequest = new RefundRequest();
        $refundRequest->reason()->associate($reasonRule->getModel());
        $refundRequest->order()->associate($orderRule->getModel());
        $refundRequest->save();
        // ...
    }
}

注意:此扩展不对查询构建器对象类型施加显式限制 - 它仅期望匹配数据库查询构建器语法。因此,您可能创建一个自定义查询构建器类,该类适用于特殊数据存储,如 MongoDB 或 Redis,并将其实例作为数据源传递。如果其方法签名匹配 \Illuminate\Database\Query\Builder - 它应该可以工作。尽管这并不保证。

自定义错误信息

您可以使用 withMessage() 方法为模型规则设置自定义错误信息。如果模型实例在验证失败后可用,则可以使用语法 model_{attribute} 将其属性用作错误信息中的占位符。例如

<?php

namespace App\Http\Controllers;

use App\Models\Item;
use Illuminate\Http\Request;
use Illuminatech\ModelRules\Unique;

class ItemController extends Controller
{
    public function update(Request $request, Item $item)
    {
        $validatedData = $this->validate($request, [
            'name' => [
                'required',
                'string',
            ],
            'slug' => [
                'required',
                'string',
                Unique::new(Item::class, 'slug')
                    ->ignore($item)
                    ->withMessage('This slug is already in use at item ID=:model_id'), // on failure produces error "This slug is already in use at item ID=19"
            ],
        ]);
        // ...
    }
}

验证多个模型

假设我们有一个数据库,其中条目和类别以 '多对多' 的方式链接。在这种情况下,存储条目的请求将包含一个类别 ID 列表,这些 ID 应与条目相关联。此请求处理器的普通外观可能如下所示

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Item;
use Illuminate\Http\Request;

class ItemController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $this->validate($request, [
            'name' => [
                'required',
                'string',
            ],
            'category_ids' => [
                'required',
                'array',
            ],
            'category_ids.*' => [
                'exists:categories,id', // executes SQL query 'SELECT ... FROM categories WHERE id =...' in cycle multiple times!
            ],
        ]);
        
        $item = new Item();
        $item->name = $validatedData['name'];
        
        $item->categories()->sync($validatedData['category_ids']);
        
        $item->save();
        // ...
    }
}

您可以使用 Illuminatech\ModelRules\MultiExist 验证规则减少此类场景中的数据库查询数量。例如

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminatech\ModelRules\MultiExist;

class ItemController extends Controller
{
    public function store(Request $request)
    {
        $validatedData = $this->validate($request, [
            'name' => [
                'required',
                'string',
            ],
            'category_ids' => [
                'required',
                'array',
                $categoryRule = MultiExist::new(Category::class), // executes single SQL query 'SELECT ... FROM categories WHERE id IN (...)'
            ],
        ]);
        
        $item = new Item();
        $item->name = $validatedData['name'];
        
        $item->categories()->sync($categoryRule->getModels());
        
        $item->save();
        // ...
    }
}

注意:如您所猜测的,还有一个 Illuminatech\ModelRules\MultiUnique 验证规则,但其实际应用相当有限。

请注意!请记住,模型验证规则不是累积的,每个规则只记住最后一次查询的模型。因此,在嵌套数组验证(例如,您希望将多个条目作为单个 HTTP 请求的一批存储)的情况下,它可能无法为您提供良好的服务。对于此类情况,最好将验证分成多个步骤。例如

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminatech\ModelRules\Exists;

class ItemController extends Controller
{
    public function storeBatch(Request $request)
    {
        $firstRoundValidated = $this->validate($request, [
            'items' => ['required', 'array'],
            'items.*.name' => ['required', 'string'],
            'items.*.category_id' => ['required', 'integer'],
        ]);
        
        $items = [];
        $categoryRule = Exists::new(Category::class);
        foreach ($firstRoundValidated['items'] as $key => $item) {
            $this->validate($request, [
                "items.{$key}.category_id" => $categoryRule, // validate single item at once
            ]);
            
            // create item draft:
            $item = new Item();
            $item->name = $item['name'];
            $item->category()->associatte($categoryRule->getModel());
            $items[] = $item;
        }
        
        foreach ($items as $item) {
            $item->save(); // save item drafts
        }
        // ...
    }
}

与表单请求一起工作

您可以使用模型规则与表单请求验证一起使用。例如

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminate\Foundation\Http\FormRequest;
use Illuminatech\ModelRules\Exists;

class ItemStoreRequest extends FormRequest
{
    private $categoryRule;
    
    public function rules()
    {
        return [
            'name' => [
                'required',
                'string',
            ],
            'category_id' => [
                'required',
                'integer',
                $this->categoryRule = Exists::new(Category::class),
            ],
        ];
    }
    
    public function validatedCategory(): Category
    {
        return $this->categoryRule->getModel();
    }
}

class ItemController extends Controller
{
    public function store(ItemStoreRequest $request)
    {
        $validatedData = $request->validated();
        
        $item = new Item();
        $item->name = $validatedData['name'];
        
        $item->category()->associate($request->validatedCategory());
        
        $item->save();
        // ...
    }
}