4. díl - Dokončení kalkulačky v Nette

PHP Nette Framework Základy Dokončení kalkulačky v Nette

Vítejte u čtvrtého dílu seriálu tutoriálů o tvorbě webových aplikací v PHP frameworku Nette. V tomto díle navážeme na tvorbu kalkulačky z minulého dílu a dokončíme ji. Minule jsme tedy dokončili model a řekli jsme si, že se můžeme přesunout na presenter, takže bez dalšího otálení jdeme na to! :)

Presenter

V šabloně Nette webového projektu máme v složce app/presenters/ již vytvořený souborHomepagePresener.php. My ho však smažeme nebo přejmenuje a vytvoříme si zde náš vlastní CalculatorPresenter.php a v něm třídu CalculatorPresenter, která rozšiřuje Nette\Application\UI\Presenter. Tím jsme si vytvořili základní kostru presenteru, do které ještě doplníme výchozí metodu pro vykreslování šablony a ve výsledku bude tedy vypadat takto:

<?php

namespace App\Presenters;

use Nette\Application\UI\Presenter;
use App\Model\CalculatorManager;
use Nette\Application\UI\Form;
use Nette\Utils\ArrayHash;

/**
 * Presenter kalkulačky.
 * @package App\Presenters
 */
class CalculatorPresenter extends Presenter
{
    /** @var int|null výsledek operace nebo null */
    private $result = null;

    /** Výchozí vykreslovací metoda tohoto presenteru. */
    public function renderDefault()
    {
        // Předání výsledku do šablony.
        $this->template->result = $this->result;
    }
}

Jak vidíte, předáváme zde výsledek do šablony, abychom ho mohli zobrazit, ale výsledek je zatím pouze null. To ale za chvíli napravíme. ;)

Získání modelu z presenteru

K tomu, abychom získali výsledek, budeme ale potřebovat přístup k modelu. Ten se v Nette standardně zajišťuje pomocí DI (Dependency Injection) a to tak, že v konfiguračním souboru config.neon zaregistrujeme náš model jako anonymní službu (service) a pak si ho necháme injektovat do našeho presenteru. Nejdříve tedy najdeme soubor app/config/congif.neon a upravíme:

…
services:
        - App\Model\CalculatorManager
        router: App\RouterFactory::createRouter

Nyní máme několik možností, jak a kde si nechat službu injektovat, já si zvolím standardní a asi nejpoužívanější postup a to přes konstruktor presenteru. Takže naší třídy CalculatorPresenter přidáme tento kód:

/** @var CalculatorManager Instance třídy modelu pro práci s operacemi kalkulačky. */
private $calculatorManager;

/**
 * Konstruktor s injektovaným modelem pro práci s operacemi kalkulačky.
 * @param CalculatorManager $calculatorManager automaticky injektovaná třída modelu pro práci s operacemi kalkulačky
 */
public function __construct(CalculatorManager $calculatorManager)
{
    parent::__construct();
    $this->calculatorManager = $calculatorManager;
}
…

Teď máme přístup k našemu modelu přes $this->calculatorManager.

Nette formuláře v Presenterech

Nyní máme už vše potřebné pro výpočet a předání výsledku do šablony a je potřeba se zamyslet nad tím, jak budeme získávat data pro výpočet od uživatele. Odpověď je jednoduchá, přes formulář. To znamená, že si potřebujeme v presenteru zadefinovat formulář, který pak předáme do šablony a ze kterého získáme data, která předáme modelu pro výpočet a výsledek uložíme do $this->result. Nette nám pro tento účel přináší systém komponent a speciálně pro formuláře v presenterech má již připravenou třídu Nette\Application\UI\Form. Nám už jen zbývá pomocí ní zadefinovat podobu formuláře a funkci pro jeho zpracování:

/** Definice konstant pro zprávy formuláře. */const
    FORM_MSG_REQUIRED = 'Tohle pole je povinné.',
    FORM_MSG_RULE = 'Tohle pole má neplatný formát.';
…
/**
 * Vrátí formulář kalkulačky.
 * @return Form formulář kalkulačky
 */
protected function createComponentCalculatorForm()
{
    $form = new Form;
    // Získáme existující operace kalkulačky a dáme je do výběru operací.
    $form->addRadioList('operation', 'Operace:', $this->calculatorManager->getOperations())
        ->setDefaultValue(CalculatorManager::ADD)
        ->setRequired(self::FORM_MSG_REQUIRED);
    $form->addText('x', 'První číslo:')
        ->setType('number')
        ->setDefaultValue(0)
        ->setRequired(self::FORM_MSG_REQUIRED)
        ->addRule(Form::INTEGER, self::FORM_MSG_RULE);
    $form->addText('y', 'Druhé číslo:')
        ->setType('number')
        ->setDefaultValue(0)
        ->setRequired(self::FORM_MSG_REQUIRED)
        ->addRule(Form::INTEGER, self::FORM_MSG_RULE)
        // Ošetříme dělení nulou.
        ->addConditionOn($form['operation'], Form::EQUAL, CalculatorManager::DIVIDE)
        ->addRule(Form::PATTERN, 'Nelze dělit nulou.', '^[^0].*');
    $form->addSubmit('calculate', 'Spočítej výsledek');
    $form->onSuccess[] = [$this, 'calculatorFormSucceeded'];
    return $form;
}

/**
 * Funkce se vykonaná při úspěšném odeslání formuláře kalkulačky a zpracuje odeslané hodnoty.
 * @param Form $form        formulář kalkulačky
 * @param ArrayHash $values odeslané hodnoty formuláře
 */
public function calculatorFormSucceeded($form, $values)
{
    // Necháme si vypočítat výsledek podle zvolené operace a zadaných hodnot.
    $this->result = $this->calculatorManager->calculate($values->operation, $values->x, $values->y);
}

Vezmeme to hezky popořadě. První funkce je speciální funkcí pro tvorbu komponent, která se definuje jako createComponent<název_komponenty> a vrací požadovanou komponentu. Jen upozorňuji, že u této metody stačí, aby byla protected. Pokud metoda splňuje tyto podmínky, můžeme jí poté zavolat přímo ze šablony a to pomocí speciálního Latte makra control, což si za chvíli ukážeme.

Dále pár slov k samotnému formuláři. Definujeme zde celkem 4 prvky:

  1. Pole radio inputů, které slouží pro výběr operace výpočtu. Zde si můžete všimnout volání metody z modelu, která nám vrátí univerzálně všechny dostupné operace. Dále nastavujeme výchozí hodnotu na sčítání a že je povinné toho pole vyplnit.
  2. Text input pro zadání prvního čísla, kde nastavíme, že se jedná o typ inputu number, což platí pro prohlížeče podporující HTML5, dále výchozí hodnotu 0 a validační pravidlo, že musí toto pole musí být vyplněno a to pouze hodnotou celého čísla.
  3. Text input pro zadání druhého čísla, který je identický s tím prvním, pouze obsahuje navíc pravidlo, kde ošetříme dělení nulou, jak jsem slíbil. Teď by asi byla na místě diskuse, proč jsem to ošetřil tady a ne již v modelu. Samozřejmě to lze udělat, já jsem se ale rozhodl demonstrovat to, že presenter by měl zpracovat všechna data z šablony a do modelu už by měla jít jen ta data, se kterými tento model umí pracovat. Navíc zde může dojít k validaci již na straně uživatele a celkově je tento způsob efektivnější, než například vyhazovat nějaké výjimky z modelu. :)
  4. Submit button jednoduché tlačítko pro odeslání formuláře.

Nakonec se formuláři ještě předá funkce, která se volá při jeho úspěšném odeslání a do parametru dostane uživatelem zadané hodnoty. Chtěl bych zde upozornit na to, že úspěšné odeslání formuláře znamená jeho kompletní validaci na straně klienta i serveru podle zadaných pravidel tzn. že do metody calculatorFormSucceeded se dostanou už jen hodnoty, se kterými umí náš model pracovat. V této metoda pak stačí jen zavolat univerzální výpočetní funkci z modelu, předat jít potřebné hodnoty od uživatele a výsledek uložit do $this->result. Nyní je náš presenter již kompletní, ovšem cesta k němu není úplně jasná. Pojďme to teď napravit.

Routování v Nette

Jelikož jsem nahradili původní HomepagePresenter naším vlastním, musíme upravit i router, který k němu určoval cestu tj. pod jakou url ho najdeme. V Nette k tomu slouží tzv. RouterFactory a opět věřím, že ti bystřejší z vás si všimli její definice v config.neon při nastavování DI. Nyní jí potřebujeme upravit tak, aby pod výchozí url bych právě náš presenter, takže si otevřeme soubor app/router/RouterFactory.php a přepíšeme ho takto:

<?php

namespace App;

use Nette\Application\Routers\SimpleRouter;

/**
 * Router pro naší aplikaci kalkulačka.
 * @package App
 */
class RouterFactory
{
    /**
     * Vrací námi definovaný router.
     * @return SimpleRouter router
     */
    public static function createRouter()
    {
        // Nastavíme výchozí routování na náš presenter.
        return new SimpleRouter('Calculator:default');
    }
}

Nyní je tedy pod výchozí url adresou náš presenter a jeho akce default (renderDefault). Teď již zbývá poslední část skládačky a to je šablona.

Šablona (View)

Jak už bylo řešeno v první díle, Nette používá vlastní šablonovací systém - Latte. V něm si teď vytvoříme šablonu pro naši aplikaci. Jako vzor si necháme původní šablonu, jenom ji trochu upravíme. Nejdříve půjde do app/presenters/templates/ a přejmenujeme složku Homepage na Calculator. V ní se pak nachází soubor default.latte, který poslouží jako šablona pro naši akci default a my ho akorát upravíme:

{block content}
<div id="banner">
        <h1 n:block=title>Kalkulačka</h1>
</div>

<div id="content">
    <h3 n:ifset="$result">Výsledek je: {$result}</h3>
    {control calculatorForm}
</div>

<style>
...

Jak je vidět, <style> zachováme a upravíme pouze kód nad ním. Nejprve upravíme <h1>, který díky makru n:block=title slouží zároveň i jako html <title> pro celou stránku. Pak vymažeme obsah <div id="content"> a doplníme naše vypsání výsledku třeba do <h3>. Pomocí {$result} se zde zobrazí data předaná z presenteru a pomocí makra n:ifset se výsledek zobrazí pouze pokud není null. Celý definovaný formulář pak vykreslíme do šablony pomocí již zmiňovaného makra control a názvu našeho formuláře (metody createComponent*). 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řílohy a projeďte si jak se data předávají z modelu do presenteru a odtud do šablony. 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/MVP objasní seriál Jednoduchý redakční systém v PHP objektově (MVC).

To je pro tento díl opravdu vše, ale nebojte, příště začneme úplně novou pořádnou aplikaci v Nette. A co to bude, to se nechte překvapit. ;)


 

Stáhnout

Staženo 346x (601.56 kB)
Aplikace je včetně zdrojových kódů v jazyce PHP

 

  Aktivity (3)

Článek pro vás napsal Jindřich Máca
Avatar
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. :-)

Jak se ti líbí článek?
Celkem (8 hlasů) :
4.8754.8754.8754.8754.875


 


Miniatura
Předchozí článek
První aplikace v Nette
Miniatura
Všechny články v sekci
Základy Nette frameworku

 

 

Komentáře
Zobrazit starší komentáře (9)

Avatar
Milan Gallas
Redaktor
Avatar
Milan Gallas:

Projekt jsem si stáhnul a spustil. Vyhodilo mi to chybovou hlášku:

Parse Error

syntax error, unexpected '['    search►

Source file

File: ...\app\presenters\CalculatorPresenter.php:89

79:                ->addRule(Form::INTEGER, self::FORM_MSG_RULE);
80:            $form->addText('y', 'Druhé číslo:')
81:                ->setType('number')
82:                ->setDefaultValue(0)
83:                ->setRequired(self::FORM_MSG_REQUIRED)
84:                ->addRule(Form::INTEGER, self::FORM_MSG_RULE)
85:                // Ošetříme dělení nulou.
86:                ->addConditionOn($form['operation'], Form::EQUAL, CalculatorManager::DIVIDE)
87:                ->addRule(Form::PATTERN, 'Nelze dělit nulou.', '^[^0].*');
88:            $form->addSubmit('calculate', 'Spočítej výsledek');
89:            $form->onSuccess[] = [$this, 'calculatorFormSucceeded'];
90:            return $form;
91:        }
92:
93:        /**

Jak je to s tím řádkem

$form->onSuccess[] = [$this, 'calculatorFormSucceeded'];

????

 
Odpovědět 29.9.2015 20:27
Avatar
Odpovídá na Milan Gallas
Martin Konečný (pavelco1998):

Zřejmě máš starší verzi PHP, která nepodporuje zápis pole pomocí hranatých závorek.
Zkus použít

$form->onSuccess[] = array($this, 'calculatorFormSucceeded';

// nebo
$form->onSuccess[] = $this->calculatorFormSucceeded;
 
Odpovědět  +1 29.9.2015 20:53
Avatar
johnsilver2010:

V presenteru při vytváření komponenty to hodí chybu - Call to a member function getOperations() on null . Co dělám špatně ?

 
Odpovědět 9.11.2015 12:39
Avatar
loading84
Člen
Avatar
loading84:

Hm, mi to hlasí tohle:

Nette\InvalidStateException

Service of type App\Forms\SignFormFactory used in @var annotation at App\Presenters\SignPresenter::$factory not found. Did you register it in configuration file?       search►

Source file

Call stack

...\vendor\nette\di\src\DI\Extensions\InjectExtension.php:41    source  Nette\DI\Extensions\InjectExtension::   checkType (arguments)

...\vendor\nette\di\src\DI\Extensions\InjectExtension.php:29    source  Nette\DI\Extensions\InjectExtension->   updateDefinition (arguments)

...\vendor\nette\di\src\DI\Compiler.php:207     source  Nette\DI\Extensions\InjectExtension->   beforeCompile ()

...\vendor\nette\di\src\DI\Compiler.php:141     source  Nette\DI\Compiler->     generateCode (arguments)

...\vendor\nette\bootstrap\src\Bootstrap\Configurator.php:268   source  Nette\DI\Compiler->     compile ()

inner-code      Nette\Configurator->    generateContainer (arguments)

...\vendor\nette\di\src\DI\ContainerLoader.php:113      source  call_user_func_array (arguments)

...\vendor\nette\di\src\DI\ContainerLoader.php:78       source  Nette\DI\ContainerLoader->      generate (arguments)

...\vendor\nette\di\src\DI\ContainerLoader.php:43       source  Nette\DI\ContainerLoader->      loadFile (arguments)

...\vendor\nette\bootstrap\src\Bootstrap\Configurator.php:222   source  Nette\DI\ContainerLoader->      load (arguments)

...\app\bootstrap.php:19        source  Nette\Configurator->    createContainer ()

 9:
10:    $configurator->setTempDirectory(__DIR__ . '/../temp');
11:
12:    $configurator->createRobotLoader()
13:        ->addDirectory(__DIR__)
14:        ->register();
15:
16:    $configurator->addConfig(__DIR__ . '/config/config.neon');
17:    $configurator->addConfig(__DIR__ . '/config/config.local.neon');
18:
19:    $container = $configurator->createContainer();
20:
21:    return $container;
22:
...\www\index.php:3     source  require (arguments)

Exception

Environment

HTTP request

HTTP response

Report generated at 2015/11/11 18:55:21
http://localhost/…lator-nette/
PHP 5.6.3
Apache/2.4.10 (Win32) OpenSSL/1.0.1i PHP/5.6.3

 
Odpovědět 11.11.2015 19:00
Avatar
Odpovídá na loading84
Martin Konečný (pavelco1998):

Jak vypadají tvé configy? Přesněji sekce services. Nette ti hlásí, že se nenašla žádná třída se jménem App\Forms\Sig­nFormFactory

 
Odpovědět 11.11.2015 19:13
Avatar
loading84
Člen
Avatar
loading84:

Po přenatavení services funguje....

services:
        - App\Model\CalculatorManager
          s timhle mi to řve chybu tak jsem to dal pryč ///- App\Model\UserManager
        - App\Forms\SignFormFactory
        router: App\RouterFactory::createRouter
 
Odpovědět 11.11.2015 20:06
Avatar
loading84
Člen
Avatar
loading84:

hm, tak jsem měl dneska školení v nete a nikde tady není psané že se v souboru app/config/con­gif.neon přidavají service pomocí tabulatoru.

services:
        - App\Model\CalculatorManager
        router: App\RouterFactory::createRouter
<style>
        html { font: normal 18px/1.3 Georgia, "New York CE", utopia, serif; color: #666; -webkit-text-stroke: 1px rgba(0,0,0,0); overflow-y: scroll; }
        body { background: #3484d2; color: #333; margin: 2em auto; padding: 0 .5em; max-width: 600px; min-width: 320px; }
        a { color: #006aeb; padding: 3px 1px; }
        a:hover, a:active, a:focus { background-color: #006aeb; text-decoration: none; color: white; }
        #banner { border-radius: 12px 12px 0 0; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAB5CAMAAADPursXAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAGBQTFRFD1CRDkqFDTlmDkF1D06NDT1tDTNZDk2KEFWaDTZgDkiCDTtpDT5wDkZ/DTBVEFacEFOWD1KUDTRcDTFWDkV9DkR7DkN4DkByDTVeDC9TDThjDTxrDkeADkuIDTRbDC9SbsUaggAAAEdJREFUeNqkwYURgAAQA7DH3d3335LSKyxAYpf9vWCpnYbf01qcOdFVXc14w4BznNTjkQfsscAdU3b4wIh9fDVYc4zV8xZgAAYaCMI6vPgLAAAAAElFTkSuQmCC); }
        h1 { font: inherit; color: white; font-size: 50px; line-height: 121px; margin: 0; padding-left: 4%; background: url(http://files.nette.org/images/logo-nette@2.png) no-repeat 95%; background-size: 130px auto; text-shadow: 1px 1px 0 rgba(0, 0, 0, .9); }
        @media (max-width: 600px) {
                h1 { background: none; font-size: 40px; }
        }
        #content { background: white; border: 1px solid #eff4f7; border-radius: 0 0 12px 12px; padding: 10px 4%; overflow: hidden; }
        h2 { font: inherit; padding: 1.2em 0; margin: 0; }
        img { border: none; float: right; margin: 0 0 1em 3em; }
</style>

Nějaka podobna blbost platí u stylu... musí tam být mezera html {/mezera/ font: normal 18px/1.3 Georgia,

když ne tak to hodí chybu.

 
Odpovědět  +1 16.11.2015 17:33
Avatar
Mazwor
Člen
Avatar
Mazwor:

Při práci s fomulářem jsem musel v presenteru (CalculatorPre­senter) kód upravit tak, že buď při každém odkazu na třídu form nestačilo napsat Form, ale bylo zapotřebí Nette\Applica­tion\UI\Form, případně jsem musel třídu na začátku načíst pomocí

use Nette\Application\UI\Form;

Stejně tomu je i při volání modelu z presenteru a podobně. Např. začátek presenteru pak vypadal následovně:

<?php

namespace App\Presenters;

use Nette;
use App\Model\CalculatorManager;
use Nette\Application\UI\Form;


class CalculatorPresenter ...

Chtěl bych se tedy zeptat, zda to tak dělám správně a autor to pouze neuvádí, protože to nejspíš považuje za samozřejmost, nebo zda mi něco uniká a celá kalkulačka by měla fungovat i bez této drobné úpravy?

Každopádně super seriál, který mě konečně donutil začít s Nette a nepsat zdlouhavě celé projekty čistě v PHP. Díky za něj! :)

Odpovědět 14. března 15:26
Pořádek je pro blbce, inteligent ovládá chaos. :D
Avatar
Jindřich Máca
Tým ITnetwork
Avatar
Odpovídá na Mazwor
Jindřich Máca:

Ahoj,

jsem moc rád, že se Ti seriál líbí. :-) Co se týče Tvého dotazu, tak máš naprostou pravdu a tyto "hlavičky" tam samozřejmě patří. Nějak jsem to opravdu považoval za samozřejmost, ale už jsem pro jistotu poslal ke schválení doplněnou verzi článku. Pokud by Tě zajímalo, jak jsem je konkrétně napsal já, tak samozřejmě v přiloženém archívu u článku jsou v příslušných zdrojový kódech uvedeny. ;-)

 
Odpovědět  +1 15. března 12:04
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 19. Zobrazit vše