اعتبارسنجی آرایه در لاراول بدون N+1
خلاصهٔ کاملتر
وقتی کاربر یه سبد خرید رو به اندپوینت checkout میفرسته، باید فرض کنیم پیلود ممکنه مخرب باشه. کسی میتونه product_idهای جعلی، آرشیوشده، یا متعلق به tenant دیگهای رو تزریق کنه. پس باید قبل از اینکه هر چیزی به دیتابیس یا پرداخت برسه، اعتبارسنجی محکم انجام بشه.
سادهترین روش اینه که از Rule::exists روی هر آیتم آرایه استفاده کنیم:
'items.*.product_id' => [
'required',
'integer',
Rule::exists('products', 'id')->where('status', 'published'),
],این روش کار میکنه و حتی خطاها رو per-item گزارش میده، ولی یه مشکل پنهان داره: لاراول بهازای هر آیتم یه SELECT جداگانه اجرا میکنه. سبد خرید با ۲۰ محصول یعنی ۲۰ کوئری فقط برای اعتبارسنجی!
یه راهحل میانی اینه که از متد after() استفاده کنیم و همه IDها رو با یه whereIn بررسی کنیم:
public function after(): array {
return [
function (Validator $validator) {
$submittedIds = collect($validator->getData()['items'] ?? [])
->pluck('product_id')->filter();
$validIds = Product::query()
->whereIn('id', $submittedIds)
->where('status', 'published')
->pluck('id');
if ($submittedIds->diff($validIds)->isNotEmpty()) {
$validator->errors()->add('items', 'One or more of the submitted products are invalid.');
}
},
];
}این روش فقط یه کوئری اجرا میکنه، ولی یه اشکال داره: پیغام خطا کلیه و معلوم نمیشه کدوم آیتم مشکل داشته.
بهترین راهحل اینه که از prepareForValidation() استفاده کنیم. این متد قبل از اجرای rules() اجرا میشه و میتونیم IDهای معتبر رو یهبار دریافت و cache کنیم:
protected array $validProductIds = [];
protected function prepareForValidation(): void {
$submittedIds = collect($this->input('items', []))
->pluck('product_id')->filter()->unique();
$this->validProductIds = Product::query()
->whereIn('id', $submittedIds)
->where('status', 'published')
->pluck('id')->all();
}بعدش در rules() یه closure ساده مینویسیم که هر product_id رو فقط در حافظه چک کنه، بدون هیچ کوئری اضافهای:
'items.*.product_id' => [
'required',
'integer',
function (string $attribute, mixed $value, Closure $fail) {
if (! in_array($value, $this->validProductIds, true)) {
$fail('The selected product is not available.');
}
},
],اینطوری هم دقیقاً یه کوئری داریم، هم خطاها per-item هستن (مثلاً items.2.product_id)، و هم کدمون خوانا و منظمه.
prepareForValidation() اصلاً برای mutation ورودیها طراحی شده (مثلاً trim کردن رشتهها یا cast کردن نوع داده)، ولی استفاده از اون برای prefetch کردن دادههای lookup هم کاملاً منطقیه، چون همون روحیه رو داره: همه آمادهسازی داده قبل از اعتبارسنجی در یه جا.
اگه دوست نداری از prepareForValidation() برای این کار استفاده کنی، میتونی یه custom Rule class بسازی که در constructor این کوئری رو اجرا کنه. در هر صورت الگو یکیه: یه بار fetch کن، در حافظه چک کن.
نکات کلیدی:
- Rule::exists روی هر آیتم آرایه باعث N+1 کوئری میشه
- متد after() کوئری رو به یکی کاهش میده ولی خطاها رو کلی میکنه
- بهترین راه: prefetch در prepareForValidation() و چک در حافظه با closure
- این روش هم یه کوئری داره، هم پیغام خطا per-item و دقیقه
- برای امنیت چندتنانتی (multi-tenant) این الگو خیلی مهمه




