Hlavní navigace

Úvod do frameworku Symfony: request-response cyklus

Ján Bodnár

V ďalšom pokračovaní série o frameworku Symfony si povieme niečo viac o request-response cykle, ktorý spracúva komponent HttpFoundation. Popíšeme si aj viaceré funkcie triedy AbstractController.

Doba čtení: 7 minut

Sdílet

Request-response cyklus

Webové aplikácie sú postavené na cykle, ktoré tvoria prúd požiadaviek (requests) a odpovedí (responses). Požiadavky sú generované klientami, na ktoré vytvárajú odpovede webové aplikácie. Centrom požiadaviek a odpovedí je internetový zdroj (resource). Namiesto internetového zdroja sa používa aj užší pojem dokument. Klienti požadujú buď načítanie potrebného zdroja, žiadajú jeho modifikáciu, alebo jeho zmazanie. Jednotlivé webové komponenty medzi sebou komunikujú pomocou HTTP(s) protokolu.

HTTP protokol definuje viacero druhov žiadostí. Nasledujúci zoznam ukazuje najčastejšie používané žiadosti:

  • GET – žiadosť o načítanie zdroja
  • POST – žiadosť poslanie dát na server
  • PUT – žiadosť o modifikáciu zdroja
  • DELETE – žiadosť o zmazanie zdroja
  • HEAD – žiadosť o metainformácie o zdroji

Treba podotknúť, že tieto druhy žiadostí majú odporúčací charakter.

Pojem klienta

Pri vytváraní webových aplikácií sa často používa pojem klient. Tento pojem sa však používa vo viacerých kontextoch, čo môže byť pre začiatočníka mätúce. Pod klientom najčastejšie rozumieme webový prehliadač. Avšak klientom môže byť aj curl utilita, Python skript, alebo iná aplikácia.

Klientom sa často označuje aj časť kódu, typicky ide o funkciu, ktorá volá funkcie nejakej knižnice.

Klientom ďalej označujeme aj hardwér, pomocou ktorého komunikujeme s webovou aplikáciu. Napríklad stolový počítač, notebook, tablet alebo smartphone.

S týmto úzko súvisí aj termín client side programming, programovanie na strane klienta. Pod týmto termínom sa rozumie tvorba HTML, CSS a JavaScript kódu, ktorý sa vykonáva na strane klienta.

Middleware

Middleware je skupina funkcií, ktoré sa vykonajú pri spracovaní požiadaviek a generovaní odpovedí. Obyčajne ide o nejaký režijný kód, ktorý netvorí jadro biznis logiky. Middleware typicky vykonáva tieto úlohy: autentifikácia, zabezpečenie proti CSRF útoku, implementácia logovania, alebo HTTPS presmerovanie. Bežne je middleware implemetovaný tak, že middleware funkcie tvoria spolu reťaz (middleware pipeline). Jednotlivé časti reťaze sa postupne vykonávajú a za sebou volajú; v .NET Core, Express.js, Laravel frameworkoch je to pomocou funkcie next().

Symfony nemá takúto middleware pipeline; namiesto toho používa Event Dispatcher v Symfony HttpKernel komponente. Request-response cyklus je tak riadený udalosťami, na ktoré reagujú registrovaní poslucháči. Interne sa takto Symfony stará o routing pomocou RouterListenera alebo o inicializáciu lokalizácie pomocou LocaleListenera.

Symfony HttpFoundation komponent

HttpFoundation komponent tvorí objektovú vrstvu nad HTTP špecifikáciou. Ukrýva pred nami nízkoúrovňové detaily request-response cyklu, akými sú napríklad PHP globálne premenné. Pre požiadavku máme Symfony\Component\HttpFoundation\Request a pre odpoveď Symfony\Component\HttpFoundation\Response.

HttpFoundation je populárny komponent, ktorý využívajú viaceré známe projekty, ako sú napr. Laravel alebo Drupal. Symfony bol od počiatku projektovaný tak, že využíval a propagoval moderné postupy a normy, vrátane PHP PSR špecifikácií.

Jeden z kľúčových tvorcov frameworku Fabien Potencier preto s nevôľou prijal normu PSR-7, ktorá štandardizuje request-response cyklus. Norma neumožňuje interoperabilitu s HttpFoundation  komponentom a tak nemôže podľa neho byť v Symfony implementovaná. ( HttpFoundation vznikol dávno pred PSR-7 normou.) Symfony poskytuje PSR-7 Bridge, ktorý umožňuje konvertovať v prípade potreby objekty HttpFoundation na PSR-7 message objekty.

Vytvárame objekt požiadavky

Existuje viacero spôsobov, akým môžeme vytvoriť objekt požiadavky v Symfony. V našich príkladoch spracovávame jednoduchú GET požiadavku, ktorú môžeme vytvoriť napríklad pomocou formulára alebo HTML kotvy.

/**
 * @Route("/greet", name="greet")
 */
public function process(): Response
{
    $request = Request::createFromGlobals();
    $name = $request->query->get("name");
    $age = $request->query->get("age");

    $msg = "$name is $age years old";

    return new Response($msg);
}

Príklad ukazuje vytvorenie požiadavky pomocou statickej metódy createFromGlobals(). Potom čo spracujeme dáta z požiadavky, vytvoríme object Response, ktorému zadáme textovú správu, ktorá je určená pre klienta. Pomocou $request->query->get() metódy získame polia atribútu požiadavky.

/**
 * @Route("/greet", name="greet")
 */
public function process(): Response
{
    $request = new Request(
        $_GET,
        $_POST,
        array(),
        $_COOKIE,
        $_FILES,
        $_SERVER
    );

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

    $msg = "$name is $age years old";

    return new Response($msg);
}

V druhom spôsobe zadávame PHP globálne premenné ako parameter konštruktora požiadavky.

/**
 * @Route("/greet", name="greet")
 */
 public function process(Request $request): Response
{
    $name = $request->query->get("name");
    $age = $request->query->get("age");

    $msg = "$name is $age years old";

    return new Response($msg);
}

V treťom spôsobe používame techniku, ktorej hovoríme dependency injection. Tým, že argument funkcie process() obsahuje typehint Request, Symfony pre nás automaticky vytvorí objekt requestu a môžeme ho priamo používať.

Atribúty Request objektu

Request nám zapúzdruje PHP globálne premenné. Pristupujeme k nim pomocou atribútov objektu. Zapúzdrenie (encapsulation) je základný pojem z objektovo orientovaného programovania. Zapúzdrenie združuje dáta a metódy do jedného celku.

  • $request->query; // $_GET
  • $request->request; // $_POST
  • $request->cookies; // $_COOKIE
  • $attributes // uloženie aplikačných dát
  • $request->files; // $_FILES
  • $request->server; // $_SERVER
  • $request->headers; // podmnožina $_SERVER

Každý z týchto atribútov je inštanciou ParameterBag, ktorý je kontainerom párov kľúč/hodnota. Pre prácu s dátami potom používame metódy ParameterBag, ako sú napr. get(), set(), has(), all() alebo keys().

Príklad

V jednoduchom príklade si vytvoríme požiadavku a odpoveď pomocou Symfony HttpFoundation komponentu. Nepôjde o plnohodnotnú Symfony aplikáciu, použijeme len jej dva komponenty.

$ mkdir reqex
$ cd reqex
$ composer req symfony/http-foundation
$ composer req symfony/var-dumper

Vytvoríme si projektový adresár a nainštalujeme symfony/http-foundation a symfony/var-dumper balíčky.

<?php
// request.php

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

require_once __DIR__ . '/vendor/autoload.php';

$request = Request::createFromGlobals();

dump($request->query);

$name = $request->query->get('name');
$age = $request->query->get('age');

$response = new Response("$name is $age years old");
$response->send();

V príklade vypíšeme $request->query parameter bag. Z toho istého bagu vyberieme polia name a age a vytvoríme hlášku, ktorú pošleme späť klientovi v rámci odpovede.

$ php -S localhost:8000

Naštartujeme zabudovaný PHP web server a v prehliadači zadáme URL http://localhost:8000/request.php?name=Peter&age=34. Pomocou daného URL sa vytvoria dva GET request parametre.


Request objekt

AbstractController

AbstractController nám dáva k dispozícii predpripravené funkcie pre renderovanie šablón, forwarding, redirecting, alebo tvorbu JSON odpovedí. Je odporúčanou triedou, z ktorej majú naše controllery dediť. Existuje aj staršia trieda Controller; tá však umožňuje prístup k viacerým detailom frameworku, ku ktorým by nemal mať programátor prístup. Preto sa jej použitie v súčasnosti už neodporúča.

AbstractController poskytuje napríklad tieto funkcie:

protected function redirect(string $url, int $status = 302): RedirectResponse

Funkcia redirect() presmeruje na uvedenú URL.

protected function forward(string $controller, array $path = [],
        array $query = []): Response

Funkcia forward() umožňuje preposlanie requestu na iný controller.

protected function json($data, int $status = 200, array $headers = [],
        array $context = []): JsonResponse

Funkcia json() vráti odpoveď vo formáte JSON.

protected function file($file, string $fileName = null,
        string $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT): BinaryFileResponse

Funkcia file() vráti súbor v odpovedi klientovi.

protected function getParameter(string $name)

Pomocou funkcie getParameter() získame zadefinovaný parameter z konfiguračného súboru.

protected function render(string $view, array $parameters = [],
        Response $response = null): Response

Funkcia render() vygeneruje HTML výstup zo šablóny.

protected function createNotFoundException(string $message = 'Not Found',
        \Exception $previous = null): NotFoundHttpException

Funkcia createNotFoundException() sa používa, keď sa hľadaný zdroj nepodarilo nájsť. Vygeneruje nám výnimku, ktorá vedie k 404 chybovému stavu a patričnej chybovej stránke.

protected function createForm(string $type, $data = null,
        array $options = []): FormInterface

Funkcia createForm() nám vygeneruje formulár, ktorý sa tvorí pomocou buildera.

Post/Redirect/Get (PRG)

Post/Redirect/Get je programovací postup, ktorý zabraňuje viacnásobnému doručeniu formulára. Využíva sa v napríklad v situáciach, keď si objednávame tovar. Týka sa to požiadaviek typu POST. V Symfony v takomto prípade použijeme po úspešnom spracovaní formulára hore spomínanú funkciu redirect(). Funkcia presmeruje na HTML dokument potvrdzujúci úspešnú objednávku. Viacnásobné stlačenie klávesy F5 alebo Refresh buttonu nám opakovane vráti daný HTML dokument.

Príklad – stiahnutie súboru

Vytvoríme si príklad, v ktorom nám aplikácia vráti PDF súbor.

$ composer create-project symfony/skeleton servefile
$ cd servefile
$ composer req server maker --dev
$ composer req annot

Vygenerujeme nový projekt a stiahneme potrebné závislosti.

$ mkdir var/files

V podadresári var vytvoríme nový adresár files, do ktorého nakopírujeme nejaký PDF súbor. V našom prípade to bude súbor s názvom test.pdf.

$ php bin/console make:controller ServeFileController

Vytvoríme si nový controller, ktorý nám vráti v odpovedi PDF súbor.

# config/services.yaml
...
parameters:
    dir.files: '%kernel.project_dir%/var/files'
...

V konfiguračnom súbore services.yaml si zadefinujeme parameter dir.files, ktorý obsahuje názov adresára, kde sa nachádza náš PDF súbor.

<?php
// src/Controller/ServeFileController.php

namespace App\Controller;

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

class ServeFileController extends AbstractController
{
    /**
     * @Route("/serve/file", name="serve_file")
     */
    public function index(): BinaryFileResponse
    {
        $pdfPath = $this->getParameter('dir.files') . '/test.pdf';

        return $this->file($pdfPath, 'test.pdf', ResponseHeaderBag::DISPOSITION_INLINE);
    }
}

Názov adresára získame funkciou getParameter(), ktorej predáme názov zadefinovaného parametra dir.files.

Použijeme pomocnú metódu file(), ktorou pošleme PDF súbor klientovi. Súbor môžme poslať ako prílohu pomocou ResponseHeaderBag::DISPOSITION_ATTACHMENT alebo zobraziť v prehliadači pomocou ResponseHeaderBag::DISPOSITION_INLINE.

$ php bin/console server:run

Spustíme server a do prehliadača zadáme cestu http://127.0.0.1:8000/serve/file.

V ďalšom pokračovaní budeme rozoberať formuláre.