Již brzy!

Slovo dalo slovo a s jedním špičkovým programátorem v Zend Frameworku připravujeme školení frameworků pro PHP programátory. Já si beru na starost školení Nette Framework s dibi (bo sou lepšejší, ni?). Žádné velké teoretizování, žádné obecné řeči – chceme vás naučit sekat kvalitní webové aplikace jako Baťa cvičky!
2 termíny, 2 dny, 2 frameworky
Přesný termín a místo ještě není určené, nicméně školení bude
dvoudenní, pro každý framework samozřejmě zvlášť. Uskuteční se
v Praze. Počet účastníků je omezen, proto nabízím možnost se
předběžně registrovat. Předběžně registrovaní účastníci budou
mít přednost před ostatními. Nyní už je vypsán termín a můžete školení objednávat.
Po účastnících je vyžadována dobrá znalost PHP, naopak znalost
frameworku není potřeba. Rád bych oslovil především firmy, které
pochopily, že takové to nimrání se v PHP nemá smysl a je potřeba tvořit
pořádné weby a to navíc rychle. Na závěr školení bude rituálně smazán
framework, který si firma sama vyvinula
No, a já letím! Sem. Kdybyste měli cestu, stavte se.
PHAR - PHP v kompaktním balení
Je tomu právě tři a půl roku, kdy jsem distribuci Texy doplnil o tzv. kompaktní verzi. Zjednodušeně se dá říci, že jde o skript vzniklý spojením všech souborů tvořících knihovnu do jednoho jediného a vynecháním komentářů a nadbytečných mezer. Výsledkem je funkčně identický kód, jen s krapet hustší konzistencí. (Prostě je hustéééj. Jak Salko!)
Záměrem bylo zjednodušit nahrávání knihovny na server – uploadovat
desítku malých souborů přes FTP bývá značně pomalejší, než nahrát
soubor jeden. Jako vedlejší efekt se ukázalo, že jednosouborová knihovna
znatelně zvyšuje výkon aplikace – klidně o 30 %. Používání
kompaktních verzí se ujalo a od té doby většina bugreportů hlásila chybu
na řádce 1
S příchodem jmenných prostoru v PHP 5.3 se generování kompaktních verzí poněkud zkomplikovalo – zatím totiž platí, že jeden soubor může obsahovat jen jeden jmenný prostor. To znamená, že „jednosouborová“ verze bude mít přinejmenším tolik souborů, kolik je v knihovně jmenných prostorů, plus případné další soubory řešící problémy se vzájemnou závislostí mezi prostory.
Zároveň však PHP 5.3 přichází s novinkou, která si klade za cíl vnést do kompaktních verzí nový svěží vítr: PHAR – PHP archive. Ostatně myslím, že promluvím za nás všechny, když řeknu: nemusí pršet, hlavně když kape!
Jak takový PHAR balíček vytvořit? Nejprve je nutné v PHP.INI zapnout
extension=php_phar.dll a nastavit direktivu phar.readonly =
Off. Následující kód zkonvertuje celý Nette Framework do
souboru nette.phar
$phar = new Phar('nette.phar');
$phar->buildFromDirectory('libs/Nette'); // enter here path to Nette
$phar->setStub("<?php
Phar::mapPhar('nette.phar');
require 'phar://nette.phar/loader.php';
__HALT_COMPILER();");
$phar->compressFiles(Phar::GZ); // or Phar::BZ2
Nejprve se vytvoří nekomprimovaný archiv ze všech souborů v určeném
adresáři a jeho podadresářích – jde o obdobu TAR archivu. Následně se
definuje tzv. stub neboli zaváděcí program. Jeho úkolem bude načíst soubor
loader.php, který je součástí Nette Frameworku a nalézá se
vlastně uvnitř PHAR archivu. Stub musí být ukončen zvoláním
__HALT_COMPILER(). Poté je možno archiv zkomprimovat.
Příklad použití:
require 'nette.phar';
echo Html::el('strong')->setText('It works!');
Vypadá to báječně, že? Ovšem podívejme se na věc podrobněji. PHP
skripty nejsou v archivu nijak minimalizované, nejsou z nich odstraněny
zbytečné komentáře ani mezery. To je potřeba udělat ručně. Takže
zatímco komprimovaný nette.phar má nějakých 180 kB, stejný
archiv vygenerovaný z kompaktní verze Nette (tj. místo adresáře
libs/Nette použijeme libs/Nette.compact) váží
pouhých 50 kB. Nicméně pokud si uvědomíme, že
- minimalizace kódu vede k rychlejšímu parsování (závislost bude dost možná lineární)
- dekomprimace naopak nutně zpomaluje
dojdeme k závěru, že ideální bude generovat PHAR rovnou z kompaktní verze a ukládat jej nekomprimovaný. Nojo – ale proč teda rovnou nezůstat u kompaktní verze? Bez PHAR režie navíc?
Důvod existuje. Kompaktní verze se vždy načte celá, zatímco PHAR lze parsovat a kompilovat po částech. Takže Zend Framework bude vhodnější zabalit do PHARu, u dibi nebo Texy si můžeme dovolit plný výkon kompaktní verze.
Singleton Sofie S.
Slíbil jsem přijít se silným lidským příběhem. Příběhem programátorky, která stojí před těžkou životní volbou. Její jméno je Sofie.
Jak už jste pochopili z druhé věty, Sofie nebyla právě pohledná žena. Zpoza prořídlých neupravených kadeří na vás přes velké umaštěné brýle hleděly dioptriemi zvětšená drobná očka, kostěné obroučky marně skrývaly uhří farmu táhnoucí se podél líček až ke knírku. V redakci, kde pracovala, platila za velmi zdatnou programátorku. Na svůj redakční systém byla náležitě pyšná, snad ještě víc, než na titul Miss Second Life 2007. Pýcha byla oprávněná, kód systému byl tak křišťálově čistý, že ani křišťál křišťálově čistější být nemůže.
Ačkoliv měla Sofie z návrhových vzorů velmi ráda právě singleton, jednalo se spíš o spřízněnost duševní – v kódu jej používala umírněně, jen tam, kde se skutečně hodil. Například pro zapouzdření HTTP požadavku:
/**
* @copyright Sofie S., 2006
*/
class HttpRequest
{
/********************* singleton *******************/
private static $instance;
private function __construct()
{}
public static function getInstance()
{
if (self::$instance === NULL) {
self::$instance = new self;
}
return self::$instance;
}
private function __clone()
{}
private function __wakeup()
{}
/********************* HTTP request encapsulation *******************/
public function getRemoteAddress()
{
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : NULL;
}
...
}
// užití
if (HttpRequest::getInstance()->isPost()) {
echo 'Hey mister POSTman!';
}
To pro představu snad stačí. Víc kódu ani ukázat nemůžu, protože nemám Sofiin souhlas a mazat kopyrajty si mi nechce. Ale určitě se shodneme na tom, že zapouzdření HTTP požadavku do formy singletonu je perfektní volba:
- HTTP požadavek může být jen jeden (a právě jeden)
- je potřeba jeho globální dostupnost
- hezky líně se inicializuje až při prvním použití
Klíčový je samozřejmě první bod. Mohlo by se stát, že by v budoucnu bylo potřeba mít dva HTTP požadavky? To bezpochyby ne, odpověděla si Sofie už v době návrhu systému a volba singletonu byla jasná věc.
Nemá smysl zastírat, že se Sofii líbil jeden redaktor z vedlejší kanceláře. Věděli to všichni. Ona z něj byla doslova hotová! Stačilo, aby prošel kolem jejího počítače a rvala si rodidla. Proto když jednoho pondělního rána našla na stole vzkaz, že s ní chce o něčem mluvit, málem štěstím omdlela.
Řeč se týkala redakčního systému. Eman nešetřil chválou a dokonce prohlásil, že Sofie je jejich poklad a co by si bez ní počal. Pak se jí, snad omylem, letmo dotkl ramene. Sofie pocítila v podbřišku slastné zachvění. „Je tu něco, co jsem ti chtěl už dávno říct,“ pohlédl jí Eman do očí, „když mám dlouho otevřený článek a pak kliknu na sejf, tak se mi objeví: byl jste automaticky odhlášen, bla bla bla, a celý článek je v řiti. Nemůžeš s tím něco udělat?“
Nebylo na světě nic, co by Sofie pro Emana neudělala. Ihned se pustila do práce.
V podstatě šlo o banální úkol. Dokonce tuhle slyšela, že stejný problém dokázali po dvou letech vyřešit borci z Jyxo na svém serveru blog.cz, to by bylo, aby si s tím ona během půl hodinky neporadila! Princip je jednoduchý: v okamžiku, kdy se detekuje vypršení časového limitu a uživatel se automaticky odhlásí, tak se objekt s aktuálním HTTP požadavkem uloží do session. A pak stačí jen upravit přihlašovací rutinu, aby po úspěšném přihlášení, zjistí-li v session uložený tento objekt, aby jej nastavila jako aktuální. Je to otázka pár řádek kódu a za dvacet minut už bude sedět u Emana a předvádět mu, jak to báječně funguje. Dají si společně šálek kávy, plánovala si. A možná … možná ji bude chtít pozvat na skleničku, ačkoliv ona sklo nerada. Ale Emanovi neumí říci ne.
Na kávu ten den nedošlo. Zrada přišla z míst, odkud je Sofie nečekala. Zradil ji singleton. Sofie musela povolit serializaci objektu HttpRequest, musela vyřešit prohození těchto objektů v metodě getInstance, ale především se musela smířit s faktem, že na ten krátký okamžik, kdy se uložený požadavek obnovil ze session, tak na tu chvíli existovaly požadavky dva. I singleton tvořil pár. Jen ona zůstala sama.
Je singleton zlo?
Singleton je jedním z nejpopulárnějších návrhových vzorů. Jeho úkolem je zajistit existenci pouze jediné instance určité třídy a zároveň poskytnout globální přístup k ní. Pro úplnost malý příklad:
class Database
{
private static $instance;
private function __construct()
{}
public static function getInstance()
{
if (self::$instance === NULL) {
self::$instance = new self;
}
return self::$instance;
}
...
}
// singleton je globálně dostupný
$result = Database::getInstance()->query('...');
Typickými rysy jsou:
- privátní konstruktor, znemožňující vytvoření instance mimo třídu
- statická vlastnost $instance, kde je unikátní instance uložená
- statická metoda getInstance(), která zpřístupní instanci a při prvním volání ji vytvoří (lazy loading)
Jednoduchý a snadno pochopitelný kód, který řeší dva problémy objektového programování. Přesto v dibi nebo Nette Framework žádné singletony nenajdete. Proč?
Unikátnost jen zdánlivá
Podívejme se na kód pozorněji – skutečně ručí za existenci pouze jedné instance? Obávám se, že nikoliv:
$dolly = clone Database::getInstance();
// nebo
$dolly = unserialize(serialize(Database::getInstance()));
// nebo
class Dolly extends Database {}
$dolly = Dolly::getInstance();
Proti tomu existuje obrana:
final public static function getInstance()
{
// finální getInstance
}
final public function __clone()
{
throw new Exception('Clone is not allowed');
}
final public function __wakeup()
{
throw new Exception('Unserialization is not allowed');
}
Jednoduchost implementace singletonu je ta tam. Ba co hůř – s každým dalším singletonem se nám bude opakovat kusanec stejného kódu. Také třída najednou plní dva zcela odlišné úkoly: kromě svého původního úkolu se stará o to být dost single. Obojí je varovný signál, že něco není v pořádku a kód by si zasloužil refaktoring. Vydržte, za chvíli se k tomu vrátím.
Globální = ošklivé?
Singletony poskytují globální přístupový bod k objektům. Není nutné si odkaz na ně neustále předávat. Zlí jazykové však tvrdí, že taková technika se nijak neliší od používání globálních proměnných a ty jsou přeci čiré zlo.
(Pokud metoda pracuje s objektem, který jí byl nějak explicitně předán, parametrem nebo jde o proměnnou objektu, říkám tomu „drátové spojení“. Pokud pracuje s objektem, který si získá přes globální bod (např. skrz singleton), říkám tomu „bezdrátové spojení“. Docela fajn analogie, ne?)
Zlí jazykové se v jedné věci mýlí – na „globálním“ není a
priori nic špatného. Stačí si uvědomit, že název každé třídy a metody
není nic jiného, než globální identifikátor. Mezi bezproblémovou
konstrukcí $obj = new MyClass a kritizovanou $obj =
MyClass::getInstance() není ve skutečnosti zásadní rozdíl. Tím
méně u dynamických jazyků jako je PHP, kde lze psát $obj = $class::getInstance().
Co ale může způsobit bolení hlavy, to jsou
- skryté závislosti na globálních proměnných
- nečekané používání „bezdrátových spojení“, které z API tříd nevyčtete (viz Singletons are Pathological Liars)
První bod se dá eliminovat, pokud se nebudou singletony chovat jako
globální proměnné, ale spíš jako globální funkce či služby. Jak tomu
rozumět? Vezměte si třeba google.com – hezký příklad singletonu
jakožto globální služby. Existuje jedna instance (fyzická serverová farma
kdesi v USA) globálně dostupná přes identifikátor
www.google.com. (Dokonce ani clone www.google.com
nefunguje, jak zjistil Microsoft, maj to holt kluci vychytaný.) Důležité je,
že tato služba nemá skryté závislosti typické pro globální
proměnné – vrací odpovědi bez neočekávatelné souvislosti s tím, co
před chvíli hledal někdo úplně jiný. Naopak nenápadná funkce strtok trpí vážnou závislostí na
globální proměnné a její používání může vést k velmi těžko
odhalitelným chybám. Jinými slovy – problém není v „globálnosti“,
ale v návrhu.
Také druhý bod je čistě záležitostí návrhu kódu. Není chybou použít „bezdrátové spojení“ a přistoupit ke globální službě, chybou je to dělat proti očekávání čili skrytě. Programátor by měl přesně vědět, jaký objekt využívá které třídy. Poměrně čistým řešením je mít v objektu proměnnou odkazující na objekt služby, která se inicializuje na globální službu, pokud programátor nerozhodne jinak (technika convention over configuration).
Unikátnost může být na škodu
K singletonům se váže problém, na který narazíme nejpozději při
testování kódu. A tím je potřeba podstrčit jiný, testovací objekt.
Vraťme se ke Google jakožto ukázkovému singletonu. Chceme otestovat
aplikaci, která jej využívá, jenže po pár stech testech začne Google
protestovat We're
sorry… a jsme kde? A jsme někde. Řešením je pod identifikátor
www.google.com podstrčit fiktivní (mock) službu. Potřebujeme
upravit soubor hosts – jenže (zpět z analogie do světa OOP)
jak toho dosáhnout u singletonů?
Jednou z možností je implementovat statickou metodu
setInstance($mockObj). Ale ouha! Copak asi chcete slečně metodě
předat, když žádná jiná instance, než ona jedna jediná,
neexistuje?
Jakýkoliv pokus odpovědět na tuto otázku povede nevyhnutelně k rozpadu všeho, co singleton dělá singletonem.
Pokud odstraníme restrikce na existenci jen jedné instance, přestane být
singleton single a my řešíme pouze potřebu globálního úložiště. Pak je
nabíledni otázka, proč v kódu stále opakovat stejnou metodu
getInstance() a nepřesunout ji do extra třídy, do nějakého
globálního registru?
Anebo ponecháme restrikce, pouze identifikátor v podobě třídy
nahradíme za interface (Database → IDatabase),
čímž se vynoří problém nemožnosti implementovat
IDatabase::getInstance() a řešením je opět globální
registr.
O pár odstavců výše jsem sliboval vrátit se k otázce opakujícího se kódu ve všech singletonech a možnému refaktoringu. Jak vidíte, problém se mezitím vyřešil sám. Singleton zemřel.
Řetězení výjimek v PHP
Dobře utajenou novinkou PHP 5.3 je řetězení výjimek (exception chaining). Tuto vlastnost můžete znát třeba z Javy (caused exception) nebo ASP.NET (inner exception). O co jde? Každá vyhozená výjimka se může odkazovat na jinou výjimku, která její vznik zapříčinila. Jinými slovy, je možné zachycenou výjimku zabalit do obecnější výjimky.
function loadFile($file)
{
if (!is_file($file)) {
throw new FileNotFoundException("File '$file' not found.");
}
...
}
function getConfig()
{
try {
$s = loadFile('config.ini');
...
} catch (FileNotFoundException $e) {
// obalíme výjimku
throw new ConfigException("Missing configuration.", 0, $e);
}
}
try {
$config = getConfig();
} catch (ConfigException $e) {
echo $e;
}
Jak vidíte, konstruktor třídy Exception se dočkal rozšíření o třetí nepovinný parametr, kterým lze zřetězenou výjimku nastavit. Získat ji můžeme metodou getPrevious(). Zřetězená výjimka se zahrne i do textového výpisu:
exception 'FileNotFoundException' with message 'File 'config.ini' not found.' in demo.php:8
Stack trace:
#0 demo.php(17): loadFile('config.ini')
#1 demo.php(28): getConfig()
#2 {main}
Next exception 'ConfigException' with message 'Missing configuration.' in demo.php:21
Stack trace:
#0 demo.php(28): getConfig()
#1 {main}
Všimněte si, že výjimky se poněkud nešťastně vypisují v pořadí, v jakém vznikly. Očekával bych pořadí opačné.
