Rozdział 8 z 9

Kurs Laravel 13 #8: wyszukiwarka artykułów na blogu

zweryfikowano 2 września 2026 · 5 min

Szybka odpowiedź

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ą.

Na koniec tego rozdziału, twój blog będzie miał działającą wyszukiwarkę, a ty będziesz dokładnie wiedzieć, co ona znajduje, a czego nie.

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:

resources/views/layouts/app.blade.php
<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.

Żadnego tokena CSRF w formularzu GET

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

routes/web.php
Route::get('/recherche', [PostController::class, 'search'])->name('search');

Metoda wyszukiwania

app/Http/Controllers/PostController.php
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

resources/views/search.blade.php
@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_search wypełnianą przy zapisie przez Str::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 LIKE nie 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 strong wycią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.

Częste błędy

Token CSRF w formularzu GET Zbędny i szkodliwy: ochrona CSRF dotyczy tylko żądań zmieniających stan, a token ląduje odsłonięty w adresie URL. Laravel i tak nigdy go nie sprawdza w żądaniu GET.
Szkice w wynikach where('is_published', 1)->where(...)->orWhere(...) tworzy SQL, w którym OR bierze górę nad AND. Zgrupuj oba orWhere w funkcji anonimowej przekazanej do where().
Strona 2 wyników gubi szukaną frazę Dodaj withQueryString() po paginate(), inaczej linki paginacji zapominają o parametrze q.
Wyszukiwanie bez akcentu nic nie zwraca To zachowanie SQLite, nie Laravela. Trzymaj kopię bez znaków diakrytycznych przez Str::ascii(), przejdź na MySQL-a albo użyj Laravel Scout.
Newsletter

Nowe testy, poradniki i projekty, e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.