Witam po krótkiej przerwie w tworzeniu poradnika dla Laravela. Dzisiejszy wpis chciałbym poświęcić projektowaniu, czy zaplanowaniu tak trywialnej czynności jaką jest obsługa formularza.
Problem wydaje się banalny i przy tym został już opisany w poprzednich wpisach o Laravelu. Mimo wszystko, poprzednie wpisy miały za zadanie zajmować się czysto funkcjonalnym podejściem, dziś chciałbym skupić się jak te elementy aplikacji typu CRUD można ułożyć, by było czysto, by można było łatwo naszą aplikację rozbudować, a przede wszystkim gruntownie zautomatyzować testy. W tym odcinku, z premedytacją nie podaję całego kodu, nie próbuję tworzyć gotowej aplikacji, a wręcz celowo będę stosował więcej pseudokodu. Chcę by wpis był raczej opisem struktury prostej aplikacji crudowej, a nie jego kompletnym przykładem.
Czego potrzebujemy na starcie? Oczywiście zainstalowanego i poprawnie skonfigurowanego Laravela. Zakładam, że środowisko zostało postawione, mamy połączenie z bazą, artisan działa poprawnie, możemy podążać dalej.
Jak napisałem w wstępie najbardziej zależy mi na utworzeniu kodu, który mimo swej prostoty będzie w jakiś sposób porządkował nasz projekt. Chciałbym więc, by najczęściej wykonywane operacje można było wykonywać w jak najprostszy i czysty sposób. Przykładem takiej operacji są właśnie operacje CRUD. W nawet niezbyt mocno rozbudowanej aplikacji często tworzy się mnóstwo obiektów, będących odwzierciedleniem rekordów bazy. Logika ich tworzenia jest zazwyczaj ta sama:
- wyświetl formularz z dodawaniem nowego obiektu
- po wysłaniu formularza do obsługi przeprowadź walidację
- w przypadku poprawności danych umieść obiekt w bazie danych + wyświetl komunikat typu success
- w przypadku braku poprawności danych, powróć do formularza, wyświetl komunikat typu failure
Tak zazwyczaj wygląda funkcjonalność dodawania nowego rekordu, a możemy mówić jeszcze o pozostałych operacjach: aktualizowaniu rekordów, czy ich usuwaniu. Pomimo faktu, iż część obiektów może posiadać pewne dodatkowe funkcje (usunięcie zdjęcia z galerii będzie skutkowało nie tylko usunięciem rekordu z bazy “Galeria” ale również trzeba będzie fizycznie usunąć zdjęcie z dysku), to jednak główny element pozostanie ten sam – usunięcie rekordu. Spróbujmy dodać uniwersalne kontrolery obsługujące dodawanie, edycję i usuwanie rekordów. Oprócz tego proponuję również dodanie kontrolera Search, który realizowałby inny z podstawowych funkcji, czyli przeszukiwanie rekordów:
1 |
php artisan make:controller Crud/Create |
1 |
php artisan make:controller Crud/Update |
1 |
php artisan make:controller Crud/Search |
1 |
php artisan make:controller Crud/Delete |
Zacznijmy od operacji “Create”. Czego będziemy potrzebowali aby ją wykonać?
- modelu obiektu do zapisu (czyli danych, które chcemy zapisać)
- obiektu, który będzie realizował walidację
- obiektu, który pozwoli nam na zapisanie naszej encji w bazie
Pamiętamy przy tym, iż zapisywanie obiektów do bazy w kontrolerze nie jest dobrą praktyką, dlatego też samo zapisywanie rekordu będzie realizowane za pomocą specjalnie przeznaczonego do tego obiektu.
Stwórzmy więc pomocnicze obiekty:
- tworzymy katalog Savers w katalogu app
- tworzymy katalog Models w katalogu app
- w katalogu Savers tworzymy plik Saver.php (czyli nasz ogólny saver)
- modyfikujemy plik composer.json, dodając autoload dla naszej nowej klasy:
12345678910<span class="str">"autoload"</span><span class="pun">:</span> <span class="pun">{</span><span class="str">"classmap"</span><span class="pun">:</span> <span class="pun">[</span><span class="pun">"app/Savers","app/Models",</span><span class="pun">],</span><span class="str">"files"</span><span class="pun">:</span> <span class="pun">[</span><span class="pun">]</span><span class="pun">},</span> - Uruchamiamy composera: composer dump-autoload (czym composer dump-autoload różni się od composer install lub composer update? Install powoduje ściągnięcie zależności i utworzenie composer.lock, jeśli nie został jeszcze stworzony, Update zawsze na nowo tworzy composer.lock i ściąga zależności, natomiast dump-autoload tworzy nową listę klas dostępnych w aplikacji)
Mamy już szkielet aplikacji:
- app/savers/Saver.php
- app/Http/Controllers/Crud/Create.php
- app/Http/Controllers/Crud/Update.php
- app/Http/Controllers/Crud/Delete.php
- app/Http/Controllers/Crud/Search.php
Przyjrzyjmy się poszczególnym plikom:
app/savers/Saver.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php namespace Savers; use Illuminate\Database\Eloquent\Model; class Saver { public function __construct() { } public function save(Model $model, $data) { $model->fill($data); $model->save(); return true; } return false; } } |
Nasz Saver nie jest jakoś specjalnie rozbudowany, po prostu wypełnia model danymi, a następnie go zapisuje.
Zajrzyjmy do create:
app/Http/Controllers/Crud/Create.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
<?php namespace <span class="pl-s1"><span class="pl-en">App\Http\Controllers\Crud</span></span>; use Savers\Saver; use Illuminate\Routing\Controller; use Illuminate\Http\Request; use Illuminate\Http\Response; class Create extends Controller { private $response; private $request; private $modelName; private $saver; private $sucess; private $failure; public function __construct(Request $request, Response $response, $modelName, Saver $saver, $success, $failure) { $this->request = $request; $this->response = $response; $this->modelName = $modelName; $this->saver = $saver; $this->failure = $failure; $this->success = $success; } public function action() { $className = $this->modelName; $model = new $className(); $saved = $this->saver->save($model, $this->request->all()); if ($saved) { return redirect($success)->with('message','Saved.'); } else { return redirect($failure)->with('message','There was a problem'); } return $this->response->setContent($content); } public function getRequest() { return $this->request; } public function getResponse() { return $this->response; } public function getModelName() { return $this->modelName; } public function getSaver() { return $this->saver; } } |
W powyższym kodzie na pierwszy miejscu uwidacznia się konstruktor, dzięki któremu możemy wstrzyknąć obiekty request, response, model, saver a także adresy, do których mamy się udać w przypadku gdy dane zostaną zapisane lub nie. W publicznej metodzie action() wywołujemy nasz saver i zapisujemy dane w bazie. Poprawne zapisanie rekordu warunkuje wynik wywołanej funkcji – redirect.
W tym miejscu spróbujmy stworzyć przykładowy model, a następnie dodać kontrolery i walidatory, najpierw model – niech przykładem posłuży prosta tabela cars posiadająca 4 pola:
1 2 |
$ php artisan make:migration:schema create_cars_table --schema="model:string, mark:string, description:text, year:unsignedInteger " |
Sprawdźamy, czy mamy nasz plik app/cars.php, powinien tam znaleźć się nasz model, który zresztą przenosimy do App/Models i od razu modyfikujemy:
1 2 3 4 5 6 7 8 9 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Car extends Model { protected $fillable = ['model', 'mark', 'description', 'year']; } |
Mając model musimy teraz zadbać o następne kilka rzeczy:
- kontroler do obsługi modelu
- walidator do obsługi walidacji modelu
- saver do obsługi zapisu
- nowe routes do naszego kontrolera
- możemy również pokusić się o widoki
Zacznijmy od walidatora, jak pisałem wcześniej, w tym celu użyjemy Form Requests:
1 |
$ php artisan make:request Validators/CarRequest |
Szybko przechodzimy do folderu app/http/Requests/Validators/ i pliku carrequest.php, po czym dodajemy nasze reguły:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'model' => 'required|max:255', 'year' => 'required'|integer|digits:4|min:1900|max:2017' 'mark' => 'required|max:255', ]; } |
W ramach walidacji nie musimy nic więcej robić, nasz kontroler otrzyma tylko i wyłącznie poprawne dane, w przypadku błędu walidacji, Laravel wróci na stronę skąd przyszło żądanie, a także w sesji umieści uzyskane błędy.
Stwórzmy teraz nasz plik CarSaver w katalogu “savers”:
1 2 3 4 5 6 7 8 9 10 |
<?php namespace Savers; class CarSaver extends Saver { public function __construct() { parent::__construct(); } } |
W przypadku zapisywania naszego samochodu, nie potrzebujemy żadnych dodatkowych funkcjonalności (nie musimy zapisywać zdjęć, itp), nasza klasa dziedzicząca nie potrzebuje więc żadnego dodatkowego kodu.
Kolejny etap to utworzenie kontrolera:
1 |
$ php artisan make:controller Car/CreateController |
I zawartość:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace App\Http\Controllers\Car; user Models\Car use Savers\CarSaver; use Validators\CarRequest; use Illuminate\Http\Request; use Illuminate\Http\Response; class Create extends App\Http\Controllers\Crud\Create { public function __construct(CarRequest $request, Response $response, CarSaver $saver) { parent::__construct($request, $response, '\Models\Car', $saver, '\success', '\failure'); } } |
Widzimy, iż w konstruktorze przekazujemy konkretny Request – samochodu, a także saver samochodu. W ciele konstruktora wywołujemy konstruktor klasy nadrzędnej również z żadaniem typu CarRequest, Modelem samochodu, a także naszym saverem. Pamiętamy również o adresach, do których ma podążyć strona w przypadku poprawnego zapisania, a także w przypadku błędu zapisu w bazie.
Pozostaje nam już tylko dodać odpowiedni routing:
1 |
<span class="token scope">Route<span class="token punctuation">::</span></span><span class="token function">post<span class="token punctuation">(</span></span><span class="token string">'car/create'</span><span class="token punctuation">,</span> <span class="token string">'CreateController@action'</span><span class="token punctuation">)</span> |
Oznacza to, iż wszystkie żądania ‘car/create’ będą obsługiwane przez stworzony przez nas kontroler. Oczywiście do pełnego działania aplikacji potrzebujemy dodatkowych podstron, ale nie powinny one mieć wpływu na ogólną logikę.
Podsumowując, schemat stworzonych plików:
- app/savers/Saver.php
- app/models/Car.php
- app/savers/CarSaver.php
- app/validators/CarValidator.php
- app/Http/Controllers/Crud/Create.php
- app/Http/Controllers/Crud/Update.php
- app/Http/Controllers/Crud/Delete.php
- app/Http/Controllers/Crud/Search.php
- app/Http/Controllers/Car/Create.php
Dodanie nowej tabeli, ogranicza się do:
– stworzenia nowej tabeli w bazie
– stworzenia formularza
– dodania plików z Modelem, pliku Saver, pliku z Walidacją
W tym momencie niektórzy mogą zapytać, po co to wszystko, dlaczego to robimy? Najlepszą odpowiedzią jest jedno słowo – test. Tak stworzony kod jest prosty w testowaniu, użycie Dependency Injection umożliwia nam dowolne mockowanie obiektów, a przy tym nadal możemy łatwo nasz kod rozszerzać, tworzyć indywidualne sposoby na zapisywanie określonych obiektów.
Co więcej, znacząco oddzielamy warstwę danych od warstwy kontrolera, nie są to sztywne powiązania, nasz saver jest swoistym DAO, kontrolera nie powinno obchodzić jak dane zostaną zapisane, on chce mieć tylko dostęp do interfejsu, który mu to umożliwi.
Poza tym unikamy zjawiska ciężkiego kontrolera, czyli umieszczanie absolutnie wszystkiego w kontrolerze.