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í.)
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:
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ř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\Configurator
→
Nette\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()
a 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\Config
→ Nette\DI
a
Nette\Utils\PhpGenerator
→ Nette\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()
a 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.
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::$blueScreen
→
Debugger::getBlueScreen()
- a adekvátně
$bar
→ getBar()
,
$logger
→ getLogger()
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\PhpGenerator
→
Nette\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 🙂
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í
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.
Aktualizováno pro Nette 2.3.
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::type('Nette\Application\Responses\TextResponse', $response);
Assert::type('Nette\Bridges\ApplicationLatte\Template', $response->getSource());
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.
Není nic horšího, než uploadovat soubory na FTP ručně,
například pomocí Total Commanderu. (Ačkoliv, ještě horší je editovat
soubory přímo na serveru a pak se zoufale pokoušet o jakousi synchronizaci.)
Jakmile totiž proces nezautomatizujete, stojí vás mnohem víc času a hrozí
riziko chyby. Třeba, že některý soubor zapomenete nahrát.
Dnes už se používají sofistikované techniky nasazování aplikací na
web, například pomocí Gitu, ale mnoho lidí stále zůstává u nahrávání
jednotlivých souborů skrze FTP. Právě pro ně je určen nástroj FTP
Deployment, který zautomatizuje a zjednoduší nahrávání aplikací
přes FTP.
FTP Deployment je skript
napsaný v PHP, který celý proces zautomatizuje. Stačí jen říct, který
adresář (local
) má kam nahrát (remote
). Tyto
údaje zapíšete do souboru deployment.ini
, jehož odkliknutí
můžete rovnou asociovat se spuštěním skriptu, takže deployment se stane
věcí jednoho kliknutí:
php deployment deployment.ini
A jak vypadá soubor deployment.ini
? Povinná je vlastně jen
položka remote
, všechny ostatní jsou nepovinné:
; remote FTP server
remote = ftp://user:secretpassword@ftp.example.com/directory
; you can use ftps:// or sftp:// protocols (sftp requires SSH2 extension)
; do not like to specify user & password in 'remote'? Use these options:
;user = ...
;password = ...
; FTP passive mode
passiveMode = yes
; local path (optional)
local = .
; run in test-mode? (can be enabled by option -t or --test too)
test = no
; files and directories to ignore
ignore = "
.git*
project.pp[jx]
/deployment.*
/log
temp/*
!temp/.htaccess
"
; is allowed to delete remote files? (defaults to yes)
allowDelete = yes
; jobs to run before uploading
before[] = local: lessc assets/combined.less assets/combined.css
before[] = http://example.com/deployment.php?before
; jobs to run after uploading and before uploaded files are renamed
afterUpload[] = http://example.com/deployment.php?afterUpload
; directories to purge after uploading
purge[] = temp/cache
; jobs to run after everything (upload, rename, delete, purge) is done
after[] = remote: unzip api.zip
after[] = remote: chmod 0777 temp/cache ; change permissions
after[] = http://example.com/deployment.php?after
; files to preprocess (defaults to *.js *.css)
preprocess = no
; file which contains hashes of all uploaded files (defaults to .htdeployment)
deploymentFile = .deployment
; default permissions for new files
;filePermissions = 0644
; default permissions for new directories
;dirPermissions = 0755
V testovacím režimu (při spuštění s parametrem -t
)
k uploadu nebo mazání souborů na FTP nedochází, můžete jej tedy použít
k ověření, zda máte všechny hodnoty dobře nastavené.
Položka ignore
používá stejný formát jako .gitignore:
log
– ignoruje všechny soubory či adresáře
log
, i uvnitř všech podsložek
/log
– ignoruje soubor či adresář log
v kořenovém adresáři
app/log
– ignoruje soubor či adresář log
v podsložce app
kořenového adresáře
data/*
– ignoruje vše uvnitř složky data
,
ale samotnou služku na FTP vytvoří
!data/session
– z předchozího pravidla učiní výjimku
pro soubor či složku session
project.pp[jx]
– ignoruje soubory či složky
project.ppj
a project.ppx
Před započetím uploadu a po jeho skončení můžete nechat zavolat
skripty na vašem serveru (viz before
a after
), které
mohou například server přepnout do maintenance režimu, kdy bude
odesílat hlavičku 503.
Aby synchronizace i velkého množství souborů proběhla (v rámci
možností) transakčně, všechny soubory se nejprve nahrají s příponou
.deploytmp
a poté, což už je rychlé, přejmenují. Zároveň se
na server uloží soubor .htdeployment
, kde jsou uloženy md5
otisky všech souborů a právě pomocí něj se nadále web synchronizuje.
Při dalším spuštění tedy nahrává pouze změněné soubory a maže
smazané (pokud to nezakážeme direktivou allowdelete
).
Nahrávané soubory je možné nechat zpracovat preprocesorem. Standardně
jsou nastaveny pravidla, že všechny .css
soubory se zkomprimují
pomocí Clean-CSS a .js
pomocí Google Closure Compiler. Před
samotnou komprimací se ještě expandují základní mod_include
direktivy Apache. Můžete tedy vytvořit například soubor
combined.js
:
<!--#include file="jquery.js" -->
<!--#include file="jquery.fancybox.js" -->
<!--#include file="main.js" -->
Který vám bude Apache na lokálním serveru za běhu sestavovat spojením
tří uvedených souborů. Říci si o to můžete takto:
<FilesMatch "combined\.(js|css)$">
Options +Includes
SetOutputFilter INCLUDES
</FilesMatch>
Přičemž na server se nahraje už ve spojené a zkomprimované podobě.
Vaše HTML stránka tak bude šetřit zdroje a načítat jediný JavaScriptový
soubor.
V konfiguračním souboru deployment.ini
můžete vytvořit
i více sekcí, případně si udělat jeden konfigurák zvlášť pro data a
jeden pro aplikaci, aby synchronizace byla co nejrychlejší a nemusel se vždy
počítat otisk velkého množství souborů.
Nástroj FTP Deployment jsem si vytvořil před mnoha lety a plně pokrývá
mé požadavky na deployovací nástroj. Zároveň je třeba zdůraznit, že FTP
protokol tím, že přenáší heslo v čitelné podobě, představuje
bezpečnostní riziko a rozhodně byste jej neměli používat třeba na
veřejných Wi-Fi.
Málokdo má takovou potřebu zdůrazňovat svou domnělou
nadřazenost, jako právě Railisti. Abyste mě nechápali špatně, jde
o dobrou marketingovou strategii. Nepříjemné je, když jí podlehnete do té
míry, že zbytek světa vnímáte jen jako upachtěné kopírovače bez šance
se vám někdy přiblížit. Svět takový totiž není.
Příkladem je Dependency Injection. Zatímco lidé kolem PHP nebo
JavaScriptu objevili DI se zpožděním, Ruby on Rails zůstávají dosud
nepolíbené. Bylo mi záhadou, proč framework s tak pokrokovou image
zůstává kdesi pozadu a začal v tom pátrat. Odpověď mi dala řada zdrojů
na Google nebo karmiq
a zní:
Ruby je tak dobrý jazyk, že vůbec Dependency Injection nepotřebuje.
Fascinující argument, který je navíc v elitářském prostředí
sebepotvrzující. A je skutečně pravdivý? Nebo jde jen o zaslepení
pýchou, stejné zaslepení, jaké způsobilo nedávno přetřásané
bezpečnostní díry v Rails?
Říkal jsem si, že je možné, že Ruby znám natolik málo, aby mi
nějaký klíčový aspekt unikl, a že skutečně jde o jazyk, který DI
nepotřebuje. Jenže primárním smyslem Dependency
Injection je zřejmé předávání závislostí, aby byl kód
srozumitelný a předvídatelný (a je pak i lépe testovatelný). Jenže
když se podívám do dokumentace Rails na tutoriál „blog za pár minut“,
vidím
tam třeba:
def index
@posts = Post.all
end
Tedy pro získání blogpostů používají statickou metodu
Post.all
, která odněkud (!) vrátí seznam článků.
Z databáze? Ze souboru? Vyčaruje je? Nevím, protože se tu nepoužívá DI.
Místo toho se tu vaří nějaké statické peklo. Ruby je bezesporu
šikovný jazyk, ale DI nenahrazuje.
V Ruby lze za běhu přepisovat metody (Monkey patch; obdobně jako
v JavaScriptu), což je forma Inversion of control (IoC), která třeba pro
potřeby testů dovolí podstrčit jinou implementaci statické metody
Post.all
. Tohle ale nenahrazuje DI, kód to zřejmější
neudělá, spíše naopak.
Mimochodem, zaujala mě i třída Post tím, že reprezentuje jak jeden
článek na blogu, tak funguje jako repozitář (metoda all), což je porušení
Single
Responsibility Principle jako vyšité.
Jako odůvodnění, proč Ruby nepotřebují DI, se často odkazuje na
článek LEGOs,
Play-Doh, and Programming. Důkladně jsem ho pročetl, sledoval, jak autor
párkrát zaměňuje „DI“ s „DI frameworkem“ (tedy něco jako
zaměňovat „Ruby“ s „Ruby on Rails“) a nakonec zjistil, že
k závěru, že Ruby Dependency Injection nepotřebují, vůbec nepřišel.
Psal, že nepotřebují DI frameworky, jaké zná z Javy.
Jeden mylně interpretovaný závěr, pokud pohladí ego, dokáže zcela
pobláznit obrovskou skupinu inteligentních lidí. Nakonec mýtus, že špenát
obsahuje neobyčejné množství železa, se taky drží od roku 1870.
Ruby je velmi zajímavý jazyk, vyplatí se v něm jako v každém jiném
používat DI a existují pro něj i DI frameworky. Rails je zajímavý
framework, který zatím neobjevil DI. Až ho objeví, půjde o velké téma
některé z příštích verzí.
(Po pokusu diskutovat o DI s Karmim, kterého pokládám za
nejinteligentnějšího Railistu, nechávám komentáře uzavřené,
omlouvám se.)
Viz také: Dependency
injection není pouze o jednoduším testování
Jednou z novinek PHP 5.4 je typehint callable
.
Byla by to parádní věc, kdyby to tvůrci tak děsně nezprasili.
PHP typ callbable je
pseudotypem, jehož hodnotou může být buď název metody či funkce (tj.
řetězec) nebo dvojice třída/objekt a její metoda, tedy pole. V PHP se
používá od pradávna, nicméně od verze 5.4 pro něj existuje typehint:
class Template
{
function registerHelper($name, callable $helper)
{
...
}
}
$template = new Template;
$template->registerHelper('date', 'Helpers::date');
Skvělé. Tedy až do chvíle, než to vyzkoušíte a PHP vás zdupne:
error: Argument 2 passed to Template::registerHelper() must be callable, string given
Aha, tak je to asi potřeba zapsat jako pole:
$template->registerHelper('date', ['Helpers', 'date']);
Výsledek je opět:
error: Argument 2 passed to Template::registerHelper() must be callable, array given
Ve skutečnosti PHP vadí to, že třída Helpers
neexistuje.
Totiž už během volání registerHelper
její přítomnost
vyžaduje. Typehint callable, narozdíl od všech jiných typehintů,
neověřuje jen formální platnost předaného argumentu, ale ujišťuje se,
že uvedená třída skutečně existuje a má zmíněnou metodu. Pokud třída
neexistuje, pokusí se ji načíst autoloadingem.
Což jednak zabíjí lazyloading – už samotné předání
parametru s callbackem načte příslušnou třídu, která by se třeba jinak
vůbec načítat nemusela.
A za druhé to generuje naprosto idiotské chybové hlášky. Což je
věc, na kterou jsem dosti citlivý. Ze zprávy
must be callable, string given
těžko někdo pochopí, že typ
string je použitý správně, jen neexistuje třída či metoda. Já bych to
chápal tak, že funkce neakceptuje řetězce. Přitom třeba funkce
call_user_func
ve stejné situaci generuje zcela smysluplné
chyby jako:
Warning: call_user_func() expects parameter 1 to be a valid callback, class 'Helpers' not found
Warning: call_user_func() expects parameter 1 to be a valid callback, class 'Helpers' does not have a method 'date'
Na chybu jsem upozornil,
bohužel Rasmus je se současným chováním nadmíru spokojen. Setrvávat na
zavádějících chybových hláškách je projevem arogance a hlouposti:
namísto opravy raději uvedou ve zmatek statisíce programátorů a připraví
je o spoustu času. Nette Framework rozhodně zprasený typ callable používat
nebude.
PHP přistupuje ke třídám způsobem známým ze staticky
typovaných jazyků a neumožňuje monkey patching, tedy
měnit za běhu metody tříd, kopírovat je mezi instancemi a podobně.
Abyste porozuměli, co mám na mysli, vytvořme třídu Greeting
s metodou say()
:
class Greeting
{
function __construct($name)
{
$this->name = $name;
}
function say($message)
{
echo "$message $this->name.";
}
}
$g = new Greeting('John');
$g->say('Hello'); // Hello John.
V PHP neočekáváme, že by bylo možné metodu třeba uložit do
proměnné či jiného atributu a poté zase zavolat:
$method = $g->say;
$g->greet = $method;
$g->greet('Hello');
Nebo dokonce za chodu přidávat metody nové:
$g->shout = function($message) {
echo "$message $this->name!!!";
};
$g->shout('Hello'); // Hello John!!!
Na jedné straně je mi líto, že tohle PHP neumí, na straně druhé vidím
ve statickém pojetí tříd podstatné výhody. A na straně třetí: v PHP
lze tohle chování snadno emulovat.
Emulace dynamiky
Vytvořit funkci jako je výše uvedená shout()
a vložit ji do
proměnné objektu PHP už umí od verze 5.3. Ale abychom ji mohli zavolat
běžným zápisem, musíme si vypomoci magickou metodou
__call()
:
class Greeting
{
function __call($name, $args)
{
if (!isset($this->$name) || !$this->$name instanceof Closure) {
throw new Exception("Method $name not found.");
}
return call_user_func_array($this->$name->bindTo($this, $this), $args);
}
...
}
A nyní už bude příklad s metodou shout()
fungovat.
Abychom mohli stejně nakládat i se statickými metodami, jako byla třeba
výše uvedená say()
, doplníme ještě __get()
:
class Greeting
{
function __get($name)
{
if (!method_exists($this, $name)) {
throw new Exception("Property $name not found.");
}
return function() use ($name) {
return call_user_func_array(array($this, $name), func_get_args());
};
}
...
}
A nyní bude fungovat i první příklad s přiřazením
$method = $g->say
a následným voláním.
Pro verzi 5.3
Uvedené příklady vyžadují PHP 5.4. Ve verzi 5.3 jsou closures
ořezané a nesmí se v nich používat $this
. Řešení by
vypadalo trošičku jinak:
// for PHP 5.3
class Greeting
{
function __call($name, $args)
{
if (!isset($this->$name) || !$this->$name instanceof Closure) {
throw new Exception("Method $name not found.");
}
array_unshift($args, $this);
return call_user_func_array($this->$name, $args);
}
function __get($name)
{
if (!method_exists($this, $name)) {
throw new Exception("Property $name not found.");
}
return function() use ($name) {
$args = func_get_args();
return call_user_func_array(array(array_shift($args), $name), $args);
};
}
...
}
A namísto $this
bychom uvnitř closure použili první
argument, pojmenovaný třeba $self
.
$g->shout = function($self, $message) {
echo "$message $self->name!!!";
};
Nicméně narozdíl od 5.4 varianty má nyní funkce přístup jen
k veřejným proměnným třídy.
Nejprve otázka: mačne nebo nemačne?
$str = "123\n";
echo preg_match('~^\d+$~', $str);
Kdo z vás si myslí, že funkce vrátí false
, protože
regulární výraz běží v jednořádkovém režimu a nepovoluje v řetězci
žádné jiné znaky krom číslic, ten se mýlil.
Malinko odbočím. Regulární výrazy v jazyce Ruby mají jednu nectnost
(nesoulad s de facto standardem PERLu): znaky ^
a $
neoznačují začátek a konec řetězce, ale jen jednoho řádku v něm.
Neznalost tohoto faktu může způsobit bezpečnostní zranitelnost, jak třeba
upozorňuje dokumentace
Rails. PHP se chová standardně, ale málokdo už ví, co přesně ono
standardní chování znamená. Dokumentace meta-znaku $
je totiž
nepřesná.
(už opraveno)
Správně má být, že znak $
znamená konec řetězce nebo
ukončující odřádkování; ve víceřádkovém režimu (modifikátor
m
) znamená konec řádku.
Skutečný konec řetězce chytá sekvence \z
.
Nebo je možné použít dolar společně s modifikátorem
D
.
$str = "123\n";
echo preg_match('~^[0-9]+$~', $str); // true
echo preg_match('~^[0-9]+$~D', $str); // false
echo preg_match('~^[0-9]+\z~', $str); // false