Kurs Laravel 13 #8: wyszukiwarka artykułów na blogu
zweryfikowano 2 września 2026 · 5 min
Wystarczy formularz w GET, zapytanie where('title', 'like', "%$fraza%") i widok. Uwaga: w SQLite LIKE ignoruje wielkość liter tylko w ASCII. Szukanie „demonstration” bez akcentu nie znajdzie „démonstration”, podczas gdy MySQL i MariaDB sobie z tym radzą.
Wyszukiwarka składa się z trzech elementów: formularza, który wysyła zapytanie, metody, która odpytuje bazę, i widoku, który pokazuje wyniki. Ten rozdział je składa, a potem mierzy, co taka wyszukiwarka potrafi, i czego nie potrafi.
Formularz
Jest już w szablonie napisanym w rozdziale 5, u góry każdej strony:
<form action="{{ route('search') }}" method="GET" role="search" class="flex gap-2">
<label for="q" class="sr-only">Rechercher</label>
<input id="q" type="search" name="q" value="{{ request('q') }}"
placeholder="Rechercher…"
class="rounded border border-gray-300 px-3 py-1.5 text-sm">
<button type="submit" class="rounded bg-gray-900 px-3 py-1.5 text-sm text-white">Go</button>
</form>Liczą się trzy szczegóły. Metodą jest GET, a nie POST: wyszukiwanie niczego nie zmienia, a otrzymany adres nadaje się do udostępniania i do indeksowania. Atrybut value="{{ request('q') }}" zachowuje wpisaną frazę po wysłaniu. <label> jest ukryty wizualnie, ale czytany przez czytniki ekranu.
Starsze wersje tego kursu wstawiały do tego formularza {{ csrf_field() }}. To zbędne i szkodliwe: ochrona CSRF dotyczy wyłącznie żądań zmieniających stan serwera, a token ląduje odsłonięty w adresie URL. Laravel zresztą nigdy nie sprawdza tokena w żądaniu GET.
Trasa
Route::get('/recherche', [PostController::class, 'search'])->name('search');Metoda wyszukiwania
use Illuminate\Http\Request;
public function search(Request $request): View
{
$validated = $request->validate([
'q' => ['nullable', 'string', 'max:100'],
]);
$key = trim($validated['q'] ?? '');
$posts = Post::published()
->when($key !== '', fn ($query) => $query->where(
fn ($q) => $q->where('title', 'like', '%'.$key.'%')
->orWhere('content', 'like', '%'.$key.'%')
))
->with(['category', 'user'])
->latest('published_at')
->paginate(5)
->withQueryString();
return view('search', [
'key' => $key,
'posts' => $posts,
'categories' => Category::withCount('posts')->orderBy('name')->get(),
'tags' => Tag::orderBy('name')->get(),
'recentPosts' => Post::published()->latest('published_at')->take(5)->get(),
]);
}Cztery rzeczy zasługują tu na uwagę.
Walidacja. validate() odrzuca zapytanie dłuższe niż sto znaków albo o nieoczekiwanym typie. Bez niej odwiedzający może wysłać ?q[]=x i wywołać błąd PHP, podając tablicę tam, gdzie oczekiwany jest łańcuch znaków.
Grupowanie warunków. Funkcja anonimowa przekazana do where() zamyka oba warunki orWhere w nawiasach w wygenerowanym SQL-u. Bez niej zapytanie staje się is_published = 1 AND title LIKE … OR content LIKE …, a priorytet OR wyciągnąłby na wierzch szkice, których treść pasuje do frazy. To cichy błąd, widoczny dopiero wtedy, gdy w bazie pojawi się pierwszy szkic.
when(). Warunek stosuje się tylko wtedy, gdy fraza nie jest pusta. Puste wyszukiwanie pokazuje więc wszystkie artykuły, a nie pustą stronę.
withQueryString(). Bez tego wywołania linki paginacji gubią parametr q, a strona 2 pokazuje wszystkie artykuły zamiast wyników.
Widok wyników
@extends('layouts.app')
@section('title', 'Recherche : '.$key)
@section('content')
<h1 class="mb-6 text-2xl font-bold">
Résultats pour « {{ $key }} »
<span class="text-base font-normal text-gray-500">({{ $posts->total() }})</span>
</h1>
@include('partials.posts-list')
@endsection$posts->total() podaje liczbę wyników ze wszystkich stron, nie tylko z tej wyświetlanej.
Co ta wyszukiwarka naprawdę potrafi
Zachowanie LIKE zależy od bazy danych, nie od Laravela. Różnica jest wyraźna i warto ją znać, zanim serwis trafi na produkcję. 2 września 2026 przepytano na obu silnikach dwanaście artykułów zatytułowanych „Article de démonstration”.
| Szukana fraza | SQLite 3.46.1 | MariaDB 11.8.9 (utf8mb4_unicode_ci) |
|---|---|---|
démonstration | znajduje | znajduje |
demonstration (bez akcentu) | nie znajduje nic | znajduje |
DÉMONSTRATION (wielkie litery z akcentem) | nie znajduje nic | znajduje |
ARTICLE (wielkie litery bez akcentu) | znajduje | znajduje |
Innymi słowy: w SQLite LIKE ignoruje wielkość liter tylko dla znaków ASCII. Gdy tylko wchodzi w grę znak diakrytyczny, porównanie znów staje się ścisłe i wyszukiwanie bez znaku diakrytycznego nie zwraca niczego. To ograniczenie domyślnej implementacji SQLite, opisane przez sam projekt.
MariaDB i MySQL nie mają tego problemu przy kolacji utf8mb4_unicode_ci lub utf8mb4_general_ci: porównanie ignoruje jednocześnie wielkość liter i znaki diakrytyczne.
Obejścia
Jeśli zostajesz przy SQLite, a temat jest dla ciebie istotny, masz trzy opcje, od najprostszej do najsolidniejszej.
- Przechowuj kopię bez znaków diakrytycznych. Dodaj kolumnę
title_searchwypełnianą przy zapisie przezStr::ascii($title)i szukaj w niej, po zastosowaniu tej samej transformacji do wpisanej frazy. - Przejdź na MySQL-a albo PostgreSQL-a na produkcji. I tak jest to zwykły wybór, gdy tylko serwis zaczyna zbierać ruch.
- Użyj dedykowanego silnika wyszukiwania. Laravel Scout podłącza aplikację do Meilisearch, Typesense albo Algolii. Radzą sobie ze znakami diakrytycznymi, literówkami i trafnością, czego
LIKEnie zrobi nigdy.
Świadomie przyjęte ograniczenia
Ta wyszukiwarka jest celowo prosta i trzeba wiedzieć, czego nie robi.
- Nie wybacza żadnej literówki: „larvel” nie znajdzie „Laravela”.
- Nie sortuje według trafności. Artykuł, którego tytuł pasuje dokładnie, wychodzi na równi z artykułem wspominającym frazę raz w treści, bo sortowanie idzie po dacie.
- Szuka w surowym HTML-u treści. Wpisanie
strongwyciągnie wszystkie artykuły zawierające pogrubiony tekst. LIKE '%fraza%'nie może skorzystać z indeksu. Przy kilku tysiącach artykułów jest to niezauważalne, powyżej zapytanie zwalnia proporcjonalnie do rozmiaru tabeli.
Dla osobistego bloga to wystarczy. Powyżej tej skali kolejnym krokiem jest Scout.
Ostatni rozdział dokłada paginację i artykuły powiązane.