Lekce 4 - Dokončení kalkulačky v Symfony

PHP Symfony Základy Dokončení kalkulačky v Symfony

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

Vítejte u další lekce online kurzu o tvorbě webových aplikací v PHP frameworku Symfony. V tomto tutoriálu navážeme na tvorbu kalkulačky z minulé lekce, První aplikace v Symfony, a dokončíme ji. Minule jsme tedy definovali operace modelu, ale to ještě není všechno, takže bez dalšího otálení jdeme na to! :)

Model

Základní operace bychom měli, ale to není všechno. Model by měl být rozšiřitelný a podle MVC bychom při změně modelu, tj. např. při přidání další operace, nejlépe neměli měnit ani kontroler, či šablonu. Abychom toho dosáhli, bude potřeba ještě několik věcí dodělat.

src/Entity/Ope­ration.php

Symfony standardně reprezentuje své datové zdroje jako tzv. entity. My si tak pro naše operace kalkulačky také definujeme jednu takovou entitu. I když to na první pohled možná není zřejmé, je to výhodný tah nejen z hlediska modelu, ale tuto entitu můžeme dále využít i v rámci Symfony formulářů. Navíc, jak již bylo výše popsáno, přináší nám možnosti snadného rozšíření, či zkrácení modelu o operaci, ba dokonce i o počet členů v dané operaci. Samotná entita pak bude vypadat takto:

<?php

namespace App\Entity;

use App\Model\Calculator;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Definice operace kalkulačky.
 * @package App\Entity
 */
class Operation
{
    /**
     * @var int Operace.
     * @Assert\NotBlank(message = "Tohle pole je povinné.")
     */
    private $operation = Calculator::ADD;

    /**
     * @var int První číslo operace.
     * @Assert\NotBlank(message = "Tohle pole je povinné.")
     * @Assert\Type(
     *     type="int",
     *     message="Hodnota {{ value }} není validní číslo."
     * )
     */
    private $x = 0;

    /**
     * @var int Druhé číslo operace.
     * @Assert\NotBlank(message = "Tohle pole je povinné.")
     * @Assert\Type(
     *     type="int",
     *     message="Hodnota {{ value }} není validní číslo."
     * )
     */
    private $y = 0;

    /**
     * Getter pro operaci.
     * @return int operace
     */
    public function getOperation(): int
    {
        return $this->operation;
    }

    /**
     * Setter pro operaci.
     * @param int $operation operace
     * @return Operation
     */
    public function setOperation(int $operation): self
    {
        $this->operation = $operation;
        return $this;
    }

    /**
     * Getter pro první číslo operace.
     * @return int první číslo operace
     */
    public function getX(): int
    {
        return $this->x;
    }

    /**
     * Setter pro první číslo operace.
     * @param int $x první číslo operace
     * @return Operation
     */
    public function setX(int $x): self
    {
        $this->x = $x;
        return $this;
    }

    /**
     * Getter pro druhé číslo operace.
     * @return int druhé číslo operace
     */
    public function getY(): int
    {
        return $this->y;
    }

    /**
     * Setter pro druhé číslo operace.
     * @param int $y druhé číslo operace
     * @return Operation
     */
    public function setY(int $y): self
    {
        $this->y = $y;
        return $this;
    }

    /**
     * Ošetření dělení nulou.
     * @return bool true jestli se dělí nulou, jinak false
     * @Assert\IsFalse(message = "Nelze dělit nulou.")
     */
    public function isDividedByZero(): bool
    {
        return $this->getOperation() == Calculator::DIVIDE && $this->getY() == 0;
    }
}

Vidíte, že v principu se jedná o klasickou třídu s definicí atributů, jejich getterů a setterů. Co je tady opravdu zajímavé jsou anotace. Pomocí nich si totiž tato třída hlídá, jaká data lze do jejích atributů uložit. V neposlední řadě také ošetřuje dělení nulou tak, že nemůže být nastavena operace dělení a zároveň v děliteli uložená nula. Pokud by vás dále do detailu zajímala všechna tato validační pravidla, odkáži vás na oficiální dokumentaci.

src/Model/Cal­culator.php

Určitě jste si všimli konstant, které naše entitní třída využívá a pocházejí ze třídy Calculator. My si je tam nyní doplníme a zároveň ještě dodáme metody pro práci s naší entitou operace:

...
/** Definice konstant pro operace. */
const
    ADD = 1,
    SUBTRACT = 2,
    MULTIPLY = 3,
    DIVIDE = 4;

/**
 * Getter pro existující operace.
 * @return array asociativní pole konstant pro operace a jejich slovního pojmenování
 */
public function getOperations(): array
{
    return [
        'Sčítání' => self::ADD,
        'Odčítání' => self::SUBTRACT,
        'Násobení' => self::MULTIPLY,
        'Dělení' => self::DIVIDE
    ];
}

/**
 * Zavolá zadanou operaci a vrátí její výsledek.
 * @param Operation $operation zadaná operace
 * @return int|null výsledek operace nebo null, pokud zadaná operace neexistuje
 */
public function calculate(Operation $operation): ?int
{
    $x = $operation->getX();
    $y = $operation->getY();

    switch ($operation->getOperation()) {
        case self::ADD:
            return $this->add($x, $y);
        case self::SUBTRACT:
            return $this->subtract($x, $y);
        case self::MULTIPLY:
            return $this->multiply($x, $y);
        case self::DIVIDE:
            return $this->divide($x, $y);
        default:
            return null;
    }
}
...

Nyní bychom měli být schopni přidat další operaci do modelu a přitom v kontroleru i v šabloně nemusíme změnit v podstatě nic. A jelikož je to z modelu již opravdu všechno, můžeme se přesunout právě na kontroler.

Získání modelu z presenteru

Když je řeč o kontroleru, je jasné, že v něm budeme potřebovat zajistit přístup k našemu modelu. To se v Symfony standardně řeší pomocí DI (Dependency Injection). To pro nás znamená, že díky standardní konfiguraci v souboru config/services.yml, si budeme moci na požádání nechat injektovat náš model do našeho kontroleru.

Kontroler

Nyní se přesuneme na vytváření kontroleru.

src/Controller/Cal­culatorContro­ller.php

V našem Symfony projektu si ve složce src/Controller/ vytvoříme soubor CalculatorController.php a v něm třídu CalculatorController, která rozšiřuje Symfony\Bundle\FrameworkBundle\Controller\Controller. Tím jsme si vytvořili základ kontroleru, do které ještě doplníme výchozí metodu pro vykreslování šablony a webový formulář naší kalkulačky. Ve výsledku bude tedy vypadat takto:

<?php

namespace App\Controller;

use App\Entity\Operation;
use App\Model\Calculator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Kontroler kalkulačky.
 * @package App\Controller
 */
class CalculatorController extends AbstractController
{
    /**
     * Výchozí vykreslovací metoda tohoto kontroleru.
     * @param Request    $request    HTTP požadavek
     * @param Calculator $calculator Model pro práci s operacemi kalkulačky.
     * @return Response HTTP odpověď
     * @Route("/", name="homepage")
     */
    public function index(Request $request, Calculator $calculator): Response
    {
        $calculatorForm = $this->createFormBuilder(new Operation())
            ->add('operation', ChoiceType::class, [
                'label' => 'Operace:',
                'choices' => $calculator->getOperations(),
                'expanded' => true,
                'multiple' => false
            ])
            ->add('x', null, ['label' => 'První číslo:'])
            ->add('y', null, ['label' => 'Druhé číslo:'])
            ->add('calculate', SubmitType::class, ['label' => 'Spočítej výsledek'])
            ->getForm();

        // Zpracování formuláře kalkulačky.
        $calculatorForm->handleRequest($request);
        if ($calculatorForm->isSubmitted() && $calculatorForm->isValid())
            $result = $calculator->calculate($calculatorForm->getData());

        // Vykreslení šablony s předáním formuláře a výsledku.
        return $this->render('calculator/index.html.twig', [
            'calculatorForm' => $calculatorForm->createView(),
            'result' => isset($result) ? $result : null
        ]);
    }
}

Zde je potřeba se pozastavit hned nad několika věcmi. První z nich je routování, které je v Symfony standardně řešeno opět pomocí anotací. Rozebírat zde všechny možnosti asi nemá úplně smysl, my se s nimi budeme postupně seznamovat v průběhu seriálu, pokud by byl někdo hodně zvědavý, opět odkáži na oficiální dokumentaci.

Dále zde můžete vidět získání modelu pomocí DI v parametru metody index(). To asi nevyžaduje další komentář.

Poté zde vidíme vytváření a zpracování formuláře naší kalkulačky. Zpracování je poměrně přímočaré s delegací na náš model. U vytváření bych však poukázal na skutečnost, že celý formulář je vytvářen pomocí tzv. builder, což je další ze standardních Symfony postupů, přímo nad naší entitou operace. To umožňuje automatické doplnění typů a validace polí formuláře právě na základě typů a validace dat v entitě samotné.

Nakonec vidíte vykreslení příslušné šablony s předáním formuláře a případného výsledku, na kterou se hned vzápětí podíváme. :)

Šablona (View)

Z první lekce již víme, že Symfony používá šablonovací systém - Twig.

templates/cal­culator/index­.html.twig

Konkrétní šablonu vytvoříme následovně. Přejdeme do složky templates/ a vytvoříme zde složku calculator/. V ní pak vytvoříme ještě soubor index.html.twig, který poslouží jako naše šablona a my do něj napíšeme následující:

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

{% block stylesheets %}
    {{ parent() }}
    <style>
        .wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    </style>
{% endblock %}

{% block body %}
    <div class="wrapper">
        <h1>{% block title %}Kalkulačka{% endblock %}</h1>

        {% if result %}
            <h2>Výsledek je: {{ result }}</h2>
        {% endif %}

        {{ form(calculatorForm) }}
    </div>
{% endblock %}

Jak je vidět, začneme definicí bloku pro CSS, kde pomocí volání parent() zachováme případné styly z nadřazené šablony base.html.twig. Dále nejprve upravíme <h1>, který bude sloužit zároveň i jako html element <title> pro celou stránku. Přepíšeme zbylý obsah na naše vypsání výsledku třeba do <h2> a pomocí {{ result }} zde zobrazíme data předaná z kontroleru. Nesmíme zapomenout na podmínku, že výsledek se zobrazí pouze pokud není null. Celý definovaný formulář pak vykreslíme do šablony pomocí funkce form() a názvu našeho formuláře ('calculatorForm').

A tím je naše práce na šabloně i na celém projektu hotová. Pokud se teď podíváte na URL projektu, uvidíte zde plně funkční kalkulačku a můžete na ní vyzkoušet všechny chytáky, které vás napadnou. :)

Pokud vám není cokoli jasné, stáhněte si projekt z přiloženého archivu a podívejte na řešení. Kódu v aplikaci tolik nemáme, po chvíli byste se v principu měli zorientovat. Pokud ani to nepomůže, určitě vám MVC ještě objasní seriál Jednoduchý redakční systém v PHP objektově (MVC).

To je pro dnešní lekci opravdu vše. Ale nebojte, příště, v lekci Jednoduchý redakční systém v Symfony - Struktura projektu, začneme úplně novou pořádnou aplikaci v Symfony :)


 

Stáhnout

Staženo 105x (13.54 MB)
Aplikace je včetně zdrojových kódů v jazyce PHP

 

 

Článek pro vás napsal Jindřich Máca
Avatar
Jak se ti líbí článek?
2 hlasů
Autor se věnuje převážně webovým technologiím, ale má velkou zálibu ve všem vědeckém, nejen ze světa IT. :-)
Aktivity (13)

 

 

Komentáře

Avatar
MArtin
Člen
Avatar
MArtin:23.5.2017 11:14

Debug tip: komu to hadze chybovu hlasku ohladne chybajuceho DefaultControllera, tak je potrebne zmazat cache - bud rucne - obsah adresara ./var/cache/ alebo slusne pouzitim konzoloveho prikazu bin/console cache:clear (prip. php bin/console cache:clear)

Editováno 23.5.2017 11:14
 
Odpovědět  +1 23.5.2017 11:14
Avatar
MArtin
Člen
Avatar
MArtin:23.5.2017 12:29

Debug tip 2: pre tych co si kod prepisovali rucne podla clanku treba pridat do suboru CalculatorMana­ger.php na zaciatok:

use AppBundle\Entity\Operation;

...za namespace ...

 
Odpovědět 23.5.2017 12:29
Avatar
MArtin
Člen
Avatar
MArtin:23.5.2017 12:58

Debug tip 3: zmenit v subore .htaccess v adresari web:

- RewriteRule ^ %{ENV:BASE}/app.php [L]
+ RewriteRule ^ %{ENV:BASE}/app_dev.php [L]

...
- RedirectMatch 302 ^/$ /app.php/
+ RedirectMatch 302 ^/$ /app_dev.php/
Editováno 23.5.2017 12:59
 
Odpovědět 23.5.2017 12:58
Avatar
solta
Člen
Avatar
solta:25.5.2017 1:58

nechápu význam služeb, může mi někdo vysvětlit jaký je rozdíl mezi

$calculatorManager = $this->get('calculator_manager');

a

$calculatorManager = new CalculatorManager();
 
Odpovědět  +1 25.5.2017 1:58
Avatar
Jindřich Máca
Tým ITnetwork
Avatar
Odpovídá na solta
Jindřich Máca:25.5.2017 13:47

Ten hlavní rozdíl je asi v tom, že pomocí toho get() vždy získáš pomocí DI jednu a tu samou instanci, když to pomocí new si pokaždé vytváříš novou.

Další rozdíl je, že pokud by měl konstruktor té získávané třídy další parametry, tak s použitím toho get() je neřešíš a rovnou dostáváš instanci se vším všudy, když to při použití new musíš mít k dispozici všechny potřebné parametry pro vytvoření té instance a to může být často docela složité.

Takže závěrem nejde ani tak o použití na tom jednom místě, ale o způsob získávání napříč celou aplikací. ;)

 
Odpovědět  +1 25.5.2017 13:47
Avatar
solta
Člen
Avatar
Odpovídá na Jindřich Máca
solta:25.5.2017 14:10

Díky za osvětlení. Bylo by fajn podobne veci rovnou vysvětlit v članku jistě nebudu sám kdo to potreboval vysvětlit:)

 
Odpovědět 25.5.2017 14:10
Avatar
Jindřich Máca
Tým ITnetwork
Avatar
Odpovídá na solta
Jindřich Máca:25.5.2017 15:53

V článku je to zmíněné jako princip DI (Dependenci Injection), což je obecný návrhový vzor, s jehož znalostí se tak trošku počítá. :-) To znamená, že pokud někdo zná DI, je mu jasné i to, co jsem napsal ve svém předchozím komentáři. ;-)

 
Odpovědět 25.5.2017 15:53
Avatar
saavikam
Člen
Avatar
saavikam:7.6.2017 9:57

Ahoj,
můžu se prosím zeptat na kod v src/AppBundle/En­tity/Operation­.php, např:

/**
* @var int První číslo operace.
* @Assert\NotBlan­k(message = "Tohle pole je povinné.")
* @Assert\Type(
* type="int",
* message="Hodnota {{ value }} není validní číslo."
* )
*/
protected $x = 0;

je to komentáři. Znamená to, že Symfony si tyto informace přečte z komentáře?

Děkuji
Andrea

 
Odpovědět 7.6.2017 9:57
Avatar
Jindřich Máca
Tým ITnetwork
Avatar
Odpovídá na saavikam
Jindřich Máca:7.6.2017 13:29

Ano, Symfony standardně dělá reflexi nad komentáři a využívá tzv. funkční anotace pro všemožné účely. :-)

 
Odpovědět 7.6.2017 13:29
Avatar
Daniel Gramblička:2. srpna 14:50

Ahoj, pre tych ktori maju problem s typeErrorom - do Calculator.php treba pridat :

use App\Entity\Operation;
 
Odpovědět 2. srpna 14:50
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 10.