Right click to open menu
Nedostatky Nette při přechodu ze Symfony2

Menu bar

Quick Actions

Ribbon

Insert:
Error: Internet connection appears to be offline (0)

Outline

Nedostatky Nette Při Přechodu Ze Symfony2Link to heading
1. Magický Production A Development MódLink to heading
2. KonzoleLink to heading
Logování ChybLink to heading
Produkční MódLink to heading
Promazat CacheLink to heading
3. RoutováníLink to heading
Https/http WebLink to heading
Seznam RoutLink to heading
Práce S HTTP Request/responseLink to heading
4. LatteLink to heading
Globální Proměnné Pro Všechny šAblonyLink to heading
Nastavování Parametrů V PresenteruLink to heading
5. OstatníLink to heading
.Htaccess/web.config V Newebových SložkáchLink to heading
Převod Php Chyb Na VýjimkyLink to heading
Formulářová Komponenta S NETTE InputyLink to heading
DokumentaceLink to heading
ZávěrLink to heading

Document

Nedostatky Nette při přechodu ze Symfony2
Úkol zněl jasně:

Na dalším projektu použij Nette, protože ...
- Ale já jsem v Nette nedělal
To nějak zvládneš

Předchozí projekty, na kterých jsem pracoval, byly v Symfony. Jakmile potom přecházíte z jednoho do druhého, tak se vždycky najde pár věcí, které jsou v jednom či druhém řešeny lépe. Framework nikdy nevyhoví všem (takový ten tweet: “Máte problém? Napíšete framework, teď mají problém i všichni ostatní”), tak jsem sepsal, s čím bojuji v Nette, resp. co by mohlo být řešeno lépe. 

1. Magický production a development mód
Symfony má explicitně oddělený produkční / a development /app_dev.php/. Nette jede development mód na localhostu, na produkci jede produkční mód. Development mód jde vypnout, ale ne zapnout (následky viz konzole). Mít zároveň produkční i development mód na localhostu je oříšek.

Produkční mód na localhostu? Hlavní výhoda Nette je Tracy!
Možná to platí při ladění webů prohlížením stránek na webu (prohlížeč je král). Při TDD vývoji chyby radši vidím v testech, ještě než vůbec zobrazuji něco na webu. Navíc pokud akceptační test jde přes web, tak chci nastolit stejný stav jako u produkčního uživatele (nechci tracy, nechci buildovat kontejner při každém requestu, ...). Zkoušeli jste někdy napsat UI test pro chybovou stránku v Nette? Můj pokus dopadl následovně:
# Then I should see "Neautorizovaný přístup" # nette is in dev mode, errors are not shown
Then I should see "Nette\Application\BadRequestException #401"
Pro odladění chybové stránky v prohlížeči je potřeba vypnout debug mode:
// app/boostrap.php
if (in_array('enableProduction', $config)) {
    $configurator->setDebugMode(Nette\Configurator::NONE);
}
Tak si napiš mechanismus pro zobrazení produkčního módu...
Jestliže aplikace používá framework, tak očekávám stejné chování při přechodu z aplikace A do aplikace B. Když si to každý napíše sám, tak bude mít každá aplikace vlastní řešení (viz https/http web). Proto si myslím, že zobrazení produkce na localhostu by měl řešit samotný framework. Symfony to řeší, Silex to neřeší, ...

2. Konzole
Nette nemá vůbec konzoli (prohlížeč je král), ale existuje integrace pro Symfony konzoli. Ale jak konzoli spustit? Jako Symfony uživatel jsem hledal app/console, bin/console, ale kde nic tu nic. Nakonec Google poradil php www/index.php. Chybějící dokumentace se dá lehce doplnit, s použitelností to bude horší, dokud nebudou autoři používat konzoli. Konzoli jsem dokázal spustit, ale tím boj teprve začíná. Následuje push na CI server.
Logování chyb
CI server rychle spadnul, protože:
ERROR: application encountered an error and can not continue. Error was logged.
Je potřeba vypnout logování chyb, protože v konzoli chci hned vidět chybu ihned bez procházení log adresářů. Stejně tak chci chybu vidět při spuštění testů. Opět s takovou situací dokumentace nepočítá, nakonec rada zní nevolat enableDebugger, aby se neaktivovala Tracy: 
if (in_array('enableDebugger', $config)) {
    $configurator->enableDebugger(__DIR__ . '/../var/log');
}
Produkční mód
CI server funguje, ale při další feature nastává problém:
$ bin/console
PHP Catchable fatal error:  Argument 2 passed to App\Model\SettingsRepository
::__construct() must be an instance of stdClass, none given,
called in /.../var/temp/cache/Nette.Configurator/Container_ce71183ad7.php on line 1796
and defined in /.../app/Model/SettingsRepository.php on line 11
Magický localhost mód je zpět na scéně. Konzole běží v produkčním módu, takže se použije starý nacachovaný kontejner. Teď by to chtělo explicitně zapnout development mód z konzole. Po nahlédnutí do Nette\Configurator jsem to vzdal, protože jsou dostupné módy:
const AUTO = TRUE,
    NONE = FALSE;

// setDebugMode
$this->parameters['debugMode'] = $value;
$this->parameters['productionMode'] = !$this->parameters['debugMode']; // compatibility
$this->parameters['environment'] = $this->parameters['debugMode'] ? 'development' : 'production';
Evidentně development mód z konzole je aktuálně nemožné nastavit, nebo ne?
Tak si poděď Configurator, zkopíruj setDebugMode a nastav dle potřeby
Skvělá rada, ale já nechci řešit implementační detaily frameworku (co znamenají parametry, co znamená komentář compatibility, ...). Vyřeším si to zatím jinak.
promazat cache
Konfigurací to nejde, tak nastává čas pro hrubou sílu, tj. promazání celého temp adresáře a opětovné založení. Zrovna by se hodil integrovaný příkaz pro mazání cache, nechci mazat složku v průzkumníku :)
// bin/console
#!/usr/bin/env php
<?php

exec('(rm -rf var/temp) && (mkdir var/temp) && (touch var/temp/.gitkeep)');

require __DIR__ . '/../vendor/autoload.php';
getService('application')->run();

3. Routování
https/http web
Vyvíjíte web, nasadíte ho na HTTPS web a najednou nic nefunguje, protože dojde k přesměrování na HTTP verzi. Náhodou jsem někde viděl Route::SECURED, takže jsem znal řešení. Ale nechápu, proč Nette přesměrovává HTTP schéma, ať se o to stará webserver. Při přesunu web na HTTPS nechci upravovat všechny aplikace. Co když chci kvůli kompatibilitě nechat API běžet i na HTTP verzi a vypnout ji až po 2 měsících po přechodu na HTTPS? Zbývá jen doufat, že to půjde.

Když už se tak Nette chová, tak by měl alespoň existovat standardní způsob, jak řešit web běžící na HTTP i HTTPS. Nastává zde situace, že si to každý musí napsat sám (někdo pracuje se $_SERVER, někdo si předá parametr do factory, ...). Pro naše účely stačil tento router:
use Nette\Application\Routers\RouteList;
use Nette\Application\Routers\Route;

class RouterFactory
{
    /** @return \Nette\Application\IRouter */
    public static function createRouter()
    {
        $router = new RouteList;
        $router[] = self::route('<presenter>/<action>[/<id>]', 'Homepage:default');
        return $router;
    }

    /** @SuppressWarnings(PHPMD.Superglobals) */
    private static function route($mask, $metadata)
    {
        $isSecured = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off';
        $flags = $isSecured ? Route::SECURED : Route::$defaultFlags;
        return new Route($mask, $metadata, $flags);
    }
}
seznam rout
Nejvíc ze Symfony mi chybí bin/console debug:router. I u úplně cizího projektu mi jediný příkaz odpoví na základní otázky. Jak je projekt velký? Co vlastně dělá? Jaké URL jsou dostupné? Jaké routy existují pro článek?
$ app/console d:r | grep article
 Name                     Method Scheme Host Path                              
 detail                   GET    ANY    ANY  /article/{id}                       
 edit                     ANY    ANY    ANY  /article/{id}/edit  
Zatímco po příchodu do Nette v nejhorším případě v routeru najdu jenom new Route('<presenter>/<action>[/<id>]', 'Homepage:default'). Abych mohl aplikaci proklikat, tak ji musím nejprve zprovoznit (debug:router by měl fungovat i bez databáze, ne?). Najít routy v kódu taky není snadné. Může to být metoda v presenteru začínající na action, render, handle a pak ještě routa může existovat bez metody jen na základě latte šablony. Ještě předtím je ale třeba najít všechny presentery, ne každý totiž dodržuje cestu app/presenters.
Práce s HTTP request/response
Symfony má jasně danou komponentu pro zpracování webových requestů. Např. API vrát response se status kódem 201 a pár dodatečnými informacemi:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

public function jsonAction(Request $request)
{
    return new JsonResponse(
        [
            'scheme' => $request->getScheme(),
            'time' => $request->server->get('REQUEST_TIME', '')
        ],
        201,
        ['Content-Language' => $request->headers->get('Accept-Language', 'default')]
    );
}
Response můžu vytvořit mimo Controller a rovnou ji vrátit jako výsledek. Dokonce nemusím ani použít framework, v souboru web/api.php použiji stejný kód a front controller. Tomu říkám interoperabilita, nezáleží mi na tom, zda je HttpFoundation samostatné repository anebo monorepo s git subtree. V Nette je situace trochu komplikovanější:
use Nette\Application\Responses\JsonResponse;

/** @SuppressWarnings(PHPMD.Superglobals) */
public function actionJson()
{
    $request = $this->getHttpRequest();
    $this->getHttpResponse()
        ->setCode(201)
        ->addHeader('Content-Language', $request->getHeader('Accept-Language', 'default'));
    $this->sendResponse(new JsonResponse([
        'scheme' => $request->getUrl()->getScheme(),
        'time' => array_key_exists('REQUEST_TIME', $_SERVER) ? $_SERVER['REQUEST_TIME'] : ''
    ])); // $this->sendJson([]);
}
Následující připomínky jsou psané z pohledu DX, když jsem vytvářel první API endpoint s Basic autentizací. Práce s response jde proti Command Query Separation, protože JsonResponse vytvořím, ale HttpResponse modifikuji. Musím pracovat s globálním polem. IDE nenapoví, že metody zavolané na getHttpResponse mají fluent interface.  Když jsem poprvé metodu neznal, tak jsem hlavičky nastavil přes funkci header, protože JsonResponse obsahuje jen payload a content-type (ale vypne i expiraci).

4. Latte
Latte je mocnější než Twig, ale pár drobností se najde:
Globální proměnné pro všechny šablony
Globální proměnné jsou zlo nebo ne? Jak kdy, pro mě jsou větší zlo duplicity. V Symfony/Twigu lze lehce přidat globální proměnné pro použití ve všech šablonách. Typický příklad je číslo verze aplikace:
// app/config/config.yml
twig:
    globals:
        appVersion: v1.0

// app/Resources/views/base.html.twig
<title>{{ appVersion }}</title>
V Nette to už jde trochu hůře:
// app/config/config.neon
parameters:
    globals:
        appVersion: v1.0
    
// app/presenters/templates/@layout.latte
<title>{$presenter->context->parameters['globals']['appVersion']}</title> 
Šahat na context je prasárna, udělej BasePresenter a přidej metodu v beforeRender
Ok, BaseCokoliv je taky prasárna. Ne že by kompozice byla vždy nejlepší řešení, ale tady baseNěco nepomůže. Globální proměnnou totiž nemusím použít jen na webu, co třeba v odeslaném e-mailu?
// Symfony: {{ appVersion }}
$this->twig->render('email.twig');

// Nette: {$appVersion}
$latte->renderToString(
    __DIR__ . "/templates/email.latte",
    ['appVersion' => $this->context->parameters['globals']['appVersion']]
)
A už má Nette duplicitu, navíc ješte tahání dat z interního kontextu. Přitom přidání globálních proměnných je změna na pár řádek. Může si to každý napsat sám? Může, ale pak musí proměnné nějak pojmenovat a definovat je v parameters, systémové řešení by je definovalo v sekci latte.
Nastavování parametrů v presenteru
Nette\Application\UI\ITemplate definuje metody render, setFile, getFile. Přitom jediná implementace rozhraní Nette\Bridges\ApplicationLatte\Template má i užitečnou metodu setParameters pro nastavení většího počtu proměnných jedním voláním: 
$this->template->anyVariable = 'any value';
$this->template->setParameters([
    'title' => '...',
    'articles' => ['...']
]);
Ten kód samozřejmě funguje, jenom IDE nedokáže napovědět, že ta metoda existuje.

5. Ostatní
.htaccess/web.config v newebových složkách
Spíš bych podporoval správný přístup, tj. z webu se dostat přímo do složky www. A ne tam dát celou aplikaci a do všech ostatních složek dávat deny from all pro všechny existující webservery. Tohle je určitě hodně subjektivní a není to chyba v Nette, takže já si je smažu a u potřebných složek je nahradím .gitkeep.
Převod php chyb na výjimky
PHP má sice výjimky, ale interní funkce používají vlastní mechanismus chyb přes Notice, Warning, ..., které se špatně zachytávají. Například cizí API vrátí strukturu dat, která je špatně nebo není vůbec zdokumentovaná. Kód je čitelnější, když se soustředí na happy path a ne když dopředu ověřuje všemožné chyby:
$externalData = [];
try {
    $id = $externalData['id']; // $id = array_key_exists('id') ? $externalData['id'] : null
    $name = $externalData['nested']['level']['name'];
} catch (\ErrorException $e) {
    $this->logger->invalidExternalData($externalData, $e);
}
PHPUnit testy můžou projít díky convertNoticesToExceptions="true", ale Nette web mi vždy skončil chybou Notice Undefined index: id. Tracy sice nastavuje set_error_handler, ale docela složitě, takže jsem přešel na jednoduchý handler z tipů Jakuba Vrány:
// app/bootstrap.php
set_error_handler('convertPhpErrorsToExceptions'); // zavolat až po $configurator->enableDebugger

function convertPhpErrorsToExceptions($severity, $message, $filename, $lineno)
{
    if (error_reporting() & $severity) {
        throw new ErrorException($message, 0, $severity, $filename, $lineno);
    }
}
formulářová komponenta s NETTE inputy
Zprovoznit formulářový prvek nad zvolenou entitou je boj. Příklady počítají s tím, že definuji renderování HTML, ale já přitom chci použít normální inputy, selecty, validace a HTML vzhled definovat na jednom místě. V Symfony to není problém, v Nette bylo potřeba hodně hledání a experimentování (kód je zjednodušen):
// app/forms/AddressControl.php
class AddressControl extends \Nette\Forms\Controls\BaseControl
{
    // hack, abych v form.latte zjistil, že getControl nevrátí HTML, ale pole komponent
    public $hasContainer = true;

    /** @var Adresa */
    private $adresa;

    public function setValue($a = null)
    {
        $this->adresa = $a ? $a : new Adresa;
    }

    public function getValue()
    {
        return $this->adresa;
    }

    // BaseControl @return Html|string Generates control's HTML element
    public function getControl()
    {
        $this->container = $this->form->addContainer($this->getName() . 'Container');
        $this->container->addText('psc', 'PSČ')
            ->setValue(
            ->addCondition(Form::FILLED)
            ->addRule(Form::MIN_LENGTH, 'PSČ má mít minimálně %s číslic', 5)
            ->endCondition();
        $this->container->addSelect('obec', 'Obec')->setPrompt('--vyberte--');
        $this->container->addSelect('castObce', 'Část obce')->setPrompt('--vyberte--');
        $this->container->addSelect('ulice', 'Ulice')->setPrompt('--vyberte--');
        $this->container->addText('cisloPopisne', 'Číslo popisné');
        $this->container->addText('cisloOrientacni', 'Číslo orientační');
        return $this->container->getComponents();
    }
}
Dokumentace
Jediný problém v dokumentaci vidím ve velké roztříštěnosti informací. Třeba psaní vlastních formulářových prvků jsem našel na blogu, na nette.org jsem odkaz ani zmínku nezahlédl. Člověk nečetl blog a na githubu se dozví For the details you can have a look at the diff...

“They cannot assume that the user has read or will read any and every blog entry, press release and tweet issued by a company”
The Principled Documentation Manifesto

Závěr
Můj boj se točil hlavně okolo app/bootstrap.php, takže v gistu je k dispozici kód, jak zkrotit módy v konzoli, testech a na webu. Je možné, že popsané nedostatky jsou způsobené mojí neznalostí. V tom případě se rád přiučím, pokud jsem něco řešil zbytečně složitě. 
Conversation

Messages Filter

This region is not screen reader accessible. To read comments within the document, turn on View-Only Mode.
You’re new to this document
Zdenda Drahoš, December 21, 2015 at 1:41 pm
Zdenda Drahoš created the document
·
Dec 21, 2015
Zdenda Drahoš, December 21, 2015 at 1:36 pm
Zdenda Drahoš made 27 edits
·
Dec 21, 2015
Nedostatky Nette při přechodu ze Symfony2

Úkol zněl jasně:

​

Na dalším projektu použij Nette, protože ...
- Ale já jsem v Nette nedělal

To nějak zvládneš

​

Předchozí projekty, na kterých jsem pracoval, byly v Symfony. Jakmile potom přecházíte z jednoho do druhého, tak se vždycky najde pár věcí, které jsou v jednom či druhém řešeny lépe. Framework nikdy nevyhoví všem (takový ten tweet: “Máte problém? Napíšete framework, teď mají problém i všichni ostatní”), tak jsem sepsal, s čím bojuji v Nette, resp. co by mohlo být řešeno lépe.

View Changes
MH
Miloslav Hůla, January 13, 2016 at 7:13 am
Miloslav Hůla
·
Jan 13, 2016

Reakce by Milo https://gist.github.com/milo/1f1cef885a9cb8e247dd

Zdenda Drahoš, February 14, 2016 at 9:08 am
Zdenda Drahoš added to the document
·
Feb 14, 2016
Type a message…