Vianoce v ITnetwork sú tu! Dobí si teraz kredity a získaj až 80 % extra kreditov na e-learningové kurzy ZADARMO. Zisti viac.
Hľadáme nové posily do ITnetwork tímu. Pozri sa na voľné pozície a pridaj sa k najagilnejšej firme na trhu - Viac informácií.

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):

Editor pre vytváranie nových článkov v redakčnom systéme v Laravel - Laravel framework pre PHP

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:

Mass assignment chyba v redakčnom systéme v Laravel - Laravel framework pre PHP

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.


 

Predchádzajúci článok
Jednoduchý redakčný systém v Laravel - Výpis článku
Všetky články v sekcii
Laravel framework pre PHP
Preskočiť článok
(neodporúčame)
Jednoduchý redakčný systém v Laravel - Správa článkov
Článok pre vás napísal Jan Lupčík
Avatar
Užívateľské hodnotenie:
1 hlasov
Autor se primárně věnuje vývoji webových stránek a aplikací v PHP (framework Laravel) a je jedním z herních vývojářů komunitní modifikace TruckersMP.
Aktivity