8. diel - Jednoduchý redakčný systém v Laravel - Tvorba článkov
V minulej lekcii, Jednoduchý redakčný systém v Laravel - Výpis článku , sme si prezreli náš prvý článok, ktorý sme si predtým pripravili. Dnes sa presunieme k tvorbe administrácie a začneme rovno upravovaním vygenerovaných metód kontroleru a vytváraním pohľadov, pretože modelovú vrstvu a ruty už máme prichystané.
Zoznam článkov
Ako prvý si vytvoríme zobrazenie zoznamu článkov.
Akcie kontroleru
Ako už vieme, použijeme na to metódu index()
. Tá neobsahuje
nič iné ako zobrazenie pohľadu, ktorému odovzdá všetky články Zoradiť
podľa abecedy:
/** * Zobraz seznam článků seřazený podle abecedy. * * @return Response */ public function index() { return view('article.index', ['articles' => Article::orderBy('title')->get()]); }
Ak ste prekvapenie upozornením IDE na neexistujúce metódu
orderBy()
, pozrite sa na koniec článku, kde nájdete kapitolu
Mágia skrytá v metóde __call () s podrobným vysvetlením
tejto funkčnosti.
Pohľad
Teraz si vytvoríme nový pohľad v priečinku
resources/views/article/
a nazveme ho index.blade.php
.
Bude sa jednať o jednoduchý výpis článkov do tabuľky:
@extends('base') @section('title', 'Seznam článků') @section('description', 'Výpis všech článků v administraci.') @section('content') <table class="table table-striped table-bordered table-responsive-md"> <thead> <tr> <th>Titulek</th> <th>Popisek</th> <th>Datum vytvoření</th> <th>Datum poslední změny</th> <th></th> </tr> </thead> <tbody> @forelse ($articles as $article) <tr> <td> <a href="{{ route('article.show', ['article' => $article]) }}"> {{ $article->title }} </a> </td> <td>{{ $article->description }}</td> <td>{{ $article->created_at }}</td> <td>{{ $article->updated_at }}</td> <td> <a href="{{ route('article.edit', ['article' => $article]) }}">Editovat</a> <a href="#" onclick="event.preventDefault(); $('#article-delete-{{ $article->id }}').submit();">Odstranit</a> <form action="{{ route('article.destroy', ['article' => $article]) }}" method="POST" id="article-delete-{{ $article->id }}" class="d-none"> @csrf @method('DELETE') </form> </td> </tr> @empty <tr> <td colspan="5" class="text-center"> Nikdo zatím nevytvořil žádný článek. </td> </tr> @endforelse </tbody> </table> <a href="{{ route('article.create') }}" class="btn btn-primary"> Vytvořit nový článek </a> @endsection
Prvý zaujímavosťou na tomto pohľade je použitie Blade direktívy
@forelse ... @empty ... @endforelse
, ktoré vypíše záznamy
pomocou PHP cyklu foreach()
iba v prípade, že nejaké existujú.
V opačnom prípade sa užívateľovi zobrazí text o chýbajúcich
článkoch.
Spustenie DELETE metódy
Za povšimnutie tiež stojí odkázaní na editáciu článku, kedy ako
druhý parameter helper funkciu route()
odovzdávame pole s
parametrami pre danú route. Kľúč každej hodnoty, ktorá je buď
identifikátor záznamu (väčšinou id
, v našom prípade
url
), alebo inštancia daného modelu, je názov parametra.
Pre odstránenie článku však nemôžeme použiť jednoduché odkázaní,
pretože sa vykonáva HTTP metódou DELETE
. Odstraňovanie dát by
z bezpečnsotních dôvodov nemalo byť odkázané na metódy GET
ani POST
. DELETE
je vlastne nadstavba
POST
. Namiesto toho si teda vytvoríme skrytý formulár, ktorý sa
odošle po kliknutí na odkaz (cez event onclick
). Deklarácia HTTP
metódy DELETE
vo formulári prebieha skrz Blade direktívu
@method
.
Blade direktíva @method
vloží skryté políčko
do formulára rovnako ako blade direktíva @csrf
. Ak sa pozrieme na
skrytý formulár jedného z článkov cez "Inspect element" (kláves
F12 v prehliadači), uvidíme iba dve skrytá políčka, ktorých
názvy začínajú prefixom _
, aby sa prípadne neplietli s nami
definovanými políčkami, viď nižšie.
<form action="http://localhost:8000/article/uvod" method="POST" id="article-delete-1" class="d-none"> <input type="hidden" name="_token" value="g7K5Lt8LRE1pzVlrWfVhCwNy78UgP6f8fPIwHXnb"> <input type="hidden" name="_method" value="DELETE"> </form>
Odkaz na zoznam článkov
Nakoniec nesmieme zabudnúť odkázať na novo fungujúce stránku v našom
menu, ktoré sa nachádza v hlavnej šablóne
resources/views/base.blade.php
:
<nav class="my-2 my-md-0 mr-md-3"> <a class="p-2 text-dark" href="#">Hlavní stránka</a> <a class="p-2 text-dark" href="{{ route('article.index') }}">Seznam článků</a> <a class="p-2 text-dark" href="#">Kontakt</a> </nav>
Vytváranie nových článkov
Ďalej sa pozrieme na vytváranie nového článku.
Akcie create () a store ()
Formulár pre vytváranie nového článku si zobrazíme v akcii
create()
:
/** * Zobraz formulář pro vytváření nového článku. * * @return Response */ public function create() { return view('article.create'); }
Validácie formulára a ukladanie nového článku bude prebiehať v akcii
store()
:
/** * Zvaliduj odeslaná data přes formulář a vytvoř nový článek. * * @param Request $request * @return Response * @throws ValidationException */ public function store(Request $request) { $this->validate($request, [ 'title' => ['required', 'min:3', 'max:80'], 'url' => ['required', 'min:3', 'max:80', 'unique:articles,url'], 'description' => ['required', 'min:25', 'max:255'], 'content' => ['required', 'min:50'], ]); $article = new Article(); $article->title = $request->input('title'); $article->url = $request->input('url'); $article->description = $request->input('description'); $article->content = $request->input('content'); $article->save(); return redirect()->route('article.index'); }
Ako si môžete všimnúť, metóda store()
obsahuje parameter $request
, aj keď nie je definovaný v tabuľke
rout. Získame ho opäť pomocou dependency injection, keďže
definujeme, o aký typ objektu sa jedná. Môžeme síce pracovať s helper
funkciou request()
ako v minulých lekciách (v takom prípade by
metóda nemala žiaden parameter), v nasledujúcej lekcii si však ukážeme,
prečo sa v niektorých prípadoch oplatí viac používať práve tento
objektový prístup.
Nezabudneme tiež importovať triedu ValidationException
:
use Illuminate\Validation\ValidationException;
Pohľad
Vytvoríme pohľad create.blade.php
v priečinku
resources/views/article/
. Novinkou tohto pohľadu je použitie
helper funkcie old()
, ktorá obsahuje staré dáta formulára,
napríklad v prípade, kedy dáta pre tento článok neprejdú cez validačné
pravidlá:
@extends('base') @section('title', 'Tvorba článku') @section('description', 'Editor pro vytvoření nového článku.') @section('content') <h1>Tvorba článku</h1> <form action="{{ route('article.store') }}" method="POST"> @csrf <div class="form-group"> <label for="title">Nadpis</label> <input type="text" name="title" id="title" class="form-control" value="{{ old('title') }}" required minlength="3" maxlength="80" /> </div> <div class="form-group"> <label for="url">URL</label> <input type="text" name="url" id="url" class="form-control" value="{{ old('url') }}" required minlength="3" maxlength="80" /> </div> <div class="form-group"> <label for="description">Popisek článku</label> <textarea name="description" id="description" rows="4" class="form-control" required minlength="25" maxlength="255">{{ old('description') }}</textarea> </div> <div class="form-group"> <label for="content">Obsah článku</label> <textarea name="content" id="content" class="form-control" rows="8">{{ old('content') }}</textarea> </div> <button type="submit" class="btn btn-primary">Vytvořit článek</button> </form> @endsection @push('scripts') <script type="text/javascript" src="{{ asset('//cdn.tinymce.com/4/tinymce.min.js') }}"></script> <script type="text/javascript"> tinymce.init({ selector: '#content', plugins: [ 'advlist autolink lists link image charmap print preview anchor', 'searchreplace visualblocks code fullscreen', 'insertdatetime media table contextmenu paste' ], toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image', entities: '160,nbsp', entity_encoding: 'raw' }); </script> @endpush
Pre editor obsahu článku som sa rozhodol použiť externý nástroj TinyMCE, užitočný WYSIWYG HTML editor pripomínajúci napr. MS Word.
Vytváranie nových článkov je plne funkčný. Môžeme si to vyskúšať
na stránke /article/create
(jednoducho sa na ňu preklikáte zo
zoznamu článkov):
Avšak kód v metóde store()
pre vytvorenie nového článku sa
zdá repetitívne a môže tak akurát spieť k zbytočným chybám kvôli
preklepom, pretože musíme definovať hodnotu pre každý atribút:
$article = new Article(); $article->title = $request->input('title'); $article->url = $request->input('url'); $article->description = $request->input('description'); $article->content = $request->input('content'); $article->save();
Poďme si túto akciu teda trochu vylepšiť.
Mass assignment
Namiesto nastavovanie hodnôt jednej po druhej môžeme využiť Eloquent
metódy create()
, kde odovzdáme iba polia dát z formulára, kedy
kľúče sú názvy stĺpcov:
Article::create($request->all());
Zo šiestich riadkov sme urobili iba jeden a pritom sme zachovali rovnakú logiku aplikácie. Bohužiaľ ako už možno tušíte, touto metódou by sa do článku mohli dostať aj nechcené dáta. V našom prípade by to ničomu nevadilo, predsa len nie je čoho zneužiť na článkoch. U dôležitejších databázových tabuliek, ako sú napríklad užívatelia, by však mohlo bez nášho vedomia dôjsť k odovzdaniu iné hodnoty, než by sme očakávali, a to napríklad pre administrátorské práva. Tento útok sa nazýva mass assignment a viac sa o ňom dočítate v prepojenom článku.
Laravel nás automaticky chráni pred týmto útokom. Ak si teraz skúsime vytvoriť nový článok, dostaneme nasledujúce chybu:
Ako nám chybová hláška napovedá, v našom modeli Article
by
sme mali definovať polia vlastností, ktoré môžu byť odovzdávané štýlom
uvedeným vyššie. Na to slúži premenná $fillable
, kam
dosadíme všetky vlastnosti našich článkov:
/** * Pole vlastností, které nejsou chráněné před mass assignment útokem. * * @var array */ protected $fillable = [ 'title', 'url', 'description', 'content', ];
Ak si teraz skúsime vytvoriť článok, všetko už bude fungovať tak, ako
má. Avšak aj napriek tomu nám IDE zobrazuje upozornenie, že metódy
create()
a orderBy()
modelu Article
nie
sú definované. Prečo tomu vlastne tak je?
Mágia skrytá v metóde __call ()
Ak ste sa niekedy viac zaujímali o PHP, pravdepodobne ste sa stretli s
pojmom magické metódy. Ak nie, určite poznáte aspoň jednu
z nich - __construct()
. Ako viete, nejedná sa zrovna o metódu,
ktorú by sme v kóde sami na nejakom objekte volali. Aj napriek tomu ju
obsahuje nespočetné množstvo tried.
Magické metódy sú totiž volané automaticky v nejakom okamihu. Okamihu,
kedy sú splnené určité kritériá. Pre práve spomínaný konštruktor je to
vytvorenie objektu. A pre __call()
je to volanie metódy, ktorá
nie je definovaná v rozsahu triedy. Ako už iste tušíte, jedná sa o jednu z
magických metód, ktorá je prepísaná triedou Model
a dedenie
naším modelom Article
. Jej obsah je nasledujúci:
/** * Handle dynamic method calls into the model. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } return $this->forwardCallTo($this->newQuery(), $method, $parameters); }
Ak by sme sa chceli pozrieť ešte hlbšie, museli by sme si otvoriť aj
metódu forwardCallTo()
. Takto by sme pokračovali ďalej a ďalej.
Nám už však stačí tento kontext. Všimnite si, že všetky metódy, ktoré
neexistujú a nejedná sa o increment()
alebo
decrement()
, sú automaticky odovzdané builder objektu, ktorý je
získaný z metódy newQuery()
. Tento objekt nám poskytuje
slávnej Eloquent ORM cez
známu triedu Builder
.
V prípade, že by sme chceli byť konkrétne a vyvarovať sa všetkým upozornením v našom IDE, by vytváranie článku vyzeralo nasledovne:
Article::query()->create($request->all());
Do takéhoto formátu sa vlastne potom prevedie púhe statickej volanie
metódy create()
za behu aplikácie.
Aj keď môžeme nájsť definícia metód
increase()
a decrease()
v triede Model
,
nejedná sa o statické metódy. Pomocou __call()
, kam spadajú aj
neznáme statické metódy, však vytvoríme ich statickú
podobu
Možnosť odovzdávanie hodnôt v poli z formulára do metódy modelu využijeme aj u editácie článku. To je však predmetom ďalšej lekcie, Jednoduchý redakčný systém v Laravel - Správa článkov , kde aj mimo iného dokončíme administráciu.