phpFashion

Na navigaci | Klávesové zkratky

Nevěřím statistikám, které si sám nezfalšuji

Václav Novotný připravil infografiku porovnávající aktivitu vývojářů v Nette a Symfony. Rád a zvědavě se podívám, leč bez vysvětlení metriky umí být čísla krutě zrádná. S nadsázkou: při určitém workflow a naivním měření mohu ve statistikách vyjít jako autor 100 % kódu, aniž bych naprogramoval jedinou řádku.

I při přímočarých workflow je poměřování množství komitů zákeřné. Není komit jako komit. Pokud přidáte pětici důležitých komitů a zároveň deset lidí vám opraví překlepy v komentářích, jste co do počtu komitů autorem třetiny kódu. Což ale není pravda, jste autorem celého kódu, opravy překlepů se za autorství (jak ho obvykle vnímáme) nepovažují.

V GITu dále věc komplikují „merge-commits“. Pokud někdo připraví zajímavý komit a vy ho schválíte (tedy vznikne ještě merge-commit), jste autorem poloviny komitů. Ale jaký je vlastně skutečný podíl? Obvykle nulový, schválení je otázka jednoho kliknutí v GitHubu, byť někdy diskusí strávíte víc času, než kdybyste si kód napsal sám, ale neuděláte to, protože potřebujete vývojáře vychovávat.

Proto místo počtu komitů je vhodnější analyzovat jejich obsah. Nejjednodušší je brát v úvahu počet změněných řádek. Ale i to může být zavádějící: pokud vytvoříte 100 řádkovou třídu a někdo jiný soubor s ní jen přejmenuje (či rozdělí na dva), „změnil“ vlastně 200 řádků a opět jste autorem třetiny.

Pokud týden ladíte u sebe několik komitů a až potom je pošlete do repozitáře, jste v počtu změněných řádek v nevýhodě oproti tomu, kdo je pošle hned a teprve poté dolaďuje následujícími komity. Nebylo by tedy od věci analyzovat třeba až souhrny za celý den. Je třeba odfiltrovat i údržbové komity, zejména ty, které mění u všech souborů letopočet nebo verzi v hlavičce.

Do toho přicházejí ještě situace, kdy se automatizovaně kopírují komity z jedné větve do jiné, nebo do jiného repozitáře. Což de facto znemožňuje dělat jakékoliv globální statistiky.

Analýza jednoho projektu je věda, natož ta srovnávací. Docela mi to připomíná skvělý analytický kvíz od Honzy Tichého.


Související: Jak se počítá „Hall of fame“ na nette.org


„dibi vs. Nette Database“ story

Je nejvyšší čas rozseknout FUD a nejasnosti kolem databázových vrstev.

Databázovou vrstvu dibi jsem začal psát cca před devíti lety se záměrem shrnutým v tomto historickém článku. Šlo mi především o to sjednotit API různorodých klientů, ošetřovat chybové stavy, přidat uživatelsky pohodlné bindování parametrů a také dynamické generování základních konstrukcí SQL, jako jsou například podmínky, řazení a INSERT & UPDATE:

$db = new DibiConnection(...); // or via monostate dibi::connect(...)

$pairs = $db->fetchPairs('SELECT id, name FROM countries');

$arr = array(
    'name' => 'John',
    'modified%d'  => time(),
);
$db->query('UPDATE users SET ', $arr, ' WHERE `id`=%i', $id);
// UPDATE users SET `name`='John', `modified`= '2005-10-12' WHERE `id` = 123

Časem se v PHP objevila nativní knihovna PDO, která v podstatě řešila polovinu věcí, co dibi, nicméně její API pro bindování parametrů bylo těžkopádné, neporadilo si s datumy a skládání základních konstrukcí SQL chybělo úplně. Takže dibi nenahradilo.

V dibi jsem si hrál i s experimenty, jako DibiTable, DibiFluent nebo DibiDataSource, ale sám jsem je nikdy nepoužíval. Jsou tam pochopitelně i věci, které bych dnes udělal lépe, ale z hlediska zpětné kompatibility je takřka nemožné do nich zasahovat. Třeba mám zmatek v tom, co který modifikátor znamená – je jich příliš mnoho. (Moc se to neví, ale místo přemýšlení, který modifikátor použít, můžete obvykle použít otazník.)

Protože téměř v každém demu pro Nette bylo potřeba pracovat s databází, vyžadovalo to nainstalovat dibi nebo Doctrine (jiné vrstvy se v podstatě nepoužívají). Dnes je to díky Composeru otázka pár úderů do klávesnice, ale tehdy neexistoval. Přemýšlel jsem proto, že bych si v příkladech vystačil jen s čistým PDO. Jenže pokud jste rozmlsaní z dibi, není návratu zpět. Chybí vám nejen přívětivé API, ale i pohodlí Tracy (tj. Laděnky) pro monitorování dotazů či chyb.

Tehdy mě napadlo, že by nebylo od věci udělat „dibi model 2010“, nově, bez historických zátěží, založené čistě nad PDO. Vyhodit hromadu driverů, všechny modifikátory nahradit jedním otazníkem a implementovat jen vlastnosti, které budou skutečně potřeba.

Nette Database

Takhle vzniklo Nette Database (NDB). Moderní ekvivalent dibi:

$db = new Nette\Database\Connection(...);

$pairs = $db->fetchPairs('SELECT id, name FROM countries');

$arr = array(
    'name' => 'John',
    'modified'  => new DateTime,
);
$db->query('UPDATE users SET ', $arr, ' WHERE `id`= ?', $id);

Brzy jsem narazil na hromadu nedostatků PDO, nareportoval a obešel mraky chyb a když bylo Nette Database odladěné, vyšlo v únoru 2012 jako součást finálního Nette Framework 2.0.

Tady musím zdůraznit, že navzdory šiřitelům FUD v Nette Database skutečně takřka žádné chyby nebyly a jediným větším problém se ukázal bug v PDO způsobující memory leaky, kvůli němuž musely být třídy NDB přepsány a v Nette 2.1 již Connection není potomkem PDO (dědit od PDO byla z dnešního pohledu stejně blbost.)

Dnes nevidím důvod, proč pro nový projekt použít staré dibi namísto NDB. Chybí asi jen:

  • bohatší možnosti skládání v klauzuli WHERE (zatím se nezdá, že by byla potřeba)
  • statická obálka dibi:: (tu v Nette nahrazuje DI Container)
  • samostatnost (vyřeší Nette 2.2)
  • fetchAssoc (tu v Nette nahrazuje NDBT, eventuálně by ji šlo doplnit je v 2.2-dev)

A tím se dostáváme k Nette Database Table (NDBT), prapříčině mnoha zmatků.

Nette Database Table

V prosinci 2010 jsem do tehdy beta verze Nette 2.0 začlenil knihovnu Jakuba Vrány NotORM, ve kterém jsem viděl úžasný nástroj pro dolování dat z databáze:

$pairs = $db->table('countries')->fetchPairs('id', 'name');
// SELECT `id`, `name` FROM `countries`

$name = $db->table('book')->get($id)->author->name;
// SELECT `id`, `author_id` FROM `book` WHERE `id` = 123
// SELECT `id`, `name` FROM `author` WHERE (`author`.`id` IN (456))

$arr = array(
    'name' => 'John',
    'modified'  => new DateTime,
);
$db->table('users')->where('id', $id)->update($arr);
// UPDATE users SET `name`='John', `modified`= '2005-10-12' WHERE `id` = 123

Kód i API jsem upravil tak, aby zapadlo do koncepce Nette, v podstatě z myšlenky využít NotORM v Latte pochází i nápad na tzv. předbíhání budoucnosti, neboli načítání jen těch sloupců, které budou později potřeba, taktéž s Latte může skvěle fungovat koncept NotORM 2.

A právě NotORM v NDB nese označení NDBT. Přičemž její použití je volitelné.

Zapojení do Nette vyvolalo o NDBT resp. NotORM velký zájem a ukázalo se, že byť byla knihovna pro potřeby Jakuba odladěná, pro různorodější požadavky bylo třeba odvést ještě hodně práce. Té se od poloviny roku 2011 ujal Hrach a z velké části původní NDBT přepsal a pečlivě doplnil testy. Knihovna procházela mnohem rychlejším vývojovým cyklem než zbytek frameworku, nicméně nebýt jeho součástí, nevyvíjí se asi vůbec.

Nette tedy má

  • NDB, obdobu dibi, stabilní od verze 2.0.0
  • NDBT, obdobu NotORM, vhodnou pro produkční nasazení až od verze 2.1

Dibi nadále udržuji, koneckonců běží mi na něm většina webů, ale žádné novinky nechystám. V podstatě ani do NDB ne. Naopak s NDBT, jenž pohání mimo jiné i tento blog, má Hrach ambiciózní plány.


Dokumentační úchylnosti

Dobře udržovaný software má mít kvalitní API dokumentaci. Jistě. Ovšem stejným prohřeškem, jakým je absence dokumentace, je i její přebytečnost. U psaní dokumentačních komentářů je totiž potřeba, podobně jako u návrhu API nebo uživatelského rozhraní, přemýšlet.

Přičemž přemýšlením bych nenazýval proces, který se udál v hlavě vývojáře, když doplnil konstruktor tímto komentářem:

class ChildrenIterator
{
    /**
     * Constructor.
     *
     * @param array $data
     * @return \Zend\Ldap\Node\ChildrenIterator
     */
    public function __construct(array $data)
    {
        $this->data = $data;
    }

Šest řádků, které nepřidaly ani jednu jedinou informaci. Místo toho roste

  • vizuální šum
  • duplicita informací
  • objem kódu
  • možnost chybovosti

Nesmyslnost uvedeného komentáře vám možná připadá evidentní, pak jsem rád. Občas totiž dostávám pull requesty, které se snaží podobné smetí do kódu propašovat. Někteří programátoři dokonce používají editory, které takto znečišťují kód automaticky. Au.

Nebo jiný příklad. Zkuste se zamyslet, zda vám komentář prozradil něco, co by bez něj nebylo zřejmé:

class Zend_Mail_Transport_Smtp extends Zend_Mail_Transport_Abstract
{
    /**
     * EOL character string used by transport
     * @var string
     * @access public
     */
    public $EOL = "\n";

S výjimkou anotace @return lze pochybovat o přínosnosti i v tomto případě:

class Form
{
    /**
     * Adds group to the form.
     * @param  string $caption       optional caption
     * @param  bool   $setAsCurrent  set this group as current
     * @return ControlGroup
     */
    public function addGroup($caption = NULL, $setAsCurrent = TRUE)

Pokud používáte výmluvné názvy metod a parametrů (což byste měli), pokud ty ještě navíc mají výchozí hodnoty nebo typehinty, nedá vám tento komentář takřka nic. Buď bych ho zredukoval o informační duplicity, nebo naopak rozšířil.

Ale pozor na opačný extrém, jakým jsou romány v phpDoc:

/**
 * Performs operations on ACL rules
 *
 * The $operation parameter may be either OP_ADD or OP_REMOVE, depending on whether the
 * user wants to add or remove a rule, respectively:
 *
 * OP_ADD specifics:
 *
 *    A rule is added that would allow one or more Roles access to [certain $privileges
 *    upon] the specified Resource(s).
 *
 * OP_REMOVE specifics:
 *
 *    The rule is removed only in the context of the given Roles, Resources, and privileges.
 *    Existing rules to which the remove operation does not apply would remain in the
 *    ACL.
 *
 * The $type parameter may be either TYPE_ALLOW or TYPE_DENY, depending on whether the
 * rule is intended to allow or deny permission, respectively.
 *
 * The $roles and $resources parameters may be references to, or the string identifiers for,
 * existing Resources/Roles, or they may be passed as arrays of these - mixing string identifiers
 * and objects is ok - to indicate the Resources and Roles to which the rule applies. If either
 * $roles or $resources is null, then the rule applies to all Roles or all Resources, respectively.
 * Both may be null in order to work with the default rule of the ACL.
 *
 * The $privileges parameter may be used to further specify that the rule applies only
 * to certain privileges upon the Resource(s) in question. This may be specified to be a single
 * privilege with a string, and multiple privileges may be specified as an array of strings.
 *
 * If $assert is provided, then its assert() method must return true in order for
 * the rule to apply. If $assert is provided with $roles, $resources, and $privileges all
 * equal to null, then a rule having a type of:
 *
 *    TYPE_ALLOW will imply a type of TYPE_DENY, and
 *
 *    TYPE_DENY will imply a type of TYPE_ALLOW
 *
 * when the rule's assertion fails. This is because the ACL needs to provide expected
 * behavior when an assertion upon the default ACL rule fails.
 *
 * @param  string                                  $operation
 * @param  string                                  $type
 * @param  Zend_Acl_Role_Interface|string|array  $roles
 * @param  Zend_Acl_Resource_Interface|string|array $resources
 * @param  string|array                          $privileges
 * @param  Zend_Acl_Assert_Interface                $assert
 * @throws Zend_Acl_Exception
 * @uses   Zend_Acl_Role_Registry::get()
 * @uses   Zend_Acl::get()
 * @return Zend_Acl Provides a fluent interface
 */
public function setRule($operation, $type, $roles = null, $resources = null, $privileges = null,
                        Zend_Acl_Assert_Interface $assert = null)

Vygenerovaná API dokumentace je pouhá referenční příručka, nikoliv kniha, kterou by si člověk četl před spaním. Litanie sem skutečně nepatří.

Asi nejoblíbenějším místem, kde se lze dokumentačně vyřádit, jsou hlavičky souborů:

<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package Zend_Db
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd    New BSD License
 * @version $Id: Abstract.php 25229 2013-01-18 08:17:21Z frosch $
 */

Kolikrát se zdá, že záměrem je hlavičku natáhnout tak, aby po otevření souboru vůbec nebyl vidět kód. K čemu je 10řádková informace o licenci New BSD, obsahující klíčové zvěsti, jako že její znění najdete v souboru LICENSE.txt, že je dostupná přes world-wide-web a pokud náhodou nedisponujete moderními výstřelky, jako je tzv. webový prohlížeč, máte odeslat email na license@zend.com a oni vám ji okamžitě pošlou? Navíc v balíku zopakovaná 4400×. Schválně jsem žádost zkusil poslat, ale odpověď nepřišla :-)

Též uvedení letopočtu v copyrightu vede k vášni dělat komity jako update copyright year to 2014, které změní všechny soubory, což komplikuje porovnávání verzí.

Je vůbec potřeba uvádět v každém souboru copyright? Z právního hlediska to potřeba není, nicméně pokud open source licence dovolují uživatelům používat části kódu s tím, že musí zachovat copyrighty, je vhodné je tam mít. Stejně tak je užitečné v každém souboru uvádět, z jakého produktu pochází, pomůže to lidem v orientaci, když na něj jednotlivě narazí. Dobrým příkladem je třeba:

/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

Přemýšlejte proto prosím nad každým řádkem, jestli skutečně má pro uživatele přínos. Pokud ne, jde o smetí, které nemá v kódu co dělat.

(Prosím případné komentátory, aby článek nevnímali jako souboj frameworků, tím rozhodně není.)


Formuláře v Nette 2.1

Jaké novinky přináší Nette Framework 2.1 ve formulářích? Začnu perličkou: formuláře byly představeny před sedmi lety, jaksi mimochodem, v komentářích pod jiným článkem, a jejich kód včetně dema se vešel do 800 řádků. Už tehdy šlo o stěžejní část frameworku, která uměla třeba vedle serverové validace generovat i tu JavaScriptovou.

Původní návrh založený na explicitním vyjmenovávání prvků se ukázal jako správný, zejména když propukla kauza se zranitelností Mass Assignment, kterou trpěly weby psané v Rails a jimi inspirovaných frameworcích. Na druhou stranu, tvorbu některých dynamických formulářů to činilo těžkopádnou. Dlouho jsem hledal kompromis a pak to přišlo:

Low-level formuláře

Nyní lze používat i prvky, které zapíšeme pouze v šabloně a nepřidáme je do formuláře některou z metod $form->addXyz(). Když například vypisujeme záznamy z databáze a dopředu nevíme, kolik jich bude a jaké budou mít ID, a chceme u každého řádku zobrazit checkbox nebo radio button, stačí jej nakódovat v šabloně:

{foreach $items as $item}
    <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p>
{/foreach}

A po odeslání hodnotu zjistíme:

$values = $form->getHttpData($form::DATA_TEXT, 'sel[]');

kde první parametr je typ elementu (DATA_FILE pro type=file, DATA_LINE pro jednořádkové vstupy jako text, password, email apod. a DATA_TEXT pro všechny ostatní) a druhý parametr sel[] odpovídá HTML atributu name.

Podstatné je, že getHttpData() vrací sanitizovanou hodnotu, v tomto případě to bude vždy pole validních UTF-8 řetězců, ať už se pokusíte serveru podstrčit cokoliv. Jde o obdobu přímé práce s $_POST nebo $_GET avšak s tím podstatným rozdílem, že vždy vrací čistá data, tak, jak jste zvyklí u standardních prvků Nette formulářů.

CheckboxList

Nový prvek pro výběr z více možností je CheckboxList. Stejně jako v případě selectboxů nebo radiolistů kontroluje, zda odeslané hodnoty jsou z těch, které nabízíme:

$form = new Form;
$form->addCheckboxList('colors', 'Favorite colors:', array(
    'r' => 'red',
    'g' => 'green',
    'b' => 'blue',
));

Multiple file upload

Najednou lze uploadovat i více souborů, všimněte si TRUE:

$form = new Form;
$form->addUpload('avatar', 'Picture:', TRUE);

Zároveň formuláře mají integrovanou kontrolu, zda nebyl překročen povolený limit velikosti odesílaných dat.

Nové vykreslovací zbraně

Velmi snadno můžete propojit formulář s existující šablonou. Stačí jen doplnit atributy n:name:

function createComponentSignInForm()
{
    $form = new Form;
    $form->addText('user')->setRequired();
    $form->addPassword('password')->setRequired();
    $form->addSubmit('send');
    return $form;
}
<form n:name=signInForm class=form>
    <p><label n:name=user>Username: <input n:name=user size=20></label>
    <p><label n:name=password>Password: <input n:name=password></label>
    <p><input n:name=send class="btn btn-default">
</form>

Atribut n:name lze používat i s elementy <select>, <button> nebo <textarea>.

Dále můžete vykreslovat prvky jako je RadioList, Checkbox nebo nový CheckList pěkně po jednotlivých HTML elementech. Říká se tomu partial rendering:

{foreach $form[gender]->items as $key => $label}
    <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}

Nebo lze použít klasická makra {input gender:$key} a {label gender:$key}, trik je tom názvu s dvojtečkou.

S tím úzce souvisí i aktualizovaný způsob vykreslování checkboxů a RadioListů. Místo dřívějšího

<label>...</label><input>

se nyní vykreslují v praktičtějším tvaru

<label><input>...</label>

pročež si myslím, že odpadne většina důvodů, proč jste tyto prvky potřebovali vykreslovat po částech.

Zároveň také odpadá nutnost kreslit checkboxy trošku jinak než jiné prvky, tj. myslet na to, aby label byl na správném místě. Metoda getLabel() či makro {label} totiž u checkboxů nyní nevrací nic a getControl() či {input} vrací HTML v onom novém tvaru. Pokud ale potřebujete staré chování, přepněte se do zmíněného partial renderingu přidáním dvojtečky: {label checkbox:} a {input checkbox:}.

Podpora pro Bootstrap

V příkladech najdete ukázky, jak nakonfigurovat vykreslování formulářů pro Twitter Bootstrap 2 a Bootstrap 3.

Chytřejší validátory

Validační pravidla Form::INTEGER, NUMERIC a FLOAT rovnou převádí hodnotu na integer resp. float. A dále pravidlo Form::URL, které akceptuje i řetězec ve tvaru např. nette.org, jej automaticky doplní na plnohodnotné https://nette.org.

Přibyla nová validační pravidla Form::BLANK (prvek nesmí být vyplněn) a Form::NOT_EQUAL.

A v argumentech všech validátorů se můžete dynamicky odkazovat na jiné prvky. Takže třeba tady prvek value musí být v rozmezí určeným aktuálními hodnotami prvků min a max:

$form->addText('min');
$form->addText('max');
$form->addText('value')
    ->addRule($form::RANGE, 'from %d to %d', array($form['min'], $form['max']));

Chybové zprávy

Makro {control form} nyní vypisuje chybové zprávy přímo vedle souvisejících prvků a nad formulářem se objeví jen ty, které žádnému prvku nepřiřadíme (tj. když místo $form['name']->addError() použijeme $form->addError()). Je to mnohem uživatelsky příjemnější a doporučuji, abyste stejným způsobem vykreslovali i formuláře manuálně, třeba takto. Pomůže vám metoda $form->getOwnErrors(), která vrací chybové zprávy přiřazené jen k formuláři.

Píšeme vlastní prvky

Výrazného zjednodušení doznala tvorba vlastních formulářových prvků. Podívejte se na příklad DateInput, což je prvek pro zadávání data. Zobrazovat se bude jako trojice políček den, měsíc, rok a z pohledu API bude přijímat a vracet objekt DateTime.

Interně se datum reprezentuje jako trojice privátních proměnných $day, $month, $year, které metoda getValue() převede na požadovaný objekt DateTime (tedy pokud půjde o platné datum) a setValue() naopak vstup do této trojice rozloží. Přičemž by měla kontrolovat validitu vstupu a v případě chyby vyhodit výjimku.

Výjimky naopak nevyhazuje loadHttpData(), která se volá po odeslání formuláře, a hodnoty, které uživatel odeslal, získá metodou getHttpData() a uloží do zmíněné trojice proměnných. Jen pozor, tentokrát mluvím o metodě třídy BaseControl, nikoliv Form. Každopádně i v tomto případě getHttpData() vrací očištěná data.

A nakonec metoda getControl() generuje HTML. Pokud je prvek reprezentován jedním HTML elementem, jeho atribut name určí metoda getHtmlName(). Jenže máme prvky tři, tak za název ještě dolepíme řetězec [day], [month] a [year] (včetně těch hranatých závorek). Stejný postfix pak uvádíme při volání getHttpData() ve zmíněné loadHttpData().

Co když místo obyčejného textového pole budeme chtít vykreslit selectbox? Pak oceníte funkci Nette\Forms\Helpers::createSelectBox(). Prvním parametrem je pole nabízených hodnot, druhým pole HTML atributů elementu <option>. V příkladu uvedené selected? s otazníkem znamená, že atribut se uvede pouze u položky s uvedenou hodnotou. Šlo by také uvést např. 'title:' => array(1 => 'January', 2 => ...) s dvojtečkou, což dává možnost každé položce dát jiný title.

Existuje také obdobná funkce createInputList() pro generování skupin inputů. Té lze jako třetí parametr předat pole HTML atributů pro element label, taktéž podporující otazník a dvojtečku.

Dále autoři nových prvků mohou ocenit dvě nové abstraktní třídy ChoiceControl a MultiChoiceControl.

A co ještě?

Pomocí $control->setOmitted() vyjmete prvek z dat, která vrací $form->getValues(). To se hodí pro různé hesla pro kontrolu, antispamové prvky atd. I všechny prvky, které označíte jako $form->setDisabled(), budou takto vyjmuty.

Vylepšeno bylo togglování, nyní by mělo fungovat přesně podle očekávání. Navíc $form->getToggles() vrátí informaci o viditelnosti všech id.

Metoda setValue() u jednotlivých prvků kontroluje datový typ a dále v případě SelectBoxů a podobně vás nenechá nastavit hodnotu, která v nabízených není.

V HTML atributech data-nette-rules se používá čistý JSON, takže nezapomeňte nasadit aktuální netteForms.js.

A nakonec – u jednotlivých tlačítek můžete omezit seznam prvků, které se mají při odeslání formuláře tímto tlačítkem validovat:

$form->addSubmit('edit')
    ->setValidationScope(array($form['name'], $form['password']));

Přejděte na Nette 2.1

Před chvílí vyšel Nette Framework 2.1. Ačkoliv číselně jde o desetinkový posun, novinek je hromada. K těm se překvapivě dostanu až v příštích článcích, nyní mi půjde o kompatibilitu.

Na tu vždycky Nette Framework velmi dbal:

  • nikdy mezi verzemi nebyla tlustá čára, vývoj je evoluční
  • ačkoliv je psán v PHP 5.3, generovala se i verze pro PHP 5.2
  • s přechodem na jmenné prostory dostali vývojáři nástroj, jenž jim všechny třídy ve zdrojových kódech přejmenoval

Přechod na verzi 2.1 by měl být snadný. Teď si říkáte: „kilometr dlouhý článek a snadný přechod?“ Inu, snaží se být vyčerpávající. Mám weby, na kterých nebylo potřeba měnit nic.

Byť se pár věcí přejmenovalo, v případě tříd existují aliasy a fungují i staré názvy metod, jen se vypíše či zaloguje upozornění. Nicméně kvůli technikáliím doporučuji stejně těch pár tříd přejmenovat, máte k tomu i nástroj na automatické přejmenování tříd.

Zamáčkněte slzu, Nette Framework 2.1 opouští PHP 5.2, verzi, kterou už 3 roky nepodporuje ani samotné PHP. Minimální požadovaná verze je tak 5.3.1 a Nette by mělo jet prakticky na každém pětrojkovém hostingu (je testováno i na nejnovější 5.5.7).

Minimalizovaná verze se nyní generuje ve formátu PHAR, takže v distribuci místo nette.min.php najdete soubor nette.phar, se kterým se však pracuje úplně stejně.

Nette Database (NDB)

NDB společně s Dependency Injection byly čerstvé části frameworku a bylo zřejmé, že nejvíce změn bude právě tady.

  • Nette\Database\Connection již není potomkem PDO
  • přejmenujte metody exec()query(), fetchColumn()fetchField() a lastInsertId() → getInsertId()
  • Nette\Database\Statement je nyní Nette\Database\ResultSet a též už není potomkem PDOStatement
  • přejmenujte metody rowCount()getRowCount() a columnCount() → getColumnCount()

Používáte Nette Database Table (NDBT), tedy skvělou část NDB, ke které se přistupuje přes $database->table(...)?

  • metoda table() byl přesunuta z Connection do nové třídy Nette\Database\Context. Ta obsahuje obsahuje všechny důležité metody pro práci s databází, takže klidně změňte Connection za Context a máte hotovo.
  • proměnné řádku ActiveRow jsou nyní read-only, pro změnu slouží metoda $row->update(array('field' => 'value')). Věřte, že dřívější chování mělo tolik úskalí, že jiná cesta nebyla.
  • změnila se tzv. backjoin syntaxe z book_tag:tag.name na :book_tag.tag.name (dvojtečka na začátku)
  • místo druhého parametru $having v metodě group() použijte metodu having()

(Pokud jste používali SelectionFactory v dev-verzi, změňte ji také na Context.)

Dependency Injection (DI)

  • třída Nette\Config\ConfiguratorNette\Configurator (původní název zněl, jako když se člověk zakoktá)
  • v konfiguračním souboru se sloučily definice factories a services do společného services. Jen těm, co byly původně factories, přidejte klíč autowired: no.
  • a zavedl se „odrážkový“ zápis anonymních služeb:
services:
    Jmeno\Tridy: self  # dříve, ukázalo se jako matoucí

    - Jmeno\Tridy  # nyní

Pracovat přímo s DI kontejnerem není obvykle dobrý nápad, ale pokud už tak činíte:

  • tovární metody volejte jako $container->createService('nazevsluzby') namísto $container->createNazevSluzby()
  • zavrženy jsou všechny výchozí továrničky jako createLatte(), createCache(), createMail()createBasicForm()
  • a ke službám přistupujte raději přes $container->getService() či getByType() namísto $container->nazevSluzby

Pokud píšete vlastní rozšíření, vězte, že došlo k přejmenování jmenných prostorů Nette\ConfigNette\DI a Nette\Utils\PhpGeneratorNette\PhpGenerator.

Oproti dev-verzi jsou anotace @inject a metody inject() automaticky zpracovány jen na presenterech. Na jiných službách je zapnete uvedením klíče inject: yes v definici.

Používáte-li ještě stařičký Environment, bude po vás vyžadovat nastavenou konstantu TEMP_DIR, kvůli výkonu.

Ufff, máme za sebou tu náročnou část. Teď už to bude brnkačka.

UI\Presenter a Control

  • Presenter nyní zabraňuje, aby vám někdo podstrčil do persistentního parametru pole. Pokud ale pole chcete, uveďte ho jako výchozí hodnotu,
  • zavržené jsou metody getService() (použijte getContext()->getService()), dále getHttpContext()getApplication()
  • magické getParameter(NULL)getParameters()
  • místo divného invalidateControl() lze používat redrawControl()

Tak to je easy, ne? Pojďme si dát Latte.

Latte

  • výchozím režimem je HTML (namísto XHTML), což lze přepnout v konfiguraci
  • automaticky ouvozovkuje atributy v <a title={$title}>, což by nemělo způsobit žádnou komplikaci, ale raději to zmiňuji
  • atribut n:input se mění na n:name, aby šel použít nejen na <input>, ale i label, select, form a textarea
  • zavržená jsou makra {attr} (nahrazuje n:attr) a {assign} → {var}
  • doporučujeme místo vykřičníkového zápisu {!$var} přejít na {$var|noescape}, je to zřejmější
  • pokud jste v dev-verzi používali zkrácený zápis bloků {#block}, tak do 2.1 se nedostal, nebyl srozumitelný

V Latte je novinka, která v <a href={$url}> automaticky kontroluje, zda proměnná $url neobsahuje něco jako javascript:hackniWeb(). Povolené jsou pouze protokoly http, https, ftp, mailto a pochopitelně relativní cesty a kontroluje i atributy src, action, formaction a také <object data=...>. Pokud někde potřebujete vypsat URL bez kontroly, použijte modifikátor |nosafeurl.

A nakonec: drobná změna souvisí s ručním vykreslování checkboxů, ale o tom níže.

Formuláře

Přes obrovskou spoustu novinek ve formulářích je možných nekompatibilit málo.

Checkboxy a RadioListy se nyní vykreslují v praktičtějším tvaru <label><input>...</label> namísto <label>...</label><input>. Jako důsledek u Checkbox metoda getLabel() či {label} nevrací nic a getControl() či {input} HTML v onom novém tvaru. Pokud ale potřebujete staré chování, přepněte se do tzv. partial renderingu přidáním dvojtečky: {label nazevprvku:} a {input nazevprvku:}. Easy.

Makro {control form} nyní vždy vypisuje chybové zprávy u jednotlivých prvků a nad formulářem jsou jen ty nepřiřazené. Doporučujeme to tak dělat i při manuálním vykreslování, třeba takto.

  • setValue() u prvků kontroluje hodnotu a v případě chyby vyhodí výjimku namísto dřívějšího mlčení
  • validační pravidla jako Form::INTEGER, NUMERIC a FLOAT převádí hodnotu na integer resp. float
  • TextArea: zrušeny výchozí hodnoty atributů cols a rows (existovaly jen proto, že to HTML4 vyžadovalo)
  • prvky označené setDisabled() se neobjeví ve $form->getValues() (prohlížeč je totiž vůbec neposílá)
  • zavrženo SelectBox::setPrompt(TRUE), místo TRUE použijte řetězec
  • přejmenováno MultiSelectBox::getSelectedItem()getSelectedItems()
  • v HTML atributech data-nette-rules se používá JSON, takže nezapomeňte nasadit aktuální netteForms.js

Debugger

  • Nette\Diagnostics\Debugger::$blueScreenDebugger::getBlueScreen()
  • a adekvátně $bargetBar(), $loggergetLogger() a $fireLogger → getFireLogger()
  • zavrženo Nette\Diagnostics\Debugger::tryError(), catchError() a také toStringException(), místo kterého použijte obyčený trigger_error()
  • zavrženy interní Nette\Diagnostics\Helpers::clickableDump() a htmlDump(), které nahrazuje nová třída Dumper

Mail

Zavržená metoda Nette\Mail\Message::send(), použijte mailer, viz dokumentace.

ostatní

  • Nette nemusí fungovat s eAccelerator a minifikovaný PHAR nemusí fungovat s APC
  • Nette\Utils\Finder::find($mask) filtruje podle masky nejen soubory, ale i adresáře
  • do Nette\Security\User se v konstruktoru předává autentikátor, pozor na kruhové závislosti
  • v loaderu se už nenastavuje iconv_set_encoding() a mb_internal_encoding()
  • zavrženy konstanty NETTE, NETTE_DIR a NETTE_VERSION_ID
  • a třída Nette\Loaders\AutoLoader
  • a proměnná Nette\Framework::$iAmUsingBadHost
  • doporučujeme přestat používat callback() a třídu Nette\Callback, neboť globální funkce mohou způsobit komplikace
  • přejmenoval se jmenný prostor Nette\Utils\PhpGeneratorNette\PhpGenerator
  • Nette varuje hláškou „Possible problem: you are sending a cookie while already having some data in output buffer,“ pokud se snažíte odeslat HTTP hlavičku nebo cookie a byl již odeslán nějaký výstup – byť do bufferu. Buffer totiž může přetéct a proto to varování.

Vyzkoušejte!

Stáhněte si verzi 2.1 a vyzkoušejte ji! A těšte se na články o novinkách :-)


How to encode and decode JSON in PHP?

Let's create simple OOP wrapper for encoding and decoding JSON in PHP:

class Json
{
    public static function encode($value)
    {
        $json = json_encode($value);
        if (json_last_error()) {
            throw new JsonException;
        }
        return $json;
    }

    public static function decode($json)
    {
        $value = json_decode($json);
        if (json_last_error()) {
            throw new JsonException;
        }
        return $value;
    }
}

class JsonException extends Exception
{
}

// usage:
$json = Json::encode($arg);

Simple.

But it is very naive. In PHP, there are a ton of bugs (sometime called as “not-a-bug”) that need workarounds.

  1. json_encode() is (nearly) the only one function in whole PHP, which behavior is affected by directive display_errors. Yes, JSON encoding is affected by displaying directive. If you want detect error Invalid UTF-8 sequence, you must disable this directive. (#52397, #54109, #63004, not fixed).
  2. json_last_error() returns the last error (if any) occurred during the last JSON encoding/decoding. Sometimes! In case of error Recursion detected it returns 0. You must install your own error handler to catch this error. (Fixed after years in PHP 5.5.0)
  3. json_last_error() sometimes doesn't return the last error, but the last-but-one error. I.e. json_decode('') with empty string doesn't clear last error flag, so you cannot rely on error code. (Fixed in PHP 5.3.7)
  4. json_decode() returns NULL if the JSON cannot be decoded or if the encoded data is deeper than the recursion limit. Ok, but json_encode('null') return NULL too. So we have the same return value for success and failure. Great!
  5. json_decode() is unable to detect Invalid UTF-8 sequence in PHP < 5.3.3 or when PECL implementation is used. You must check it own way.
  6. json_last_error() exists since PHP 5.3.0, so minimal required version for our wrapper is PHP 5.3
  7. json_last_error() returns only numeric code. If you'd like to throw exception, you must create own table of messages (json_last_error_msg() was added in PHP 5.5.0)

So the simple class wrapper for encoding and decoding JSON now looks like this:

class Json
{
    private static $messages = array(
        JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded',
        JSON_ERROR_STATE_MISMATCH => 'Syntax error, malformed JSON',
        JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
        JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
        5 /*JSON_ERROR_UTF8*/ => 'Invalid UTF-8 sequence',
        6 /*JSON_ERROR_RECURSION*/ => 'Recursion detected',
        7 /*JSON_ERROR_INF_OR_NAN*/ => 'Inf and NaN cannot be JSON encoded',
        8 /*JSON_ERROR_UNSUPPORTED_TYPE*/ => 'Type is not supported',
    );


    public static function encode($value)
    {
        // needed to receive 'Invalid UTF-8 sequence' error; PHP bugs #52397, #54109, #63004
        if (function_exists('ini_set')) { // ini_set is disabled on some hosts :-(
            $old = ini_set('display_errors', 0);
        }

        // needed to receive 'recursion detected' error
        set_error_handler(function($severity, $message) {
            restore_error_handler();
            throw new JsonException($message);
        });

        $json = json_encode($value);

        restore_error_handler();
        if (isset($old)) {
            ini_set('display_errors', $old);
        }
        if ($error = json_last_error()) {
            $message = isset(static::$messages[$error]) ? static::$messages[$error] : 'Unknown error';
            throw new JsonException($message, $error);
        }
        return $json;
    }


    public static function decode($json)
    {
        if (!preg_match('##u', $json)) { // workaround for PHP < 5.3.3 & PECL JSON-C
            throw new JsonException('Invalid UTF-8 sequence', 5);
        }

        $value = json_decode($json);

        if ($value === NULL
            && $json !== ''  // it doesn't clean json_last_error flag until 5.3.7
            && $json !== 'null' // in this case NULL is not failure
        ) {
            $error = json_last_error();
            $message = isset(static::$messages[$error]) ? static::$messages[$error] : 'Unknown error';
            throw new JsonException($message, $error);
        }
        return $value;
    }
}

This implementation is used in Nette Framework. There is also workaround for another bug, the JSON bug. In fact, JSON is not subset of JavaScript due characters \u2028and \u2029. They must be not used in JavaScript and must be encoded too.

(In PHP, detection of errors in JSON encoding/decoding is hell, but it is nothing compared to detection of errors in PCRE functions.)


SASS, LESS, Stylus or pure CSS? (3)

Journey into the heart of the three most known CSS preprocessors continues, though not in the way I originally planned.

CSS preprocessor is a tool that take code written in their own syntax and generates the CSS for the browser. The most popular preprocessors are SASS, LESS and Stylus. We have talked about installation and syntax + mixins. All three preprocessors have a fundamentally different way of mixins conception.

Each of them have gallery of finished mixins: For SASS there is a comprehensive Compass, the LESS has framework Twitter Bootstrap or small Elements a Stylus NIB.

… this was opening sentences of article I started write year and quarter ago and never finished. I came to the conclusion that all three preprocessors are useless. They required to do so many compromises that potential benefits seemed insignificant. Today I will explain it.

…pokračování


SASS, LESS, Stylus nebo čisté CSS? (3)

Cesta do nitra tří nejznámějších CSS preprocesorů pokračuje, i když ne tak, jak jsem původně plánoval.

CSS preprocesor je nástroj, který vám ze zdrojového kódu zapsaného ve vlastní syntaxi vygeneruje CSS pro prohlížeč. Mezi nejznámější patří SASS, LESS a Stylus. Ukázali jsme si, jak je nainstalovat a naťukli téma syntaxe a mixinů. Všechny tři preprocesory nabízejí fundamentálně rozdílný způsob, jak programovat s mixiny. Každý je v tom jinak konzistentní a každý umí být jinak matoucí.

Pro každý preprocesor existuje galerie hotových mixinů, do kterých nahlédněte přinejmenším k posouzení jejich srozumitelnosti. Pro SASS existuje komplexní Compass, LESS má framework Twitter Bootstrap nebo drobné Elements a Stylus NIB.

…tak takhle začínal článek, který jsem rozepsal před rokem a čtvrt a nikdy nedokončil. Přišel jsem totiž k závěru, že všechny tři preprocesory jsou, alespoň zatím, nepoužitelné. Jejich nasazení by představovalo tolik ústupků, že by se vedle nich potenciální výhody dočista ztrácely. Dnes to vysvětlím.

…pokračování


Velestručné testování presenterů v Nette

Téma testování presenterů by vydalo na celý seriál, ale ušetříme si čas a místo toho popíšu, jak v několika krocích začít.

Jako testovací framework budu používat Nette Tester. Pochopitelně by šel použít třeba i PHPUnit.

A jako vzorovou aplikaci můžeme vzít třeba Nette Sandbox, protože jej najdete v každé distribuci Nette, nebo si ho můžete stáhnout, včetně frameworku, pomocí Composeru:

composer create-project nette/sandbox myApplication

V něm už máme připravený testovací bootstrap, který vytváří DI kontejner (a vlastně se moc neliší od klasického app/bootstrap.php).

Vyrobíme si tedy instanci presenteru. Buď ručně operátorem new a předáme všechny závislosti, nebo jednodušeji za využití PresenterFactory:

// z DI kontejneru, který vytvořil bootstrap.php, získáme instanci PresenterFactory
$presenterFactory = $container->getByType('Nette\Application\IPresenterFactory');

// a vyrobíme presenter Sign
$presenter = $presenterFactory->createPresenter('Sign');

A bude vhodné vypnout autoCanonicalize, aby presenter nepřesměrovával na kanonické URL:

$presenter->autoCanonicalize = FALSE;

A rovnou můžeme začít testovat, třeba akci Sign:in:

// zobrazení stránky Sign:in metodou GET
$request = new Nette\Application\Request('Sign', 'GET', array('action' => 'in'));
$response = $presenter->run($request);

Presenter je stavěn na jedno voláním run(), pro další requesty vytvoříme vždy nový presenter.

Ověříme, zda odpověď je skutečně šablona:

Assert::true( $response instanceof Nette\Application\Responses\TextResponse );
Assert::true( $response->getSource() instanceof Nette\Templating\ITemplate );

Necháme šablonu vygenerovat HTML kód:

$html = (string) $response->getSource();

A nyní třeba zkontrolujeme, zda se na stránce nacházejí formulářová políčka pro jméno a heslo. Syntax je stejná jako u CSS selektorů.

$dom = Tester\DomQuery::fromHtml($html);

Assert::true( $dom->has('input[name="username"]') );
Assert::true( $dom->has('input[name="password"]') );

Toliko úvodem.


FTP Deployment: smart file upload

FTP deployment is a tool for automated deployment to an FTP server.

There is nothing worse than uploading web applications to FTP server manually, using tools like Total Commander. (Although, editing files directly on the server and then trying to keep some kind of synchronization is even worse ;-)

Once the process is automated, it costs you a fraction of time and minimizes the risk of error (didn't I forget to upload some files?). There are lots of sophisticated deploying techniques available today, but many people are still using FTP. This tool is designed for them.

FTP Deployment is a script written in PHP (requires PHP 5.4 or newer) and will automate the entire process. Just say which local folder to upload and where. This information is stored in a deployment.ini text file, which you can associate with deployment.php script, so deployment will become a one click thing.

php deployment.php deployment.ini

And what does the deployment.ini file contain? Only the remote item is required, all the others are optional:

; log file (defaults to config file with .log extension)
log = ...

; directory for temporary files (defaults to system's temporary directory)
tempdir = /temp/deployment

; enable colored highlights? (defaults to autodetect)
colors = yes

[my site] ; Optional section (there may be more than one section).
; remote FTP server
remote = ftp://user:secretpassword@ftp.example.com/directory
; you can use ftps:// or sftp:// protocols (sftp requires SSH2 extension)

; FTP passive mode
passivemode = yes

; local path (optional)
local = .

; run in test-mode? (can be enabled by option -t or --test)
test = no

; files and directories to ignore
ignore = "
    .git*
    project.pp[jx]
    /deployment.*
    /log
    temp/*
    !temp/.htaccess
"

; is the script allowed to delete remote files? (defaults to yes)
allowdelete = yes

; jobs to run before file upload
before[] = local: lessc assets/combined.less assets/combined.css
before[] = http://example.com/deployment.php?before

; jobs to run after file upload
after[] = remote: unzip api.zip
after[] = http://example.com/deployment.php?after

; directories to purge after file upload
purge[] = temp/cache

; files to preprocess (defaults to *.js *.css)
preprocess = no

; file which contains hashes of all uploaded files (defaults to .htdeployment)
deploymentfile = .deployment

Configuration can also be stored in a PHP file.

In test mode (with -t option) uploading or deleting files is skipped, so you can use it to verify your settings.

Item ignore uses the similar format to .gitignore:

log - ignore all 'log' files or directories in all subfolders
/log - ignore 'log' file or directory in the root
app/log - ignore 'log' file or directory in the 'app' in the root
data/* - ignore everything inside the 'data' folder, but the folder will be created on FTP
!data/db/file.sdb - make an exception for the previous rule and do not ignore file 'file.sdb'
project.pp[jx] - ignore files or folders 'project.ppj' and 'project.ppx'

Before the upload starts and after it finishes, you can execute commands or call your scripts on the server (see before and after), which can, for example, switch the server to a maintenance mode. If you use php-config – you can run lambda function with deployment environment.

Syncing a large number of files attempts to run in (something like) a transaction: all files are uploaded with extension .deploytmp and then quickly renamed.

An .htdeployment file is uploaded to the server, which contains MD5 hashes of all the files and is used for synchronization. So the next time you run deployment.php, only modified files are uploaded and deleted files are deleted on server (if it is not forbidden by the allowdelete directive).

Uploaded files can be processed by a preprocessor. These rules are predefined in the deployment.php file: .css files are compressed using the YUI Compressor and .js minified by Google Closure Compiler. These tools are already included in the distribution, however, they require the presence of Java.

There is also a rule for expanding mod_include Apache directives. For example, you can create a file combined.js:

<!--#include file="jquery.js" -->
<!--#include file="jquery.fancybox.js" -->
<!--#include file="main.js" -->

This tool will combine scripts together and minify them with the Closure Compiler to speed-up your website.

In the deployment.ini, you can create multiple sections, i.e. you may have separate rules for data and for application.