Lekce 8 - Jednoduchý redakční systém v Laravel - Tvorba článků
V předchozím kvízu, Kvíz - Struktura projektu, migrace a šablony v Laravel, jsme si ověřili nabyté zkušenosti z předchozích lekcí.
Dnes se přesuneme k tvorbě administrace a začneme rovnou upravováním vygenerovaných metod kontroleru a vytvářením pohledů, jelikož modelovou vrstvu a routy již máme přichystané.
Seznam článků
Jako první si vytvoříme zobrazení seznamu článků.
Akce kontroleru
Jak již víme, použijeme k tomu metodu index()
. Ta neobsahuje
nic jiného než zobrazení pohledu, kterému předá všechny články
seřazené podle abecedy:
/** * Zobraz seznam článků seřazený podle abecedy. * * @return View */ public function index(): View { return view('article.index', ['articles' => Article::orderBy('title')->get()]); }
Pokud jste překvapení upozorněním IDE na neexistující
metodu orderBy()
, podívejte se na konec článku, kde najdete
kapitolu Magie skrytá v metodě __call() s podrobným
vysvětlením této funkčnosti.
Pohled
Nyní si vytvoříme nový pohled ve složce
resources/views/article/
a nazveme ho index.blade.php
.
Bude se jednat o jednoduchý výpis článků do tabulky:
@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
První zajímavostí na tomto pohledu je použití Blade direktivy
@forelse ... @empty ... @endforelse
, jež vypíše záznamy pomocí
PHP cyklu foreach()
pouze v případě, že nějaké existují. V
opačném případě se uživateli zobrazí text o chybějících
článcích.
Spuštění DELETE metody
Za povšimnutí také stojí odkázání na editaci článku, kdy jako druhý
parametr helper funkci route()
předáváme pole s parametry pro
danou routu. Klíč každé hodnoty, která je buď identifikátor záznamu
(většinou id
, v našem případě url
), nebo
instance daného modelu, je název parametru.
Pro odstranění článku však nemůžeme použít jednoduché odkázání,
jelikož se provádí HTTP metodou DELETE
. Odstraňování dat by z
bezpečnsotních důvodů nemělo být odkázáno na metody GET
ani
POST
. DELETE
je vlastně nadstavba POST
.
Místo toho si tedy vytvoříme skrytý formulář, jenž se odešle po
kliknutí na odkaz (přes event onclick
). Deklarace HTTP metody
DELETE
ve formuláři probíhá skrz Blade direktivu
@method
.
Blade direktiva @method
vloží skryté políčko
do formuláře stejně jako Blade direktiva @csrf
. Pokud se
podíváme na skrytý formulář jednoho z článků přes "Inspect element"
(klávesa F12 v prohlížeči), uvidíme pouze dvě skrytá
políčka, jejichž názvy začínají prefixem _
, aby se
případně nepletly s námi definovanými políčky, viz níže.
<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 seznam článků
Nakonec nesmíme zapomenout odkázat na nově fungující stránku v našem
menu, které se nachází v hlavní šabloně
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áření nových článků
Dále se podíváme na vytváření nového článku.
Akce create()
a
store()
Formulář pro vytváření nového článku si zobrazíme v akci
create()
:
/** * Zobraz formulář pro vytváření nového článku. * * @return View */ public function create(): View { return view('article.create'); }
Validace formuláře a ukládání nového článku bude probíhat v akci
store()
:
/** * Zvaliduj odeslaná data přes formulář a vytvoř nový článek. * * @param Request $request * @return RedirectResponse * * @throws ValidationException */ public function store(Request $request): RedirectResponse { $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'); }
Jak si můžete všimnout, metoda store()
obsahuje
parametr $request
, i když není definovaný v tabulce rout.
Získáme ho opět pomocí dependency injection, jelikož
definujeme, o jaký typ objektu se jedná. Můžeme sice pracovat s helper
funkcí request()
jako v minulých lekcích (v takovém případě
by metoda neměla žádný parametr), v následující lekci si však ukážeme,
proč se v některých případech vyplatí více používat právě tento
objektový přístup.
Nezapomeneme také importovat třídy ValidationException
a
RedirectResponse
:
use Illuminate\Http\RedirectResponse; use Illuminate\Validation\ValidationException;
Pohled
Vytvoříme pohled create.blade.php
ve složce
resources/views/article/
. Novinkou tohoto pohledu je použití
helper funkce old()
, která obsahuje stará data formuláře,
například v případě, kdy data pro nový článek neprojdou přes
validační pravidla:
@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
Pro editor obsahu článku jsem se rozhodl použít externí nástroj TinyMCE, užitečný WYSIWYG HTML editor připomínající např. MS Word.
Vytváření nových článků je plně funkční. Můžeme si to vyzkoušet
na stránce /article/create
(jednoduše se na ni proklikáte ze
seznamu článků):

Avšak kód v metodě store()
pro vytvoření nového článku
se zdá repetitivní a může tak akorát spět ke zbytečným chybám kvůli
překlepům, jelikož musíme definovat hodnotu pro každý atribut:
$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();
Pojďme si tuto akci tedy trochu vylepšit.
Mass assignment
Místo nastavování hodnot jedné po druhé můžeme využít Eloquent
metody create()
, kde předáme pouze pole dat z formuláře, kdy
klíče jsou názvy sloupečků:
Article::create($request->all());
Z šesti řádků jsme udělali pouze jeden a přitom jsme zachovali stejnou logiku aplikace. Bohužel jak už možná tušíte, touto metodou by se do článku mohly dostat i nechtěná data. V našem případě by to ničemu nevadilo, přeci jen není čeho zneužít na článcích. U důležitějších databázových tabulek, jako jsou například uživatelé, by však mohlo bez našeho vědomí dojít k předání jiné hodnoty, než bychom očekávali, a to třeba pro administrátorská práva. Tento útok se nazývá mass assignment a více se o něm dočtete v odkazovaném článku.
Laravel nás automaticky chrání před tímto útokem. Pokud si nyní zkusíme vytvořit nový článek, dostaneme následující chybu:

Jak nám chybová hláška napovídá, v našem modelu Article
bychom měli definovat pole vlastností, které mohou být předávané stylem
uvedeným výše. Na to slouží proměnná $fillable
, kam
dosadíme všechny vlastnosti našich článků:
/** * Pole vlastností, které nejsou chráněné před mass assignment útokem. * * @var array */ protected $fillable = [ 'title', 'url', 'description', 'content', ];
Pokud si nyní zkusíme vytvořit článek, vše už bude fungovat tak, jak
má. Avšak i přesto nám IDE zobrazuje upozornění, že metody
create()
a orderBy()
modelu Article
nejsou definované. Proč tomu vlastně tak je?
Magie skrytá v metodě
__call()
Pokud jste se někdy více zajímali o PHP, pravděpodobně jste se setkali s
pojmem magické metody. Pokud ne, určitě znáte alespoň
jednu z nich - __construct()
. Jak víte, nejedná se zrovna o
metodu, kterou bychom v kódu sami na nějakém objektu volali. I přesto ji
obsahuje nespočet tříd.
Magické metody jsou totiž volané automaticky v nějakém okamžiku.
Okamžiku, kdy jsou splněná určitá kritéria. Pro právě zmíněný
konstruktor je to vytvoření objektu. A pro __call()
je to
volání metody, která není definovaná v rozsahu třídy. Jak už jistě
tušíte, jedná se o jednu z magických metod, která je přepsaná třídou
Model
a děděná naším modelem Article
. Její obsah
je následující:
/** * 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); } if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) { return $resolver($this); } return $this->forwardCallTo($this->newQuery(), $method, $parameters); }
Pokud bychom se chtěli podívat ještě hlouběji, museli bychom si
otevřít i metodu forwardCallTo()
. Takto bychom pokračovali dále
a dále. Nám již však stačí tento kontext. Povšimněte si, že všechny
metody, které neexistují a nejedná se o increment()
nebo
decrement()
, popřípadě Eloquent vztah (ty si vysvětlíme
později), jsou automaticky předané builder objektu, jenž je získán z
metody newQuery()
. Tento objekt nám poskytuje slavné Eloquent ORM přes známou
třídu Builder
.
V případě, že bychom chtěli být konkrétní a vyvarovat se všem upozorněním v našem IDE, by vytváření článku vypadalo následovně:
Article::query()->create($request->all());
Do takovéhoto formátu se vlastně poté převede pouhé statické volání
metody create()
za běhu aplikace.
I když můžeme najít definice metod increase()
a
decrease()
ve třídě Model
, nejedná se o statické
metody. Pomocí __call()
, kam spadají i neznámé
statické metody, však vytvoříme jejich statickou podobu
Možnost předávání hodnot v poli z formuláře do metody modelu využijeme i u editace článku.
V další lekci, Jednoduchý redakční systém v Laravel - Správa článků, si povíme něco o třídách HTTP požadavků a podíváme se také na správu článků.
Měl jsi s čímkoli problém? Zdrojový kód vzorové aplikace je ke stažení každých pár lekcí. Zatím pokračuj dál, a pak si svou aplikaci porovnej se vzorem a snadno oprav.