Hlavní navigace

Úvod do frameworku Symfony: jednoduché formuláre

Ján Bodnár

Vo štvrtom pokračovaní série článkov o PHP frameworku Symfony budeme rozoberať jednoduché formuláre. Keďže je problematika formulárov rozsiahla, bude rozdelená do viacerých častí.

Doba čtení: 10 minut

Sdílet

Symfony umožňuje vytvárať formuláre manuálne alebo pomocou builderov. S formulármi úzko súvisí problematika validácie dát a flash správ. Tie si budeme potom postupne rozoberať.

HTML formuláre

HTML formuláre sa používajú na interakciu medzi užívateľmi a webovou aplikáciou. Umožňujú si vyžiadať dáta (GET request), alebo poslať dáta (POST request). Formulár sa skladá z viacerých komponentov ako sú textové polia, select boxy, alebo rôzne buttony. Tie môžu byť spárované s popiskami, ktoré určujú význam daného komponentu.

HTML formuláre umožňujú posielať len GET alebo POST requesty. Je záhadou, prečo doteraz neboli ostatné metódy do špecifikácie HTML pridané. O dôvodoch môžeme len špekulovať.

<form>
    <input type="hidden" name="_method" value="put">
    ...
</form>

Iné druhy requestov sa vytvárajú nepriamo použitím skrytého input elementu. (Alebo sa použije JavaScript.)

Poznámka: formuláre, ktoré umožňujú meniť stav vo webovej aplikácii je nevyhnutné ošetriť proti CSRF (Cross-Site Request Forgery) útokom. Takzvané safe metódy túto ochranu mať nemusia.

Príklad jednoduchého formulára

V prvom príklade si vytvoríme obyčajný formulár. Bude vytvárať GET request, preto nebudeme implementovať CSRF ochranu.

$ composer create-project symfony/skeleton myform
$ cd myform

Pomocou composera vytvoríme novú Symfony skeleton aplikáciu.

$ composer req annotations twig
$ composer req server maker --dev

Nainštalujeme základné balíčky.

$ composer require rakit/validation
$ composer require tightenco/collect

Stiahneme si externé balíčky rakit/validation a tightenco/collect. Prvý je určený na validáciu dát, druhý na flexibilnú prácu s PHP poliami. Symfony má svoj komponent symfony/validator určený pre validáciu dát. Avšak Symfony nás neobmedzuje vo výbere externých balíčkov a my si môžeme zvoliť podľa uváženia vlastný. (Neskôr si ukážeme použitie symfony/validator komponentu.)

$ php bin/console make:controller HomeController

Pomocou Symfony maker komponentu vytvoríme šablónu controllera.

<?php
// src/Controller/HomeController.php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
    /**
     * @Route("/", name="home")
     */
    public function index()
    {
        return $this->render('home/index.html.twig');
    }
}

HomeController vracia domovskú stránku, ktorá nám zobrazí náš formulár.

{# templates/home/index.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Home page{% endblock %}

{% block body %}

<section class="ui container">

    <form class="ui form" action="{{ path('message') }}" method="get">

        <div class="field">
            <label>Name *</label>
            <input type="text" name="name" required>
        </div>

        <div class="field">
            <label>Message *</label>
            <input type="text" name="message" required>
        </div>

        <button class="ui button" type="submit">Send</button>

    </form>

</section>

{% endblock %}

Formulár je vytvorený pomocou Twig šablóny. Vo formulári zadávame meno a správu. Formulár sme koncipovali tak, že oba vstupy sú povinné; povinné polia sa bežne označujú hviezdičkou. Vzhľad stránky je riešený pomocou CSS tried (container, ui, field, form, button) z frameworku Semantic UI. Semantic UI zadefinujeme v bázovej šablóne, z ktorej naše ostatné šablóny (potomkovia) dedia.

Validáciu dát je možné vykonávať aj na strane klienta, napríklad pomocou pattern atribútu. Je triviálnou záležitosťou validáciu na strane klienta obísť. Preto je využívaná len ako doplnok k validácii na strane servera.

{% extends 'base.html.twig' %}

Twig umožňuje dedenie šablón pomocou direktívy extends. Dedenie nám pomáha výrazne redukovať duplicitu v HTML kóde.

{% block title %}Home page{% endblock %}

V šablónach-potomkoch dedíme spoločné prvky bázového pohľadu, ako sú napríklad základné tagy html alebo head, metatagy alebo spoločné link a script tagy. Ak chceme vytvoriť svoj jedinečný obsah, použijeme direktívu block, kde si zadefinujeme unikátny obsah pre daný pohľad. V tomto príklade si vytvárame unikátny titulok pre pohľad domovskej stránky.

<form class="ui form" action="{{ path('message') }}" method="get">

Formulár je typu GET. Na jeho štýlovanie sme použili CSS triedy ui a form; tie sme zdedili z bázovej šablóny.

Atribút action obsahuje cestu s názvom message, ktorú Symfony použije pri rozhodovaní, ktorej akcii priradí prichádzajúci request. Mapovaniu cesty requestu na akciu sa hovorí routing. Na pomenovanú cestu sa odkazujeme pomocou Twig funkcie path(); názov cesty sa definuje v anotácii @Route pomocou atribútu name.

<div class="field">
    <label>Name *</label>
    <input type="text" name="name" required>
</div>

Tento field očakáva meno úžívateľa. Je tvorený jednoduchým popiskom a input boxom. Hodnotu, ktorú zadáme do input boxu, budeme mať k dispozícii ako atribút GET requestu. Názov atribútu si zvolíme v name  voľbe input tagu.

<button class="ui button" type="submit">Send</button>

Submit button odošle formulár aplikácii.

{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"
                    rel="stylesheet">
    </head>

    <body>
        {% block body %}{% endblock %}
    </body>


<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js">
</html>

Toto je náš bázový pohľad. Obsahuje spoločné prvky všetkých pohľadov-potomkov. CSS framework Semantic UI si stiahneme z Cloudfare CDN (Content Delivery Network). CDN je špeciálna sieť počítačov, ktorá poskytuje rýchly prístup k zdrojom, ako sú CSS, JS súbory, obrázky, alebo videá. (Neskôr si ukážeme, ako použijeme Semantic UI lokálne.)

<body>
    {% block body %}{% endblock %}
</body>

V bázovej šablóne si zadefinujeme bloky kódu, ktoré budú nahradené v šablónach-potomkoch.

$ php bin/console make:controller MessageController

Vytvoríme nový controller s názvom MessageController.

Poznámka: validačný kód sme vložili do metódy controllera kvôli zjednodušeniu. Takýto kód je však vhodnejšie delegovať do nejakej servisnej triedy.

<?php
// src/Controller/MessageController.php

namespace App\Controller;

use Rakit\Validation\Validator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class MessageController extends AbstractController
{
    /**
     * @Route("/message", name="message", methods="GET")
     */
    public function index(Request $request): Response
    {
        $name = $request->query->get("name");
        $message = $request->query->get("message");

        $validator = new Validator;

        $vals = ['name' => $name, 'message' => $message];
        $rules = ['name' => 'required|alpha_num|min:2',
            'message' => 'required|min:8'];

        $validation = $validator->make($vals, $rules);
        $validation->validate();

        if ($validation->fails()) {

            $coll = collect($validation->errors());
            $messages = $coll->flatten();

            return new Response($messages, Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        return $this->render('message/index.html.twig', ["name" => $name,
            "message" => $message]);
    }
}

V akcii index() zistíme parametre GET requestu, validujeme ich pomocou Rakit Validation knižnice a pošleme buď validované dáta Twig šablóne na spracovanie, alebo chybovú hlášku späť klientovi.

$name = $request->query->get("name");
$message = $request->query->get("message");

Vo formulári sme mali input boxy pomenované ako name a message. Parametre requestu načítame pod takýmito názvami.

$vals = ['name' => $name, 'message' => $message];
$rules = ['name' => 'required|alpha_num|min:2',
    'message' => 'required|min:8'];

Máme dve polia dát a pravidiel. Použitím pravidiel required, alpha_num, a min sme stanovili, že políčko name musí byť vyplnené a obsahovať minimálne dva alfanumerické znaky.

$validation = $validator->make($vals, $rules);
$validation->validate();

if ($validation->fails()) {

    $coll = collect($validation->errors());
    $messages = $coll->flatten();

    return new Response($messages, Response::HTTP_UNPROCESSABLE_ENTITY);
}

Vytvoríme objekt validátora a validujeme dáta. V prípade chýb pošleme chybové hlášky späť klientovi v Response  objekte. Ako chybový kód som použil Response::HTTP_UNPROCESSABLE_ENTITY. Často nie je jasné, ktorý z chybových kódov je optimálny. Ja sa v takýchto prípadoch snažím nájsť na StackOverflow odporúčania od iných vývojárov.

Poznámka: bežným postupom je presmerovanie späť na formulár a zobrazenie chybových hlášok pomocou flash správ. To si ukážeme neskôr.

return $this->render('message/index.html.twig', ["name" => $name,
    "message" => $message]);

Ak nedošlo pri validácii dát formulára ku chybe, zavoláme render() funkciu. Funkcia render() prijíma názov Twig šablóny a dáta, ktoré sa v šablóne spracujú. Týmto dátam sa hovorí tiež kontextové dáta.

{# templates/message/index.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Show message{% endblock %}

{% block body %}

    {{name}}  says: {{message}}

{% endblock %}

V našej šablóne vypíšeme obsah kontextových dát name a message pomocou Twig direktívy {{}}.

Formulár s CSRF ochranou

Vytvorme si novú Symfony skeleton aplikáciu s balíčkami twig a server. V príklade nevalidujeme dáta, sústredíme sa na zabezpečenie proti CSRF útoku.

$ composer require symfony/security-csrf

Pre zabezpečenie proti CSRF útokom potrebujeme balíček symfony/security-csrf.

CSRF

Cross-site request forgery (CSRF) je typ útoku na webovú aplikáciu alebo službu, v ktorom sa útočník snaží podvrhnúť legitímnemu užívateľovi dáta, ktoré on nezamýšľal poslať. Úspešný CSRF útok môže viesť k zmene stavu v aplikácii, napr. k transferu peňažných prostriedkov alebo zmene profilu.

Ochrana proti CSRF útoku spočíva vo vytvorení špeciálneho skrytého políčka, v ktorom sa nachádza token vygenerovaný pre daného užívateľa. Ten token sa potom v aplikácii verifikuje.

Na zabezpečenie proti CSRF útoku sa používa komponent symfony/security-csrf, ktorý obsahuje CsrfTokenManager pre generovanie a overenie tokenov. Twig funkcia csrf_token() nám v šablóne generuje token pre užívateľa. Formuláre vytvorené pomocou Symfony builderov majú túto ochranu už v sebe zakomponovanú. V prípade manuálne vytváraných formulárov sa o to musíme postarať sami.

# config/routes.yaml
index:
    path: /
    controller: App\Controller\AppController::index

process-form:
    path: /process
    controller: App\Controller\AppController::processForm

Tentoraz si namapujeme cesty v súbore routes.yaml. Mapovanie môžeme spraviť pomocou anotácií, PHP kódom alebo v XML alebo YAML konfiguračných súboroch. Cesta index nám vráti domovskú stránku, ktorá obsahuje formulár. Druhá cesta spracuje formulár a overí CSRF token.

<?php

// src/Controller/AppController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class AppController extends AbstractController
{
    public function index()
    {
        return $this->render('home/index.html.twig');
    }

    public function processForm(Request $request)
    {
        $token = $request->request->get("token");

        if (!$this->isCsrfTokenValid('myform', $token))
        {
            return new Response('Operation not allowed', Response::HTTP_BAD_REQUEST,
                ['content-type' => 'text/plain']);
        }

        $name = $request->request->get("name");
        $email = $request->request->get("email");

        $msg = "$name with $email saved";

        return new Response($msg, Response::HTTP_CREATED,
                ['content-type' => 'text/plain']);
    }
}

AppController má dve akcie: index() a processForm().

$token = $request->request->get("token");

Z requestu získame pomocou get() metódy CSRF token.

if (!$this->isCsrfTokenValid('myform', $token))
{
    return new Response('Operation not allowed', Response::HTTP_BAD_REQUEST,
        ['content-type' => 'text/plain']);
}

Platnosť tokenu overíme pomocou metódy isCsrfTokenValid(). Ak je token neplatný, vrátime chybovú hlášku.

{# templates/home/index.html.twig #}

{% extends 'base.html.twig' %}

{% block title %}Home page{% endblock %}

{% block body %}

    <section class="ui container">

        <form class="ui form" action="{{ path('process-form') }}" method="post">

            <input type="hidden" name="token" value="{{ csrf_token('myform') }}">

            <div class="field">
                <label>Name</label>
                <input name="name" type="text">
            </div>

            <div class="field">
                <label>Email</label>
                <input name="email" type="text">
            </div>

            <button class="ui button" type="submit">Send</button>

        </form>

    </section>

{% endblock %}

Toto je domovská stránka s formulárom.

<input type="hidden" name="token" value="{{ csrf_token('myform') }}">

Máme skrytý input tag s názvom token. Jeho hodnota sa vygeneruje pomocou Twig csrf_token() funkcie.

{# templates/base.html.twig #}

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>
        {% block title %}Welcome!
        {% endblock %}
    </title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
</head>

<body>
    {% block body %}{% endblock %}
</body>


<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js">
</html>

Na záver máme bázovú šablónu.

$ php bin/console server:run

Spustíme server.

$ curl -d "name=Peter&email=peter@example.com" -X POST http://localhost:8000/process
Operation not allowed

Ak sa pokúsime postnúť dáta bez tokenu, dostaneme chybovú hlášku Operation not allowed.