Podstawy Laravela część druga – pochylamy się bliżej nad routingiem oraz kontrolerami Laravela. Poznajemy lepsze i bardziej zaawansowane techniki, bardziej czytelne i mocniej oparte o wzorzec MVC.

Tworzymy nową aplikację – niezbędne kroki

Aplikację tworzymy w folderze htdocs używając tam w terminalu znanej nam już komendy:

composer create-project laravel/laravel routing-app

Gdy wszystko się poprawnie zainstaluje, przechodzimy do folderu aplikacji przy pomocy komendy cd (change directory):

cd routing-app

Możemy teraz odpalić serwer i sprawdzić, czy wszystko poprawnie się nam wyświetla przy pomocy komendy:

php artisan serve

Po przejściu na stronę wyświetloną w terminalu (adres naszego localhosta z odpowiednim portem) zobaczymy widok startowy 'welcome’ dodany domyślnie przez Laravela.

Pochodzi on z pliku web.php i wygląda tak:

Route::get('/', function () {
    return view('welcome');
});

Pierwszą rzeczą, jakiej się dzisiaj nauczymy, jest pewne usprawnienie – widoki odwiedzane metodą GET, które zwracają nazwę widoku pod dany route możemy uprościć przy pomocy statycznej metody 'view’ klasy Route:

Route::view('/', 'welcome');

Metoda 'match’ – kilka metod na raz

W pliku web.php zaimportujemy sobie bardzo ważną klasę, jaką jest Request (będzie nam za chwilę potrzebny):

use Illuminate\Http\Request;

Teraz utworzymy dwie ścieżki – jedna z metodą GET, wyświetlająca formularz, druga z metodą POST, obsługująca formularz:

Route::view('/form', 'form');
Route::post('/sendform', function(Request $request){
    dd($request->all());
})->name('sendform');

W pierwszym przypadku użyliśmy sobie ułatwienia z 'view’ (musimy tylko nasz widok stworzyć i tam formularz dać), w drugim wstrzykujemy klasę Request przez konstruktor i wyświetlamy to, co formularz przesłał, aby można było się do tej route jakoś odnieść nadaliśmy jej nazwę przez 'name’.

Teraz idziemy do widoków utworzyć plik o nazwie form.blade.php (jego nazwa to drugi argument, który przekazaliśmy do view + rozszerzenie). Piszemy prosty formularz:

<form method="POST" action="{{route('sendform')}}">
    @csrf
    <label for="name">Name:</label>
    <input type="text" name="name" id="name">
    <button type="submit">Send</button>
</form>

Teraz możemy sprawdzić, jak nam to działa. Imię powinno się wyświetlać, plus wartość tokena, po przesłaniu. Teraz osiągniemy tę samą funkcjonalność korzystając z metody match:

// Route::view('/form', 'form');
// Route::post('/sendform', function(Request $request){
//     dd($request->all());
// })->name('sendform');
 
Route::match(['get', 'post'], '/form',function(Request $request){
    if($request->isMethod('get')) {
        return view('form');
    } else {
        dd($request->all());
    }
 })->name('sendform');

Na jednym route o nazwie '/form’ mamy zbieranie dwóch metod – GET i POST. Jak GET – pokazuje formularz. Jak POST – pokazuje, co przesłaliśmy. Route tak samo nazwany, więc nic nie musimy zmieniać w formularzu.

Możliwe, że będziemy musieli zrestartować serwer, zrobić twarde odświeżenie (ctrl+f5) na stronie, jeśli coś jest w cache starego, koniec końców działać będzie tak samo, choć pod jednym adresem.

Parametry URLa i parametry query – różnica

Zobaczmy sobie, jak będzie wyglądał parametr URLa i zrozumiemy to raz na zawsze. Dodajmy taki route w web.php:

Route::get('/books/{id}', function($id){
    return "Book with id {$id}";
});

Tutaj symulujemy akcję wyciągnięcia z bazy danych (poprzez model Book zapewne) jakiegoś rekordu o ID przekazanym w URLu. Teraz możemy wejść na stronę '/books/1′ (na przykład, możemy wpisać i 100) i zobaczymy naszą informację. To był właśnie parametr URLa.

Możemy mu jeszcze nałożyć restrykcję, aby był zawsze typu numerycznego:

Route::get('/books/{id}', function($id){
    return "Book with id {$id}";
})->where('id', '[0-9]+');;

Teraz podanie tekstu jako ID przeniesie nas do strony 404. Jeżeli nie podoba nam się ta prezentowana przez Laravela, możemy sami sobie napisać własną poprzez metodę fallback:

Route::fallback(function(){
    return "404 NOT FOUND";
});

Pamiętajmy tylko, aby ten fallback trzymać na samym dole. Okej, a parametry query? Cóż, one są tymi parami klucz-wartość, które dopisujemy do naszego URLa po znaku zapytania i nimi zajmuje się request.

Dodajmy sobie to poniżej naszego pojedynczego route dla książki:

Route::get('/books', function(Request $request){
    $per_page = $request->query('per_page', 5);
    return "Here i display {$per_page} books per page";
});

Symulujemy tutaj zaciąganie iluś rekordów z bazy danych. Mamy parametr query 'per_page’ (jeżeli niewypełniony, zwróci domyślnie 5). Możemy przejść pod adres '/books’ – zobaczymy informację, że wyświetla 5 rekordów książkowych.

Możemy też przejść pod '/books?per_page=10′ (to właśnie parametr query) i zobaczyć, że teraz wyświetla 10 rekordów książkowych. Jeżeli zaś przejdziemy pod '/books/10′ to zobaczymy, że wyświetla książkę o ID 1 (to właśnie parametr URLa).

Warto jeszcze dodać, że formularze z metodą GET wysyłają zawsze dane w postaci parametrów query (dlatego nie korzysta się z nich przez GET, tylko POST, które trzyma te dane w swoim body).

Za dokumentacją Laravela – parametry URL mogą być opcjonalne:

Route::get('/user/{name?}', function (?string $name = null) {
    return $name;
});

Opcjonalne parametry URLa mogą przyjmować domyślne wartości:

Route::get('/user/{name?}', function (?string $name = 'John') {
    return $name;
});

Parametry query stringa zawsze są opcjonalne. Możemy się o tym przekonać, próbując przejść na stronę '/’ (czyli główną), dopisując jakiś query param:

/?param=nobodycares

Wyświetli się ta sama strona. Jedyny sposób, aby te parametry jakoś wpływały na działanie strony to samemu dopisać jakąś logikę, która sprawdza określony parametr, pobiera jego wartość i w oparciu o to wykonuje jakąś akcję, jak w naszym przykładzie '/books’.

Grupowanie routes poprzez middleware – przykład

Teraz napiszemy sobie głupie middleware, które sprawdza, czy mamy w query stringu parametr o nazwie 'param’ i wartości 'nobodycares’ i tylko wtedy przepuszcza nas dalej.

Zrobimy to dla ćwiczenia. Na początek komenda:

php artisan make:middleware StupidMiddleware

Teraz w app/http/middleware odszukujemy nasze middleware i implementujemy logikę:

class StupidMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $value = $request->query('param');
        if($value == 'nobodycares'){
            return $next($request);
        }
        return response('Page not found', 404);
    }
}

Teraz importujemy to middleware w naszym pliku routingu, czyli web.php:

use App\Http\Middleware\StupidMiddleware;

Dokumentacja Laravela pokazuje, że grupy objęte middleware tworzy się tak:

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // Uses first & second middleware...
    });
 
    Route::get('/user/profile', function () {
        // Uses first & second middleware...
    });
});

Wrzucamy więc w coś takiego nasze middleware a w środek wszystkie nasze routes:

Route::middleware([StupidMiddleware::class])->group(function () {
    Route::view('/', 'welcome');
    Route::match(['get', 'post'], '/form',function(Request $request){
        if($request->isMethod('get')) {
            return view('form');
        } else {
            dd($request->all());
        }
     })->name('sendform');
    Route::get('/books/{id}', function($id){
        return "Book with id {$id}";
    })->where('id', '[0-9]+');
    Route::get('/books', function(Request $request){
        $per_page = $request->query('per_page', 5);
        return "Here i display {$per_page} books per page";
    });
    Route::fallback(function(){
        return "404 NOT FOUND";
    });
});

Dodaliśmy głupie middleware do każdej z naszych routes. Oznacza to, że teraz nie przekieruje nas na żadną stronę, jeżeli nie dopiszemy do któregokolwiek adresu ’?param=nobodycares’. Część naszych routes może stracić funkcjonalność (np. nasz formularz czegoś takiego nie dopisuje).

Nie jest to też pokazywanie przykładu, jak robić coś bezpiecznego (na przykład jakiś token, sprawdzany przez middleware) – jeżeli już, to antyprzykład robienia bezpiecznych tokenów, takich rzeczy nigdy w query stringu nie trzymamy.

Jest to natomiast przykład, jak wziąć grupę routes i za jednym zamachem objąć je wszystkie jakimś middleware (albo wieloma z nich).

Grupować możemy też po innych kryteriach. Możemy na przykład utworzyć wspólny prefix dla URLa dla różnych routes, a już wewnątrz grupy dopisywać kolejne części URLa:

Route::prefix('admin')->group(function () {
    Route::get('/users', function () {
        // Matches The "/admin/users" URL
    });
});

Możemy też robić prefixy, ale nie dla URLa, tylko nazw naszych routes (tych tworzonych funkcją ->name, do których potem odsyłamy poprzez {{route(’nazwa’)}}):

Route::name('admin.')->group(function () {
    Route::get('/users', function () {
        // Route assigned name "admin.users"...
    })->name('users');
});

Możliwości są naprawdę różne, polecam tutaj dokumentację Laravela, która jest ładnie napisana i posiada w miarę czytelne przykłady.

Kontrolery – single-action controller

Uporządkujemy sobie nasz plik web.php, usuwając niepotrzebne nam już ścieżki i głupie middleware:

use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;

Route::view('/', 'welcome');


Route::fallback(function(){
        return "404 NOT FOUND";
});

Stworzymy sobie kontroler typu 'single-action’, który tworzymy odpowiednią komendą z flagą ’–invokable’:

php artisan make:controller SingleActionController --invokable

Kontroler jednoakcyjny ma tylko jedną metodę – __invoke. To ta metoda zawiera w sobie akcję, którą chcemy wykonać:

class SingleActionController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        return "Hello from single action controller";
    }
}

Importujemy nasz kontroler w web.php i przypisujemy go do jakiegoś route:

use App\Http\Controllers\SingleActionController;
Route::view('/', 'welcome');
Route::get('/invokable', SingleActionController::class);
Route::fallback(function(){
        return "404 NOT FOUND";
});

Kontroler – wiele różnych akcji

Tworzymy kontroler, który jest w stanie obsługiwać wiele różnych akcji, czyli bez flagi „–invokable” i metody __invoke:

php artisan make:controller MultiActionController

Dla przykładu zrobimy sobie 4 metody w tym kontrolerze zgodne z ogólną konwencją nazewniczą Laravela:

class MultiActionController extends Controller
{
    function index(){
        return "index";
    }
    function create(){
        return "create form";
    }
    function store(){
        return "store in database and redirect";
    }
    function show($id)
    {
        return "record with id {$id}";
    }
}
  • index – dostępny pod GET, pokazuje listę rekordów
  • create – dostępny pod GET, pokazuje formularz dodawania nowego
  • store – dostępny pod POST i takim samym route co index, dodaje do bazy danych i przekierowuje do index
  • show – dostępny pod GET, przyjmuje ID w URLu i pokazuje rekord o podanym ID

To oczywiście tylko konwencja. Teraz importujemy nasz kontroler w web.php i uczymy się go używać z różnymi akcjami:

use App\Http\Controllers\MultiActionController;
(...)
Route::get('/photos', [MultiActionController::class, 'index']);
Route::get('/photos/create', [MultiActionController::class, 'create']);
Route::post('/photos', [MultiActionController::class, 'store']);
Route::get('/photos/{id}', [MultiActionController::class, 'show']);

Trzymamy się Laravelowej konwencji, także przy nazywaniu routes. Pod każdy (z wyjątkiem post) możemy teraz przejść. Przekazujemy kontroler i nazwę metody – nie jest to trudne.

Możemy to jednak uprościć, korzystając z prefixa:

Route::prefix('photos')->group(function () {
    Route::get('/', [MultiActionController::class, 'index']);
    Route::get('/create', [MultiActionController::class, 'create']);
    Route::post('/', [MultiActionController::class, 'store']);
    Route::get('{id}', [MultiActionController::class, 'show']);
});

Nie tylko początek URLa jest dla naszych routes wspólny – także kontroler jest ten sam. Możemy zatem uprościć jeszcze bardziej:

Route::controller(MultiActionController::class)
->prefix('photos')
->group(function () {
    Route::get('/','index');
    Route::get('/create','create');
    Route::post('/','store');
    Route::get('{id}', 'show');
});

Możemy też te nasze routes odpowiednio ponazywać z prefixem i zakończeniem innym dla każdego route:

Route::controller(MultiActionController::class)
->prefix('photos')
->name('photos.')
->group(function () {
    Route::get('/','index')->name('index');
    Route::get('/create','create')->name('create');
    Route::post('/','store')->name('store');
    Route::get('{id}', 'show')->name('show');
});

Teraz dopiero wychodzi nam przydatność komendy:

php artisan route:list

Ta komenda pokaże nam nasze routes – metody HTTP, adres, nazwę (name) oraz kontroler i akcję:

GET|HEAD   photos .................................................................................... photos.index › MultiActionController@index  
POST       photos .................................................................................... photos.store › MultiActionController@store  
GET|HEAD   photos/create ........................................................................... photos.create › MultiActionController@create  
GET|HEAD   photos/{id} ................................................................................. photos.show › MultiActionController@show

Taki routing jest już naprawdę dobry.

Resource controller – kontroler dla wszystkich zadań CRUD

Czasami potrzebujemy różnych kontrolerów, które mają dziwne, inne funkcje. Czasami są to kontrolery jednej akcji (invokable), czasami bardziej skomplikowane kontrolery, które poza klasycznymi CRUD (Create Read Update Delete) posiadają jakieś inne, specyficzne akcje, dodatkowe funkcjonalności.

Najczęściej jednak mamy do czynienia z pewnym schematem, takim jak:

  • pokaż listę rekordów (index)
  • pokaż formularz dodawania nowego (create)
  • obsłuż dodawanie nowego i przekieruj na listę (store)
  • pokaż jeden rekord (show)
  • pokaż formularz edycji jednego rekordu (edit)
  • obsłuż formularz edycji jednego rekordu i przekieruj na ten rekord (update)
  • usuń rekord (destroy)

W takich wypadkach pomocne okażą się tzw. resource controllery. Tworzymy je komendą z flagą „–resource”:

php artisan make:controller PhotoController --resource

Mamy teraz utworzony kontroler z odpowiednimi metodami, gdzie trzeba – wstrzyknięte atrybuty bądź request, w dodatku ładnie opisane w komentarzach do czego metody służą:

class PhotoController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id)
    {
        //
    }
}

Możemy teraz w web.php zaimportować i użyć nasz resource controller, poprzedni zaś zakomentować:

use App\Http\Controllers\PhotoController;
(...)
// Route::controller(MultiActionController::class)
// ->prefix('photos')
// ->name('photos.')
// ->group(function () {
//     Route::get('/','index')->name('index');
//     Route::get('/create','create')->name('create');
//     Route::post('/','store')->name('store');
//     Route::get('{id}', 'show')->name('show');
// });
Route::resource('photos', PhotoController::class);
(...)

Różnica jest drastyczna – uproszczenie maksymalne. Możemy teraz sprawdzić komendą 'php artisan route:list’ jakie routes nam wygenerowało:

GET|HEAD        photos ..................................................................................... photos.index › PhotoController@index  
POST            photos ..................................................................................... photos.store › PhotoController@store  
GET|HEAD        photos/create ............................................................................ photos.create › PhotoController@create  
GET|HEAD        photos/{photo} ............................................................................... photos.show › PhotoController@show  
PUT|PATCH       photos/{photo} ........................................................................... photos.update › PhotoController@update  
DELETE          photos/{photo} ......................................................................... photos.destroy › PhotoController@destroy  
GET|HEAD        photos/{photo}/edit .......................................................................... photos.edit › PhotoController@edit

Tam, gdzie mamy {photo} de facto mielibyśmy {id}. I w naszym przypadku tam będzie lądować ID, aczkolwiek resource controller jest gotowy na route-model binding i zamiast ID możemy wstrzyknąć model w odpowiednich metodach. Podobnie – zamiast Request możemy wstrzykiwać własny FormRequest.

My modelu nie mamy w tym przykładzie, ale wygenerowanie resource controllera w oparciu o istniejący model wygląda tak:

php artisan make:controller PhotoController --model=Photo --resource

Jeżeli do tego chcemy, aby nasze formularze miały wygenerowane form-requesty (osobne dla dodawania i edycji) zamiast standardowego Request, to dodajemy kolejną flagę:

php artisan make:controller PhotoController --model=Photo --resource --requests

Warto jeszcze wspomnieć o resource controllerach, które nie muszą mieć wszystkich metod. Laravel nazywa to 'partial resource routes’. Możemy zaznaczyć, których metod chcemy:

Route::resource('photos', PhotoController::class)->only([
    'index', 'show'
]);

Możemy też ustalić, które metody wykluczamy – nasz wybór:

Route::resource('photos', PhotoController::class)->except([
    'create', 'store', 'update', 'destroy'
]);

Route-model binding – kolejne ułatwienie

Robiliśmy już to w pierwszym odcinku, ale warto przypomnieć. Oto 'show’ route dla modelu Person, wykorzystujące klasyczny parametr ID:

Route::get('/people/{id}', function (Request $request, $id) {
    $person = Person::findOrFail($id);
    return view('personsingle', compact('person'));
})->name('personsingle');

Oto ten sam kod, wykorzystujący parametr {nazwa-modelu} i mechanizm route-model binding, który tworzymy wstrzykując model jako parametr funkcji:

Route::get('/people/{person}', function (Person $person, Request $request,) {
    return view('personsingle', compact('person'));
})->name('personsingle');

Nie musimy robić 'findOrFail’, pod zmienną $person mamy obiekt Person pozyskany przy pomocy ID przekazanego do parametru URLa o nazwie {person}. Spore ułatwienie, z którego grzech nie korzystać.

Musimy tylko pamiętać o jednym – nasz parametr nazywa się teraz {person}, nie {id}. Oznacza to, że wcześniej w plikach blade odnosiliśmy się do naszego route w ten sposób:

<a href="{{ route('personsingle', ['id' => 123]) }}">Click me<a/>

Teraz musimy zastosować odpowiednią nazwę, już nie 'id’ tylko 'person’:

<a href="{{ route('personsingle', ['person' => 123]) }}">Click me<a/>

Oczywiście przy jednym parametrze nie ma większego problemu, bo można go przekazać pomijając nazwę:

<a href="{{ route('personsingle', 123) }}">Click me<a/>

Jeżeli jednak w trakcie pisania projektu przeniesiemy się z {id} na route-model binding, to musimy pamiętać, że nazwa parametru URL jest teraz inna i być może trzeba odświeżyć linki w naszych plikach Blade – warto mieć to z tyłu głowy, zwłaszcza, jeżeli coś nie będzie działać, jak należy.