Piszemy pierwszy mini-projekt w Laravelu. Tworzymy podwaliny pod napisanie aplikacji Todo, na razie poznając podstawy CRUD w Laravelu, podstawy serwowania stron z użyciem blade, podstawy konfiguracji API oraz CSS/JS a także poznajemy pojęcie modelu, migracji, fabryki i seedera. Podstawy pracy z Laravelem.

Laravel – utworzenie naszego projektu

Zakładam, że korzystamy z XAMPPa. Przechodzimy do folderu htdocs w naszej instalacji XAMPPa. Z uruchomionym PHP oraz MySQL używamy tam komendy:

composer create-project laravel/laravel example-app

Gdy wszystko zainstaluje się poprawnie, przechodzimy tam do folderu korzystając z komendy change directory:

cd example-app

Sprawdzamy, czy działa odpalając serwer:

php artisan serve

Przechodzimy na link, który wyświetli się w konsoli. Jeżeli nie widzimy tam żadnych błędów tylko ładny ekran Laravela, możemy wyłączyć serwer poprzez CRTL+C. Podstawową konfigurację mamy za sobą. Miło jednak by było przynajmniej podłączyć się do bazy danych. Będziemy to robili w zasadzie we wszystkich niemal aplikacjach Laravela, więc możemy nauczyć się tego już teraz.

Przechodzimy do pliku .env i szukamy takiej linijki:

DB_CONNECTION=sqlite

Zamieniamy to na:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

Tam oczywiście podajemy nasze dane – port, nazwa użytkownika, hasło (jeśli jest) i tak dalej. Natomiast bazy danych o takiej nazwie nie musimy tworzyć – jest łatwiejszy sposób.

W terminal wrzucamy komendę:

php artisan migrate

Jako że nasz Laravel nie widzi bazy danych o takiej nazwie (’laravel’) zapyta nas, czy ma taką bazę utworzyć. Dajemy „y” oraz ENTER. Już. Zrobił to za nas. Jest to też swego rodzaju patent na „chamskie rollbacki”, które możemy chcieć z lenistwa robić ucząc się Laravela. Jeżeli coś w migracjach (nauczymy się o nich później) popsujemy, możemy po prostu wywalić migrację, wywalić bazę danych i wykorzystać powyższą komendę do jej utworzenia.

Jeżeli mamy jeszcze dobrze napisane seedery, które jedną komendą tworzą nam fejkowe dane do testowania, nie będzie żadnej straty w takim brutalnym „dropnięciu” bazy danych. Oczywiście – możemy się po prostu nauczyć komendy rollback. Nazbyt jednak w przyszłość wybiegam.

Sprawdzamy, czy działa komendą:

php artisan serve

Teraz wyłączamy serwer i przechodzimy do folderu routes, do pliku web.php dokonać następujących zmian:

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

Teraz sobie zakomentowaliśmy przekierowanie do tego ładnego domyślnego okna Laravela, zaś na ścieżkę główną naszej aplikacji (’/’) daliśmy napis „Hello World”. Możemy raz jeszcze odpalić i zobaczyć, czy działa.

Jeżeli nas to interesuje, to domyślny widok Laravela wcześniej nam serwowany (zanim go „hello-worldem” zamieniliśmy) znajduje się w folderze resources, w folderze views pod nazwą 'welcome.blade.php’.

Własny widok i przekazywanie wartości do widoku

Migracjami i bazami danych w tym odcinku jeszcze się bawić nie będziemy, ale własny widok już utworzymy. Przechodzimy do resources/views i tworzymy pilk o nazwie msg.blade.php, do którego wklejamy:

<div>
    <p>Message {{$msg}}</p>
</div>

Ten plik ma nam dynamicznie wyświetlać wiadomość. Teraz musimy wrócić do routes/web.php i utworzyć mu route oraz przekazać wiadomość. Tak tworzymy route '/message’ odsyłający do widoku 'msg.blade.php’:

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

Wypada jeszcze przekazać tę naszą wiadomość. Nie jest to specjalnie trudne:

Route::get('/message', function () {
    return view('msg', ['msg' => 'Hello World']);
});

Możemy to zrobić na inny sposób przy pomocy funkcji compact:

Route::get('/message', function () {
    $msg = "Hello World Again!";
    return view('msg', compact('msg'));
});

Tak czy inaczej możemy teraz wykonać komendę:

php artisan serve

Pamiętajmy, aby do naszego adresu dopisać '/message’. Działa. Możemy teraz wyjść i jeszcze sprawdzić sobie wszystkie ścieżki dostępne komendą:

php artisan route:list

Wyświetlanie danych w pętli – dyrektywa 'forelse’ w Blade

Nie mamy bazy danych, ale ją sobie zasymulujemy. Jako że jestem wygodny, skopiowałem taki fragment z manuala PHP z dokumentacji funkcji array_column (do pliku routes/web.php):

$records = array(
    array(
        'id' => 1,
        'first_name' => 'John',
        'last_name' => 'Doe',
    ),
    array(
        'id' => 2,
        'first_name' => 'Sally',
        'last_name' => 'Smith',
    ),
    array(
        'id' => 3,
        'first_name' => 'Jane',
        'last_name' => 'Jones',
    ),
    array(
        'id' => 4,
        'first_name' => 'Peter',
        'last_name' => 'Doe',
    )
);
Route::get('/', function () {
         return "Hello World";
});

Do pełni szczęścia potrzebujemy jeszcze, aby te nasze rekordy zachowywały się jak obiekty jakiejś klasy, ale na pisanie własnej klasy jesteśmy zbyt wygodni. Wykonamy więc rzutowanie do typu 'object’:

$people = array();
foreach($records as $person) {
    $people[] = (object)$person;
}

W ten sposób w 'people’ będziemy mieli te dane jako obiekty i to people będziemy używać do symulowania naszej bazy danych. Zobaczmy, czy to nam działa, modyfikując nieznacznie nasz route '/message’ dla testu:

Route::get('/message', function () use ($people) {
    $person = $people[0];
    return view('msg', compact('person'));
});

Każemy naszemu callbackowi używać tablicy $people, gdzie trzymamy nasze rekordy jako obiekty. Wyciągamy pierwszą osobę i przekazujemy przez compact do widoku msg (czyli /resources/views/msg.blade.php).

Teraz tylko zmodyfikujmy ten widok:

<div>
    <p>Person ID: {{$person->id}}</p>
    <p>Person first name: {{$person->first_name}}</p>
    <p>Person last name: {{$person->last_name}}</p>
</div>

W ten sposób pracujemy z obiektami – odnosimy się do nich „po strzałce”, inaczej niż tablice, które bierzemy „po kluczu” albo indeksie. Teraz, jeżeli nie zapomnieliśmy w żadnym z plików zrobić CTRL+S możemy odpalić serwer i zobaczyć, że wszystko działa.

Pora napisać widok, który wylistuje nam wszystkie osoby, jakie mamy. Tworzymy plik /resources/views/userlist.blade.php:

<div>
    <p>Users:
        <ul>
            @forelse ($people as $person )
                <li><b>Name:</b>{{$person->first_name}} {{$person->last_name}} <b>ID</b>: {{$person->id}}</li>
            @empty
                <li>No users available!</li>
            @endforelse
        </ul>
</div>

Dyrektywa @forelse pozwala nam przechodzić w pętli po naszych rekordach oraz zapewnić pewien fallback na wypadek, gdyby lista była pusta. Teraz tylko dodajmy odpowiedni route:

Route::get('/users', function () use ($people) {
    return view('userlist', compact('people'));
});

Można odpalić serwer i przejść na '/users’, który przekierowuje do widoku userlist.blade.php, gdzie przekazujemy tablicę 'people’ i w pętli przechodzimy po wszystkich rekordach.

Możemy jeszcze zakomentować sobie nasze rekordy, aby sprawdzić, czy fallback działa:

$records = array(
    /*
    array(
        'id' => 1,
        'first_name' => 'John',
        'last_name' => 'Doe',
    ),
    array(
        'id' => 2,
        'first_name' => 'Sally',
        'last_name' => 'Smith',
    ),
    array(
        'id' => 3,
        'first_name' => 'Jane',
        'last_name' => 'Jones',
    ),
    array(
        'id' => 4,
        'first_name' => 'Peter',
        'last_name' => 'Doe',
    ) */
);

Jak zobaczymy 'no users available’ to odkomentujmy to sobie.

Widok pojedynczego usera – dynamiczne parametry route

Tworzymy plik /resources/views/user.blade.php, który ma wyświetlać dane jednego użytkownika:

<div>
    <p>Person ID: {{$person->id}}</p>
    <p>Person first name: {{$person->first_name}}</p>
    <p>Person last name: {{$person->last_name}}</p>
</div>

Teraz musimy to dobrze „ugryźć”. Przechodzimy do /routes/web.php i tworzymy funkcję, która przyjmuje dynamiczny parametr „id”:

Route::get('/users/{id}', function ($id) {
    //
});

Sprawiamy, że nasz callback jest świadomy tablicy 'people’:

Route::get('/users/{id}', function ($id) use ($people) {
    //
});

Filtrujemy tablicę people tak, aby wyciągnąć z niej rekord o ID przekazanym w adresie:

Route::get('/users/{id}', function ($id) use ($people) {
    $usersFiltered = array_filter($people, function($usr) use ($id){
        return $usr->id == $id;
    });
    //
});

Względnie możemy użyć rzutowania i potrójnego porównania (dane z urla są stringiem zaś nasze ID to typ int):

Route::get('/users/{id}', function ($id) use ($people) {
    $usersFiltered = array_filter($people, function($usr) use ($id){
        return $usr->id === (int)$id;
    });
    //
});

Teraz przekazujemy pierwszy wynik filtrowania do naszego widoku (array_filter zwraca tablicę, nawet jeśli to tylko jeden rekord):

Route::get('/users/{id}', function ($id) use ($people) {
    $usersFiltered = array_filter($people, function($usr) use ($id){
        return $usr->id == $id;
    });
    return view('user', ['person' => $usersFiltered[0]]);
});

Możemy teraz odpalić serwer i wpisać ID w adresie, po '/users’. Istniejące ID, np. 1. Wszystko działa.

Dodawanie użytkownika – named routes, formularze

W naszym web.php zaimportujmy Request, który będzie nam potrzebny:

use Illuminate\Http\Request;

Teraz dodamy Route korzystającą nie z metody http 'GET’ (odwiedzanie stron) ale 'POST’ (wysyłanie formularzy):

Route::post('/users/store', function (Request $request) {
    dd($request->all());
})->name('storeuser');

Na razie będzie nam logować przesłane dane. Użyliśmy tzw. named route, ścieżki nazwanej. Będzie to potrzebne, aby w naszym formularzu móc do niej przekierować po jej nazwie takim kodem:

<form method="POST" action="{{ route('storeuser') }}">

Okej, napiszmy sobie ten formularz. Moglibyśmy stworzyć nowy route pokazujący nowy widok z samym formularzem, ale to prosty projekt, więc możemy formularz dodać np. do widoku userlist.blade.php:

<form method="POST" action="{{ route('storeuser') }}">
        @csrf
        <label for="id">ID</label>
        <input text="text" name="id" id="id" />
        <label for="firstname">First Name:</label>
        <input text="text" name="firstname" id="firstname" />
        <label for="lastname">Last Name:</label>
        <input text="text" name="lastname" id="lastname" />
        <div>
      <button type="submit">Add</button>
    </div>
</form>

Token csrf to konieczność ze względu na bezpieczeństwo – Laravel robi wszystko za nas. Teraz możemy wysłać formularz i zobaczyć wylogowane dane, które wpisaliśmy.

Widzimy, że mamy do czynienia z tablicą. Pojedynczy element wyświetlimy tak:

Route::post('/users/store', function (Request $request) {
    dd($request['id']);
})->name('storeuser');

Okej, mamy już wszystko, aby napisać dodawanie użytkowników.

Route::post('/users/store', function (Request $request) use ($people) {
    $id = (int)$request['id'];
    $fname = $request['firstname'];
    $lname = $request['lastname'];
    $user_to_add = array('id' => $id, 'first_name' => $fname, 'last_name' => $lname);
    $people[] = (object) $user_to_add;
    return redirect()->route('userlist');
})->name('storeuser');

Oczywiście musimy jeszcze nazwać naszą route 'userlist’:

Route::get('/users', function () use ($people) {
    return view('userlist', compact('people'));
})->name('userlist');

Jeżeli nas przekierowało do userlist to dobrze. Natomiast naszego użytkownika tam nie będzie – dane przechowywane w tablicach są ulotne (od tego są bazy danych). My to sobie zrobiliśmy dla treningu (nie tylko samego Laravela, ale także pewnych podstaw PHP).

Jeżeli koniecznie chcemy zobaczyć tablicę z naszym rekordem dodanym, potrzebujemy kodu:

Route::post('/users/store', function (Request $request) use ($people) {
    $id = (int)$request['id'];
    $fname = $request['firstname'];
    $lname = $request['lastname'];
    $user_to_add = array('id' => $id, 'first_name' => $fname, 'last_name' => $lname);
    $people[] = (object) $user_to_add;
    // return redirect()->route('userlist');
    return print_r($people);
})->name('storeuser');

Jak widać wszystko dodaje się prawidłowo. Później jednak następuje redirect i nasza tablica ginie w pamięci a reszta jest odzyskiwana z tablicy records. Dotarliśmy do końca naszej symulacji, do takiej „niewidzialnej ściany” mówiąc językiem gier komputerowych.

Dalej już tylko musimy pisać „prawdziwe” aplikacje, z bazą danych, modelami i migracjami i tym się zajmiemy w następnych odcinkach tutoriala. Mamy natomiast już pewne podstawy, które nie będą nam zawadzać w przyszłości.

Konfiguracja vite – statyczne pliki CSS/JS

Jest jeszcze jedna rzecz, którą lepiej opanować wcześniej niż później – serwowanie statycznych plików CSS/JS. Dzisiejszy Laravel przychodzi w pakiecie z vite, takim asset bundlerem (kiedyś był mix). Mamy to w package.json dodane, jedyne co musimy zrobić, to w tym samym katalogu, w którym robimy „php artisan serve” odpalić komendę:

npm install

Następnie przechodzimy do vite.config i zgodnie z wytycznymi usuwamy plik CSS, zostawiając tylko JS (tak vite działa najlepiej):

export default defineConfig({
    plugins: [
        laravel({
            input: [ 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});

Teraz przechodzimy do pliku resources/js/app.js i importujemy tam, jako JS, nasz plik CSS, który wywaliliśmy z defineConfig:

import './bootstrap';
import '../css/app.css'; 

Teraz idziemy folder wyżej do css/app.css i dodajemy cokolwiek:

body {
    color: teal;
}

To jeszcze nie wszystko. Szukamy jakiegoś widoku (ja dałem userlist.blade.php) i wklejamy u góry w sekcji <head>:

<!doctype html>
<head>
    {{-- ... --}}
 
    @vite('resources/js/app.js')
</head>

To jeszcze nie wszystko. Wykonujemy komendę:

npm run build

Teraz możemy odpalić serwer:

php artisan serve

Po przejściu na ścieżkę /users powinniśmy zauważyć, że nasze zmiany zostały wprowadzone (kolor tekstu zmieniony). Cóż, jest trochę „zabawy” z tym wszystkim, ale to już tyczy się większości dzisiejszych frameworków, nie tylko Laravela.

Raz opanujemy i później będziemy to z zamkniętymi oczami robić. Natomiast fakt – różni się to nieco od podłączania pliku CSS do strony napisanej w HTML.

Laravel 11 – ustawiamy obsługę API

Jeżeli korzystamy z Laravela 11, to nie będziemy mieli pliku api.php razem z web.php w folderze 'routes’ domyślnie zainstalowanego. Będziemy musieli ten plik sami wygenerować. Robimy to komendą:

 php artisan install:api

Teraz możemy wejść do folderu routes, do pliku api.php i dodać testowy route:

Route::get('/helloapi', function (Request $request) {
    return 'Hello API!';
});

Następnie korzystając z poniższej komendy sprawdzimy, jaka jest ścieżka do naszego api:

php artisan route:list

Otrzymujemy informację, że nasz route nazywa się:

GET|HEAD   api/helloapi

Wszystkie ścieżki w api.php mają ten przedrostek 'api’. Możemy teraz wypróbować, czy to działa.

Oczywiście, API nie jest od tego, aby serwować tekst ani html, tylko dane w formacie JSON. Przekopiujmy sobie 'records’ do api.php:

$records = array(
    array(
        'id' => 1,
        'first_name' => 'John',
        'last_name' => 'Doe',
    ),
    array(
        'id' => 2,
        'first_name' => 'Sally',
        'last_name' => 'Smith',
    ),
    array(
        'id' => 3,
        'first_name' => 'Jane',
        'last_name' => 'Jones',
    ),
    array(
        'id' => 4,
        'first_name' => 'Peter',
        'last_name' => 'Doe',
    ) 
);

Teraz zróbmy route, który zwróci te dane w formacie JSON:

Route::get('/users', function (Request $request) use ($records) {
    return response()->json([
        'users' => $records, 
    ]);
});

Gotowe. Podstawy API już znamy.

Pierwsza migracja, model, fabryka i seeder – podstawowe kroki

Nie chciałem o tym przesadnie wspominać w pierwszym tutorialu, ale te kroki stanowią podstawę pewnego workflow, jakie mamy z Laravelem. Zrobimy sobie więc model, migrację, fabrykę i seeder komendą:

php artisan make:model Person -mfs

Końcówka „-mfs” oznacza „migration factory seeder” i jest to coś, co będziemy często robić przy pracy z Laravelem. Co tak właściwie stworzyliśmy? Wyjaśnijmy to sobie:

  • migracja instruuje Laravela co ma utworzyć w bazie danych (jaką tabelę, jakie kolumny). Nasza migracja znajduje się w folderze migrations i posiada nazwę „create_people_table” (Laravel w 11 odsłonie jest już całkiem sprytny z nazewnictwe) i jak można się domyślić tworzy tabelę o nazwie „people”
  • model służy do bezstresowego (bez SQL queries) wyciągania informacji z bazy danych
  • fabryka służy do testowania aplikacji na fejkowych danych – ustalamy tam w jaki sposób tworzy się jeden fejkowy obiekt danej klasy do treningu (takie backendowe lorem ipsum)
  • seeder służy do odpalania fabryki – możemy tam ustalić, że za każdorazowym odpaleniem seedera ma on stworzyć za pomocą fabryki 10 obiektów

Przechodzimy do naszej migracji i dodajemy jedno dodatkowe pole, jakim będzie 'name’ (typ string):

public function up(): void
    {
        Schema::create('people', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('name')->nullable(false);
        });
    }

Teraz możemy odpalić migrację komendą:

php artisan migrate

W naszej bazie danych możemy teraz zobaczyć (np. w phpMyAdmin) tabelę 'people’ z polami id, name oraz odpowiednimi timestamps. Migrację możemy cofnąć komendą:

php artisan migrate:rollback

Wtedy nasza tabela zniknie. I trzeba będzie raz jeszcze migrować. Teraz przechodzimy do PersonFactory.php wygenerowanego w folderze database/factories i dodajemy definicję, jak fejkowy obiekt ma być tworzony:

class PersonFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => fake()->name()
        ];
    }
}

Nasze 'name’ ma być generowane przez fakera korzystającego z metody 'name’. Teraz pora przejść do folderu seeders, odnaleźć PersonSeeder.php i zaimportować tam model Person:

use App\Models\Person;

Teraz możemy uzupełnić funkcję run:

class PersonSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Person::factory()->count(10)->create();
    }
}

Korzystając z modelu Person wykorzystujemy jego fabrykę 10 razy tworząc fejkowego za każdym razem użytkownika (czy tam osobę) i to działanie podpisamy do komendy:

php artisan db:seed --class=PersonSeeder

Wykonanie tej komendy spowoduje utworzenie 10 fejkowych użytkowników, co możemy zaobserwować w bazie danych. Jeżeli coś nie działa to w 9/10 przypadków jest to niezapisany plik (brak ctrl+s po dokonaniu zmian), brak importu bądź literówka.

Teraz zrobimy sobie widok peoplelist.blade.php o takiej treści:

<p>Users:</p>
        <ul>
            @forelse ($people as $person )
                <li><b>Name:</b>{{$person->name}} <b>ID</b>: {{$person->id}}</li>
            @empty
                <li>No people available!</li>
            @endforelse
        </ul>

Następnie przechodzimy do routes/web.php, importujemy nasz model:

use App\Models\Person;

Teraz napiszemy route, która odsyła nas do widoku peoplelist.blade.php wcześniej pobierając z bazy danych wszystkie rekordy i przekazując je jako kontekst:

Route::get('/people', function (Request $request) {
    $people = Person::all();
    return view('peoplelist', ['people' => $people]);
});

Jak widać nie jest to szczególnie trudne. Po odpaleniu zobaczymy, że działa i wyświetla to, co w bazie danych.

Dodawanie nowej osoby – niezbędne kroki

Będziemy chcieli pod tym samym adresem, który stworzymy, dla metody GET wyświetlać formularz dodawania osoby, dla metody POST – dodawać osobę do bazy danych i przekierowywać do listy osób.

Pierwsza rzecz, jaką musimy zrobić, to nazwać route, do którego będziemy przekierowywać:

Route::get('/people', function (Request $request) {
    $people = Person::all();
    return view('peoplelist', ['people' => $people]);
})->name('peoplelist');

Druga rzecz – stworzyć route odsyłający do widoku z formularzem:

Route::get('/people/store', function (Request $request) {
    return view('addpersonform');
});

Nasz formularz będzie odsyłał pod ten sam adres, ale z metodą POST, musimy zatem i taki route sobie stworzyć i najlepiej od razu nazwać:

Route::post('/people/store', function (Request $request) {
   return "Not impelemted yet";
})->name('peoplestore');

Okej, teraz możemy stworzyć widok addpersonform.blade.php:

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

Metoda POST, odsyłamy do named route 'peoplestore’ (który jeszcze musimy zaimplementować), token CSRF oraz pole formularza z labelem i przyciskiem submit. Wracamy do web.php i implementujemy dodawanie do bazy danych:

Route::post('/people/store', function (Request $request) {
    $data = $request->validate([
        'name' => 'required|max:255',
    ]);
    $person = new Person();
    $person->name = $data['name'];
    $person->save();
    return redirect()->route('peoplelist');

})->name('peoplestore');

Mamy tutaj walidację (’name’ jest obowiązkowe, max 255 znaków), następnie za pomocą modelu Person tworzymy nowy obiekt, przypisujemy imię, zapisujemy, robimy redirect do named route 'peoplelist’.

Teraz powinno działać. Pytanie co się stanie gdy wpiszemy puste imię. Cóż, nic się nie stanie. Aby wyświetlić wiadomość o błędzie (puste imię) musimy zmodyfikować nasz widok:

<form method="POST" action="{{route('peoplestore')}}">
    @csrf
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
    @error('name')
    <div class="alert alert-danger">{{ $message }}</div>
    @enderror
    <button type="submit">Add</button>
</form>

Teraz wiadomość będzie się pokazywać. Treść wiadomości będzie automatyczna (ucząc się robić FormRequest nauczymy się pisać własne) – na razie możemy to zostawić albo zamienić {{$message}} na własny komunikat.

Własne komunikaty błędów – FormRequest

FormRequest tworzymy w ten sposób:

php artisan make:request StorePersonRequest

W folderze app/http/requests pojawi się nasz plik StorePersonRequest.php. Dokonujemy w nim pewnych zmian. Pierwsza to authorize:

public function authorize(): bool
    {
        return true;
}

Na razie nie mamy żadnych mechanizmów autoryzacji, więc ustawiamy domyślnie na true. Dalej, wypełniamy nasze rules, czyli zasady:

 public function rules(): array
    {
        return [
            'name' => 'required|max:255'
        ];
    }

Zasady są dwie dla pola 'name’ – wymagane, max 255 znaków. Teraz musimy jeszcze stworzyć funkcję messages:

public function messages(): array
{
    return [
        'name.required' => 'Imię jest wymagane!',
        'name.max' => 'Maksymalna długość - 255 znaków!',
    ];
}

Tutaj podajemy te nasze własne informacje na każdy błąd jaki chcemy. Format jest czytelny i łatwy do opanowania – najpierw nazwa pola, po kropce nazwa zasady, która została złamana.

Będziemy jeszcze musieli w naszym modelu Person nadać „fillable” czyli określić, jakie pola mogą być wypełniane:

class Person extends Model
{
    use HasFactory;
    protected $fillable = ['name'];
}

Tylko pole imię – reszta to id i timestampy nadawane automatycznie, nie mamy innych pól. Teraz przechodzimy do web.php i importujemy nasz Request:

use App\Http\Requests\StorePersonRequest;

Odszukujemy metodę zapisującą do bazy danych i patrzymy, jaki request jest wstrzykiwany przez konstruktor:

Route::post('/people/store', function (Request $request) {

Najbardziej klasyczny. Musimy wstrzyknąć nasz, ten, który napisaliśmy i zaimportowaliśmy:

Route::post('/people/store', function (StorePersonRequest $request) {

Teraz, mając to wszystko (to jest StorePersonRequest oraz atrybut fillable dla modelu) możemy użyć naszego requesta do walidacji:

Route::post('/people/store', function (StorePersonRequest $request) {
    Person::create($request->validated());
    return redirect()->route('peoplelist');

})->name('peoplestore');

Dla jasności:

  • Person::create jest możliwe dzięki nadaniu modelowi atrybutu fillable, dzięki któremu Laravel wie, które pola przekazujemy my, a które nie mają znaczenia (id, timestampy, albo nawet cokolwiek innego, co mogłoby znaleźć się w body POST a nie znajduje się w fillable)
  • request->validated() jest możliwy, bo wstrzykujemy przez konstruktor StorePersonRequest, który napisaliśmy, ustawiliśmy mu autoryzację i walidację (a także opcjonalne własne wiadomości błędów)

Własne komendy artisana – plik console.php

Obok web.php (oraz api.php wygenerowanego komendą – przynajmniej w Laravelu 11, wcześniej był tam domyślnie) znajdziemy też plik console.php, w którym możemy tworzyć własne komendy do wpisywania w konsoli.

Zrobimy sobie taką komendę, która podaje nam ilość obiektów Person. Musimy zatem zaimportować model Person do console.php:

use App\Models\Person;

Stworzenie komendy, pobranie z modelu Person ilości obiektów oraz wyświetlenie tego w konsoli trudne nie jest. Robimy to w ten sposób (wiem, że można dostać jakiejś nerwicy od czytania „robimy to w ten sposób” ale frameworki pokroju Laravela mają właśnie to do siebie, że „batteries included”, że poniekąd wymuszają pewną konwencję, można to porównać do Django w Pythonie i jego bardziej lekkiego odpowiednika o nazwie Flask, który ma nieco inną filozofię):

Artisan::command('personcount', function () {
    $cnt = Person::count();
    $this->comment("There are $cnt Person objects in database");
})->purpose('Shows person count');

Teraz wystarczy tylko wpisać naszą komendę poprzedzoną „php artisan” czyli:

php artisan personcount 

Jeżeli nie działa to znaczy, że zapomnieliśmy zapisać plik console.php, albo zaimportować model Person.

Chwytanie modelu poprzez ID – widok pojedynczego rekordu

Utworzymy sobie w naszym /resources/views widok o nazwie personsingle.blade.php:

<ul>
    <li>ID: {{$person->id}}</li>
    <li>Name: {{$person->name}}</li>
</ul>

Nie ma tu nic, czego byśmy nie znali. Teraz przechodzimy do web.php i koniecznie przed funkcją zbierającą do '/people’ tworzymy nowy route '/people/{id}’:

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

Route::get('/people', function (Request $request) {
    $people = Person::all();
    return view('peoplelist', ['people' => $people]);
})->name('peoplelist');

Kolejność naszych funkcji ma znaczenie – ta bardziej szczegółowa powinna być u góry, ta mniej szczegółowa na dole. Inaczej to '/people’ będzie przechwytywać także te urle, które powinny iść do '/people/{id}’.

Używamy tam modelu Person oraz funkcji findOrFail, która przyjmuje ID i albo takiego rekordu nie znajdzie – i zwróci 404 – albo znajdzie i się dalej bawimy. W tym wypadku – przekazujemy znaleziony rekord do widoku 'personsingle’.

Teraz możemy wejść do '/people/{id}’, w miejsce ID wpisać istniejące (np. 10) i nieistniejące (np. 100, chyba że naszego seedera wywołaliśmy 10 razy) i sprawdzić, że wszystko działa jak należy.

Pozostaje nam w tym temacie jeszcze jedna rzecz, jaką są linki do stron z parametrami (takimi jak ID). Przejdźmy do personlist.blade.php i zobaczmy, co tam mamy:

<p>Users:</p>
        <ul>
            @forelse ($people as $person )
                <li><b>Name:</b>{{$person->name}} <b>ID</b>: {{$person->id}}</li>
            @empty
                <li>No people available!</li>
            @endforelse
        </ul>

Miło by było, aby każdy element <li> był jednocześnie linkiem do strony pojedynczego rekordu. Aby to zrobić musimy użyć {{route()}} z odpowiednią nazwą oraz – jako że ścieżka przyjmuje parametr ID – z odpowiednim parametrem.

ID mamy w naszym modelu – pytanie jak to przekazać do funkcji route(). Cóż, możliwe, że zbytniego zaskoczenia nie będzie:

<li><a href="{{route('personsingle', $person->id)}}"><b>Name:</b>{{$person->name}} <b>ID</b>: {{$person->id}}</a></li>

Teraz przechodząc do '/people’ każdy element jest jednocześnie linkiem do właściwego dla danego rekordu widoku pojedynczego.

Route-model binding – wstrzyknięcie przez konstruktor

Możemy jeszcze zaimplementować tzw. route-model binding. Wygląda to tak:

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

Zamiast parametru ID mamy parametr $person typu Person (wstrzyknięcie przez konstruktor) dzięki czemu nie musimy robić findOrFail. Wystarczy, że odpowiedni ID zostanie przekazany do person i Laravel zrobi resztę.

W tym momencie warto zwrócić uwagę na jedną sprawę – route-model binding zmienia nazwę argumentu. Był id, jest person. Dla nas to nie problem, ponieważ my na razie przekazywaliśmy tylko wartość bez nazywania jej:

<a href="{{route('personsingle', $person->id)}}">

Gdybyśmy jednak gdzieś wcześniej korzystali z takiego zapisu, który podaje i nazwę i wartość, byłby problem:

<a href="{{ route('personsingle', ['id' => $person->id]) }}">

Wtedy musielibyśmy wszędzie, gdzie z takiego zapisu korzystaliśmy, zamienić to na poprawną nazwę, czyli person:

<a href="{{ route('personsingle', ['person' => $person->id]) }}">

Wartość pozostaje bez zmian – route-model binding bierze nazwę modelu i ID i sam robi odpowiedniego „findOrFaila”.

Kontrolery czyli bardziej realistyczny projekt

Dotychczas bawiliśmy się w pisanie naszej logiki głównie w pliku web.php, gdzie przekazywaliśmy do naszych routes funkcje callback z całą logiką tam zawartą. Laravel to jednak framework MVC (Model, View, Controller) i w ten sposób dobrego kodu się w nim nie pisze.

Modele i widoki już znamy, zostały nam kontrolery. A one wchodzą tam, gdzie mieliśmy nasze funkcje callback.

Napiszemy sobie taki bardzo prosty kontroler, korzystając z flagi „–invokable”, co sprawa, że taki kontroler może wykonać tylko jedną akcję. Stwórzmy go sobie:

 php artisan make:controller HelloController --invokable

Przechodzimy do app/http/controllers/HelloController.php i piszemy:

class HelloController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        return "Hello World";
    }
}

Funkcja invoke to taki odpowiednik naszego callbacku, który przekazywaliśmy do Route w web.php. Tam też się udamy i teraz przekażemy nasz kontroler. Na początku będziemy musieli go zaimportować:

use App\Http\Controllers\HelloController;

Teraz tworzymy nasz Route, ale zamiast callbacka przekazujemy klasę kontrolera:

Route::get('/fromcontroller', HelloController::class);

I już. Oczywiście wewnątrz __invoke możemy wrzucić każdą logikę, jaką pisaliśmy w callbackach. Tutaj jest jeszcze jedna rzecz warta uwagi – nasz kontroler to „single action controller”, który ma jedną metodę __invoke. Często jednak tworzymy kontrolery bez flagi „–invokable”, które mają różne metody.

Wtedy poza klasą musimy podać nazwę metody. Przykład z dokumentacji Laravela:

Route::get('/user/{id}', [UserController::class, 'show']);

Tutaj pod podany route kierujemy do klasy UserController pod jej metodę o nazwie 'show’. Ta klasa (zaimportowana rzecz jasna w web.php) ma gdzieś 'public function show’ czyli metodę, która obsługuje podany przez nas route.

To oczywiście nie koniec zagadnień z routingu i kontrolerów Laravela, ale myślę, że całkiem niezłe podstawy, na bazie których zbudujemy porządną wiedzę, wykonując kolejne ćwiczenia i aplikacje.

Blade components – komponenty Laravela

Rzućmy okiem na ten fragment z personlist.blade.php (zamienimy go na komponent Blade za chwilę):

<li><a href="{{route('personsingle', $person->id)}}"><b>Name:</b>{{$person->name}} <b>ID</b>: {{$person->id}}</a></li>

Potrzebujemy czegoś, co przyjmuje dwa argumenty – id oraz imię – i zwraca powyższy markup. Komponent tworzymy komendą:

 php artisan make:component Person

I niestety – są „chocki klocki” z nazewnictwem oraz samą składnią. Tutaj nazwa z wielkiej, ale używać go będziemy w ten sposób:

<x-person :id="$zmienna1" :name="$zmienna2">

Utworzyło nam dwa pliki. Jeden znajduje się w App/View/Components (namespace podałem, ścieżka akurat będzie małą) i nie wchodząc w szczegóły, ten konstruktor domyślny jest głupi i aż się dziwię, że od Laravela 8 (albo i wcześniej) tego nie zmienili. Używamy takiego konstruktora:

class Person extends Component
{
    /**
     * Create a new component instance.
     */
    public function __construct(
    
        public readonly string $name,
        public readonly int $id,
    ){}

    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        return view('components.person');
    }
}

Tam may nasze atrybuty, których będziemy używać w markupie w ten mniej więcej sposób:

<x-person :id="$zmienna1" :name="$zmienna2">

Funkcja reder odsyła nas do resources/views/components/person.blade.php, gdzie zaś napiszemy sobie wygląd naszego komponentu. Ma zamieniać to co powyżej na to co poniżej:

<li><a href="{{route('personsingle', $person->id)}}"><b>Name:</b>{{$person->name}} <b>ID</b>: {{$person->id}}</a></li>

A zatem powinien wyglądać tak:

<li>
    <a href="{{route('personsingle', $id)}}"><b>Name:</b>{{$name}} <b>ID</b>: {{$id}}</a>
</li>

Teraz jeszcze jak go użyć poprawnie. Przechodzimy do peoplelist.blade.php. Są pewne „chocki klocki”. Po pierwsze atrybuty zaczynamy od znaku „:”. Po drugie – wartość atrybutów w cudzysłowie, ale nie potrzebujemy klamr do wrzucania zmiennych. Ja to zawsze metodą prób i błędów ogarniam ilekroć tego potrzebuję, bo nie idzie tego spamiętać.

Tak czy inaczej pętla powinna teraz wyglądać w ten sposób:

<p>Users:</p>
        <ul>
            @forelse ($people as $person )
               <x-person :id="$person->id" :name="$person->name"/>
            @empty
                <li>No people available!</li>
            @endforelse
        </ul>

I już mamy pierwszy komponent Blade.

Podstawy routingu – Route::fallback i Route::any

Route::fallback służy do utworzenia akcji, która jest wykonywana, gdy wpiszemy do URLa nasz adres + końcówkę, która nie jest rozpoznawana, nie utworzyliśmy jej i nie pokazuje się w żaden sposób przy użyciu komendy:

php artisan route:list

Taki fallback umieszczamy na końcu i przyjmuje on tylko funkcję callback, która ma wykonać się, gdy nic nie zostanie znalezione. Wygląda to tak:

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

Oczywiście mówimy tu o nieznalezieniu ścieżki, jaką użytkownik podał w URLu. Nieznalezienie modelu (np. przez findOrFail) i wyświetlenie 404 już widzieliśmy wcześniej i nie potrzebowaliśmy fallbacka do tego.

Teraz możemy odpalić komendę 'php artisan route:list’ i znajdziemy na samym końcu takie coś:

GET|HEAD   {fallbackPlaceholder}

Możemy tam się dostać metodą GET lub HEAD i dowolną ścieżką, która nie pasuje do żadnych innych. Możemy to teraz wypróbować. Warto zwrócić uwagę, że fallback to idealny przykład na to, do czego może przydać się Single-Action Controller. Stwórzmy go:

php artisan make:controller FallbackController --invokable

Teraz wypełnijmy jego jedyną metodę, czyli __invoke:

class FallbackController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        return "not found!!!";
    }
}

W web.php importujemy kontroler na samej górze pliku:

use App\Http\Controllers\FallbackController;

Teraz używamy go w naszym fallback route, na samym dole pliku:

Route::fallback(FallbackController::class);

Oczywiście tych funkcji jest więcej. Mamy funkcje odpowiadające metodom HTTP:

  • Route::get – metoda domyślna stosowana przez przeglądarkę do odwiedzania stron
  • Route::post – metoda stosowana przez formularze do wysyłania danych inaczej niż przez adres URL z query params
  • Route::put/patch/delete – inne metody służące do update/usuwania, używane głównie przez API

Mamy też metody tworzące grupy z pewnym prefixem albo wspólną częścią nazwy (czyli prefix do ->name), metody dla resource controllerów (poznamy później) i inne – to dopiero podstawa routingu. Ja na razie pokażę metodę any, która tworzy route dostępny dla każdej metody HTTP:

Route::any('/anyverbroute', function(){
    return "any verb route!";
});
Route::fallback(FallbackController::class);

W to miejsce możemy przejść każdą metodą HTTP. Korzystając z komedy 'php artisan route:list’ możemy zobaczyć naszą ścieżkę:

ANY   anyverbroute

Tam, gdzie zazwyczaj mieliśmy nazwy metod HTTP (np. GET) tutaj mamy ANY, czyli miejsce pod tym adresem dostępne jest pod każdą metodą HTTP. Przyda się to nam do następnego przykładu.

Koncept middleware – jak to jest zrobione w Laravelu

Mamy taki bardzo głupi route korzystający z funkcji any, która pozwala tam dostać się jakąkolwiek metodą HTTP:

Route::any('/anyverbroute', function(){
    return "any verb route!";
});

Załóżmy, że wpadliśmy na pomysł, aby jednak dodać tam sprawdzanie, jaka metoda jest użyta. I nie, nie zamienimy 'any’ na 'get’ albo 'post’ – to najlepsze rozwiązanie, ale wtedy nie zrozumiemy jak działa middleware. Nie skorzystamy też z metody match, która wygląda w ten sposób:

Route::match(['get', 'post'], '/', function () {
    // ...
});

Nie wykorzystamy logiki wewnątrz funkcji, do której wstrzyknąć można obiekt Request:

Route::any('/anyverbroute', function(Request $request){
    if($request->isMethod('get')){
        return "any verb route!";
    } else {
        return "wrong method!";
    }
});

To wszystko można zrobić, ale my chcemy napisać middleware, czyli coś, co będzie przed „przepuszczeniem” nas na dany route wykonywać jakąś akcję (np. sprawdzenie w cookies, czy potwierdziliśmy, że mamy 18 lat, sprawdzenie tokena autoryzującego albo w tym wypadku – sprawdzenie metody HTTP) i albo przepuści nas w dane miejsce, albo odeśle gdzie indziej.

Wcześniej musimy tylko uzbroić się w możliwość sprawdzania, czy nasze any działa pod różnymi metodami. Na początek, nazwijmy je:

Route::any('/anyverbroute', function(){
    return "any verb route!";
})->name('any');

Teraz utwórzmy widok hello.blade.php a w nim taki formularz:

<form method="POST" action="{{route('any')}}">
    @csrf
<button type="submit">GO</button>
</form>

Ten formularz sprawia, że przechodzimy na naszą stronę, ale z metodą POST. Teraz tylko trzeba go zwrócić, u mnie pod adresem '/’:

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

Teraz możemy odpalić serwer, przejść do formularza, wcisnąć submit – przeniosło nas na stronę metodą POST. Następnie kopiujemy adres, wklejamy do nowej karty, dajemy enter – przeniosło nas metodą get.

Czy aby na pewno metoda jest poprawna możemy sprawdzić, wciskając odśwież. Przy POST przeglądarka zapyta nas, czy mamy ponownie przesłać żądanie, GET odświeża bez żadnych pytań.

Teraz możemy napisać nasze middleware. Zaczynamy od utworzenia go odpowiednią komendą:

php artisan make:middleware EnsureMethodIsGet

Przechodzimy do app/Http/middleware i otwieramy nasz plik EnsureMethodIsGet.php, dokonując niezbędnych modyfikacji:

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

Middleware posiada metodę handle. To ta metoda określa, jak ma ono działać podpięte do jakiegoś route. Tutaj przechwytujemy request idący na daną ścieżkę, sprawdzamy, czy metoda HTTP to GET, jeżeli tak – przepuszczamy dalej, w innym wypadku zamiast odsyłać do żądanej strony zwracamy odpowiedź 404.

Teraz musimy wrócić do web.php i odpowiednio użyć naszego middleware. Najpierw je importujemy:

use App\Http\Middleware\EnsureMethodIsGet;

Teraz używamy, jest to bardzo proste i oczywiste:

Route::any('/anyverbroute', function(){
    return "any verb route!";
})->name('any')->middleware([EnsureMethodIsGet::class]);

No i napisaliśmy – choć korzystamy z route, które obsługuje każdą metodę HTTP (Route::any) to w praktyce mamy podpięte middleware, które nie przepuści żądania z inną metodą, niż GET. To tak dla zobrazowania, czym jest middleware.

Tam w tej tablicy możemy oczywiście podawać po przecinku więcej middleware, które ma być aplikowane dla danego żądania – w takim wypadku, gdy mamy więcej niż jedno middleware, $next($request) odsyła do następnego middleware, aż któreś nie przepuści nas dalej albo skończą się middlewary i trafimy pod żądany adres.