Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 8 - Jednoduchý redakční systém v Laravel - Tvorba článků

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

Editor pro vytváření nových článků v redakčním systému v Laravel - Laravel framework pro PHP

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:

Mass assignment chyba v redakčním systému v Laravel - Laravel framework pro PHP

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.

Předchozí článek
Kvíz - Struktura projektu, migrace a šablony v Laravel
Všechny články v sekci
Laravel framework pro PHP
Přeskočit článek
(nedoporučujeme)
Jednoduchý redakční systém v Laravel - Správa článků
Článek pro vás napsal Jan Lupčík
Avatar
Uživatelské hodnocení:
24 hlasů
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