HTTP-метод QUERY - це нове доповнення до специфікації HTTP (RFC 10008), розроблене саме для одного завдання: запитів. Це безпечний метод із підтримкою кешування, який передає параметри в тілі запиту, надаючи виразність POST-payload без відмови від read-only семантики GET - і без необхідності втискати складні фільтри в URL, який може досягти обмежень довжини.
Laravel 13.19 додав Http::query() до HTTP-клієнта разом із тестовими хелперами query() та queryJson(). Повноцінний хелпер Route::query() вже злито для Laravel 14, але чекати не обов'язково: роутер Laravel вже сьогодні приймає користувацькі HTTP-дієслова через Route::match(). У цьому tutorial ми використаємо його для побудови невеликого пошукового ендпоінту на основі database-драйвера Laravel Scout.
Коли потрібен метод QUERY
Один пошуковий рядок, як у нашому прикладі, не обов'язково потребує QUERY - метод починає виправдовувати себе, коли запит розростається до чогось, що GET не може зручно обробити:
- Структуровані фільтри з діапазонами, масивами та групами and/or (пошук продуктів чи оголошень)
- Геопросторові запити, що надсилають координати полігонів ("пошук у цій області карти")
- Пакетні запити, що отримують сотні записів за ID
- Пошук за чутливими значеннями, як-от email або телефони, які не повинні потрапляти в URL, логи доступу або історію браузера
- Ендпоінти звітності з діапазонами дат і group-by, які є читанням, але сьогодні моделюються як
POST /reports/run
Налаштування проєкту
Починаючи зі свіжого Laravel 13-додатку, встановіть файл API-маршрутів і Scout:
php artisan install:api
composer require laravel/scout
Database-драйвер Scout не потребує зовнішнього пошукового сервісу - він використовує WHERE LIKE запити до вашої існуючої бази даних, що добре підходить для малих і середніх датасетів. Увімкніть його у файлі .env:
SCOUT_DRIVER=database
Далі створіть модель Article з міграцією та фабрикою:
php artisan make:model Article -mf
Міграція визначає поля title та body:
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
Додайте трейт Searchable до моделі та визначте, які колонки Scout має шукати:
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Article extends Model
{
use HasFactory, Searchable;
protected $fillable = ['title', 'body'];
public function toSearchableArray(): array
{
return [
'title' => $this->title,
'body' => $this->body,
];
}
}
Визначення QUERY-маршруту
Роутер Laravel не підтримує Route::query() у Laravel 13 (він буде доступний починаючи з Laravel 14). Однак роутер Laravel реєструє маршрути для будь-якого дієслова, яке ви передаєте в Route::match(), включно з QUERY.
Додайте наступне до routes/api.php:
use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::match(['QUERY'], '/articles/search', function (Request $request) {
$validated = $request->validate([
'search' => ['required', 'string'],
'per_page' => ['sometimes', 'integer', 'between:1,50'],
]);
return Article::search($validated['search'])
->paginate($validated['per_page'] ?? 15);
});
Зверніть увагу, що валідація та $request->input() зчитують дані з JSON-тіла запиту так само, як вони робили б це для POST-запиту - без додаткової роботи. Виконання php artisan route:list підтверджує, що маршрут зареєстровано з дієсловом QUERY:
QUERY api/articles/search
Запит до цього ендпоінту виглядає як POST із GET-семантикою - критерії пошуку передаються в тілі:
QUERY /api/articles/search HTTP/1.1
Content-Type: application/json
Accept: application/json
{"search": "scout", "per_page": 10}
Тестування ендпоінту
Тестовий хелпер queryJson() Laravel 13.19 дзеркалить postJson() та подібні методи, тому тестування ендпоінту відчувається знайомо:
namespace Tests\Feature;
use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ArticleSearchTest extends TestCase
{
use RefreshDatabase;
public function test_it_searches_articles_with_the_query_method(): void
{
Article::factory()->create(['title' => 'Getting Started With Laravel Scout']);
Article::factory()->create(['title' => 'Queues in Depth']);
$this->queryJson('/api/articles/search', ['search' => 'scout'])
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.title', 'Getting Started With Laravel Scout');
}
public function test_it_validates_the_search_term(): void
{
$this->queryJson('/api/articles/search', [])
->assertUnprocessable()
->assertJsonValidationErrors('search');
}
}
На стороні клієнта HTTP-клієнт Laravel може викликати QUERY-ендпоінти методом Http::query(), доданим у версії 13.19:
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()->query('https://example.com/api/articles/search', [
'search' => 'scout',
]);
$articles = $response->json('data');
Підводний камінь: вбудований сервер PHP
Якщо ви розробляєте з php artisan serve, запити з методом QUERY отримують відповідь 501 Not Implemented ще до того, як потраплять до Laravel - вбудований сервер жорстко кодує методи запитів, які він приймає. Середовища на основі Nginx, як-от Laravel Herd та Valet, передають метод до вашого додатку. Тестові хелпери Laravel HTTP повністю уникають цієї проблеми, оскільки вони відправляють запити безпосередньо через фреймворк.
Використання QUERY на веб-маршрутах
Наш ендпоінт знаходиться в routes/api.php, тому захист CSRF взагалі не виникає. Однак якщо ви зареєструєте QUERY-маршрут у routes/web.php, ви отримаєте помилку 419: middleware PreventRequestForgery Laravel 13 трактує лише GET, HEAD та OPTIONS як запити читання, тому QUERY отримує ту саму перевірку токена, що й POST. RFC 10008 визначає QUERY як безпечний метод, і наступний мажорний реліз Laravel звільняє його відповідно - але до того часу у вас є два варіанти.
Перший варіант переносить майбутню поведінку, розширюючи middleware та додаючи QUERY до списку read-дієслів:
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as Middleware;
class PreventRequestForgery extends Middleware
{
/**
* Визначити, чи HTTP-запит використовує read-дієслово.
* QUERY є безпечним методом (RFC 10008), тому трактуємо його як GET.
* Laravel 14 поставляється з цією поведінкою; видаліть це перевизначення після оновлення.
*/
protected function isReading($request)
{
return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS', 'QUERY']);
}
}
Потім замініть його в групі middleware web у bootstrap/app.php:
use App\Http\Middleware\PreventRequestForgery;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as BasePreventRequestForgery;
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(replace: [
BasePreventRequestForgery::class => PreventRequestForgery::class,
]);
})
Тепер кожен QUERY-маршрут звільнено від CSRF, тоді як POST, PUT, PATCH та DELETE зберігають повний захист, відповідаючи тому, що Laravel 14 робитиме з коробки. Коли ви оновитеся, видаліть клас і рядок replace, і нічого більше не зміниться.
Другий варіант виключає один маршрут із middleware:
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
Route::match(['QUERY'], '/articles/search', $handler)
->withoutMiddleware(PreventRequestForgery::class);
Це добре працює для разового ендпоінту, але вам потрібно буде пам'ятати про це на кожному QUERY-маршруті, який ви додаєте, і це повністю видаляє middleware з маршруту, а не класифікує дієслово як читання. Оскільки перевизначення middleware - це зміна двох файлів, яка чисто зникає після оновлення, ми б обрали його першим.
Одна примітка щодо перевірки будь-якого підходу: CSRF middleware пропускає перевірку, коли середовище додатка - testing, тому feature-тест не отримає 419, якщо ви спочатку не переназначите середовище (наприклад, $this->app['env'] = 'production'; у методі setUp() вашого тесту).
Нарешті, пам'ятайте, що означає звільнення: фреймворк довіряє, що ваші QUERY-обробники є read-only, так само як він довіряє вашим GET-обробникам. Не змінюйте стан у QUERY-маршруті.
Перед відправкою QUERY у production
Laravel добре обробляє QUERY - маршрути навіть працюють із php artisan route:cache - але фреймворк - це лише один крок у ланцюгу. Кілька речей, які потрібно перевірити перед використанням методу в production API:
- Кожен проміжний сервіс має пропускати метод. Ми вже бачили, як вбудований сервер PHP повертає 501; CDN, WAF та load balancer можуть бути так само категоричними щодо дієслів, які вони не розпізнають. Тестуйте повний шлях від клієнта до додатка, а не лише додаток.
- Браузерні виклики мають застереження.
fetch() надсилає QUERY без проблем, але HTML-форми говорять лише GET та POST, і QUERY не є CORS-safelisted методом - кожен cross-origin запит викличе preflight.
- Переваги кешування поки що на папері. RFC визначає QUERY як cacheable, але браузери та CDN ще не реалізують кешування QUERY, тому не приймайте метод лише через цю перевагу сьогодні.
- Інструменти специфікацій наздоганяють. OpenAPI отримав повноцінну операцію
query лише у версії 3.2, випущеній у вересні 2025 року - генератори та SDK-ланцюжки інструментів, які все ще орієнтуються на 3.0 або 3.1, не можуть описати ендпоінт, а деякі HTTP-клієнтські бібліотеки жорстко кодують дієслова, які вони приймають.
Жодне з цього не є dealbreaker для внутрішнього API, де ви контролюєте обидва кінці - що також є найпростішим місцем для першої спроби QUERY.
Що далі
Коли вийде наступна мажорна версія Laravel, виклик Route::match(['QUERY'], ...) може стати Route::query('/articles/search', ...), а перевизначення CSRF вище можна видалити - решта коду в цьому tutorial залишається незмінною.
Між маршрутизацією, HTTP-клієнтом і тестовими хелперами Laravel покриває обидва кінці методу QUERY сьогодні. Ви можете дізнатися більше про метод QUERY у RFC 10008 та слідкувати за підтримкою маршрутизації в Pull Request #60655.