Ендпоінти, які приймають набір опцій, мають тихий режим відмови. Клієнт надсилає ?filter[stat us]=draft з друкарською помилкою, ваш код читає $filters['status'], нічого не знаходить і повертає нефільтрований список. Ніхто не отримує помилку, відповідь виглядає нормально, а баг спливає пізніше як "фільтр іноді не працює".
Laravel 13.24 додає правило валідації array_keys саме для цього: масив може містити ці ключі і нічого більше, з повідомленням про помилку, яке вказує, що пішло не так.
Правило валідації
Працюють як builder-форма, так і рядкова:
use Illuminate\Validation\Rule;
$request->validate([
'filter' => Rule::arrayKeys(['status', 'author', 'tag']),
]);
// Еквівалент
$request->validate([
'filter' => 'array_keys:status,author,tag',
]);
Якщо передано ['status' => 'draft', 'stat us' => 'draft'], валідація завершиться з помилкою:
The filter field must only contain the following keys: status, author, tag.
Ключі є дозволеними, але не обов'язковими. Правило обмежує те, що може з'явитися, але не вимагає обов'язкової присутності. Якщо потрібна друга половина, required_array_keys все ще покриває це, і обидва правила можна комбінувати:
'coordinates' => [
'required_array_keys:lat,lng',
Rule::arrayKeys(['lat', 'lng']),
],
Ця пара читається як "точно ці ключі, не більше і не менше".
Чому не array:key_1,key_2?
Rule::array() вже деякий час приймає список ключів і справді відхиляє неочікувані ключі. Різниця полягає в тому, що відбувається при помилці. Правило array відповідає на два запитання: "це масив?" і "він містить тільки ці ключі?", і повідомляє про обидва одним повідомленням:
| Правило |
Повідомлення для ['status' => 'draft', 'colour' => 'red'] |
array:status,author |
The filter field must be an array. |
array_keys:status,author |
The filter field must only contain the following keys: status, author. |
Перше повідомлення активно вводить в оману, оскільки значення є масивом. Також немає placeholder для проблемних ключів, тому неможливо написати власне повідомлення, яке їх називає.
Обидва правила звітуються окремо в $validator->failed() як Array та ArrayKeys, тому можна застосовувати обидва, коли потрібно розрізняти кожну помилку:
'filter' => ['array', Rule::arrayKeys(['status', 'author', 'tag'])],
Rule::array() та його повідомлення залишаються незмінними, тому нічого з уже написаного коду не поводиться інакше.
Вказування проблемних ключів
Правило постачається з двома placeholder. :values містить прийняті ключі і використовується в стандартному повідомленні. :unexpected містить ключі, які фактично спричинили помилку, і хоча він не використовується в стандартному повідомленні, зазвичай це кориснішша половина у відповіді API:
$request->validate(
['filter' => Rule::arrayKeys(['status', 'author', 'tag'])],
['filter.array_keys' => 'The :attribute field may not contain :unexpected.'],
);
// The filter field may not contain colour, sort.
Вказуючи клієнту, який ключ він зробив неправильно, ви перетворюєте гру в здогадки на виправлення в один рядок, що варте власного повідомлення в публічному API.
Приклад фільтрованого ендпоінту
Об'єднуючи все разом на маршруті, який приймає фільтри, сортування та include з власним набором дозволених ключів:
namespace App\Http\Requests;
use App\Enums\PostStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexPostRequest extends FormRequest
{
public function rules(): array
{
return [
'filter' => ['sometimes', 'array', Rule::arrayKeys(['status', 'author', 'tag'])],
'filter.status' => ['sometimes', Rule::enum(PostStatus::class)],
'filter.author' => ['sometimes', 'integer', 'exists:users,id'],
'filter.tag' => ['sometimes', 'string', 'max:50'],
'sort' => ['sometimes', 'string', Rule::in(['title', '-title', 'published_at', '-published_at'])],
];
}
public function messages(): array
{
return [
'filter.array_keys' => 'Unknown filter: :unexpected. Allowed filters are :values.',
];
}
}
Контролер може тоді читати набір фільтрів без захисних перевірок, оскільки все, що до нього дійшло, є ключем, який ви вказали:
public function index(IndexPostRequest $request)
{
$filters = $request->validated('filter', []);
return PostResource::collection(
Post::query()
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
->when($filters['author'] ?? null, fn ($q, $author) => $q->where('user_id', $author))
->when($filters['tag'] ?? null, fn ($q, $tag) => $q->whereRelation('tags', 'slug', $tag))
->paginate()
);
}
Без цього правила ?filter[autor]=3 повертає всі пости і виглядає як робочий запит. З ним клієнт отримує 422 з вказівкою на autor.
Валідація JSON-колонки
Те саме правило корисне при записі даних, де колонка налаштувань або уподобань схильна накопичувати все, що випадково надіслав frontend:
'preferences' => ['sometimes', 'array', Rule::arrayKeys(['theme', 'timezone', 'digest_frequency'])],
'preferences.theme' => ['sometimes', Rule::in(['light', 'dark', 'system'])],
'preferences.timezone' => ['sometimes', 'timezone'],
'preferences.digest_frequency' => ['sometimes', Rule::in(['daily', 'weekly', 'never'])],
Перейменоване поле frontend тепер явно падає під час розгортання замість того, щоб записувати ключ, який ніхто не читає, у кожен рядок.
Деталі, які варто знати
Кілька особливостей поведінки, які не очевидні з повідомлення:
-
Значення, яке не є масивом, не проходить правило. Передача 'filter' => 'draft' створює повідомлення "must only contain the following keys", яке читається дивно для рядка. Поєднуйте його з array, коли вхідні дані можуть взагалі не бути масивом, щоб помилка типу повідомлялася окремо.
-
Правило потребує принаймні одного ключа. Написання 'array_keys' без ключів викликає InvalidArgumentException під час виконання валідації, а не провалює поле. Немає форми "не дозволяти нічого"; цей випадок покриває prohibited.
-
Ключі можуть походити з будь-якого Arrayable. Працюють колекція або список на основі enum, а backed enum розв'язуються до їхніх значень:
Rule::arrayKeys(FilterKey::cases());
Rule::arrayKeys(collect(config('filters.allowed')));
- Варіативна форма теж працює.
Rule::arrayKeys('status', 'author') еквівалентна передачі масиву, що зручно для inline-використання.
Правило було внесено @nebarg у #60918.