phpFashion

Otevřený dopis komunitě: přestávám mít radost z programování

Z něčeho, co má člověka bavit, se stala otročina.

(Hele, ono by to tak mohlo vypadat, ale tohle není naříkání, jen konstatování léta známých faktů:)

Nette Framework je i po deseti letech projekt, který vymýšlím sám, programuji sám, píšu mu sám dokumentaci, píšu všechny články, co kdy o něm vyšly, sám spravuji a platím web. (Přeháním, vývoj DB vrstvy převzal Honza Škrášek, pár lidí se podílelo na překladu dokumentace a tvorbě quick startu a hodně jich dělá podporu na fóru.)

Zpětná vazba je především žádná (ze žádné z firem používajících Nette nevím o ničem, co vylepšit) nebo negativní (občas z těch firem prosákne, že je všechno špatně.)

Investuji do toho neuvěřitelné množství času. A pochopitelně to negeneruje žádný příjem, jde o open source. Nestěžuji si, je skvělé mít takový koníček.

Vím, že Nette je projekt kvalitativně srovnatelný se světovou špičkou.

Dnes jsem vydal Nette Framework 2.0.14, který uzavírá starou větev, a úplně novou verzi 2.1.0, se kterou jsem velmi spokojen. A mám spoustu nápadů na nové věci.

Jenže jsem z toho začal mít regulérní deprese. Analyzuji důvody. Asi neustálá nutnost čelit negativitě, nutnost brát ohledy na uživatele, kteří pro framework nehnou ani prstem, to všechno mě vnitřně ničilo a dostalo do velmi divného rozpoložení, ze kterého bych rád unikl. No a jelikož jsem dnes vydal ty nové verze, které uzavírají dlouho etapu, napadlo mě vydat se taky dál 🙂

před 12 lety v rubrice Nette


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 🙂


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. 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.


FTP Deployment: nahrávejte přes FTP chytře

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.ppjproject.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.


Objeví Rails Dependency Injection?

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í

před 13 lety v rubrice Random


PHP 5.4 má nepoužitelný typ callable

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.


Monkey patching v PHP

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.


Víte, co znamená $ v regulárním výrazu?

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

DI a property injection

Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Co kdyby se závislosti předávaly přímo do proměnných? Proberu úskalí a výhody property injection.

Property injection má jednu podstatnou výhodu: stručnost. Srovnejte:

class Foobar
{
	/** @var HttpRequest */
	private $httpRequest;

	/** @var Router */
	private $router;

	function __construct(HttpRequest $httpRequest, Router $router)
	{
		$this->httpRequest = $httpRequest;
		$this->router = $router;
	}

}

versus

class Foobar
{
	/** @var HttpRequest @inject */
	public $httpRequest;

	/** @var Router @inject */
	public $router;

}

Proměnné musíme definovat tak jako tak. Zpravidla u nich uvádíme i anotaci @var a příslušný datový typ. Je lákavé si ušetřit práci a místo psaní rutinního kódu konstruktoru nebo metody inject() doplnit prosté @inject. Property injection kromě minimálního režijního kódu navíc parádně řeší problém s předáváním závislostí a dědičností.

Použití anotace představuje jinou konvenci pro předání závislostí. Zdůrazňuji slovo jinou, protože ať už vyjmenujeme závislosti jakožto argumenty metody nebo anotováním, jde o ekvivalentní činnost. Čímž oponuji názoru, že použití anotace představuje závislost na kontejneru. To v žádném případě není pravda, jde jen o konvenci, koneckonců dosud o kontejnerech nepadla řeč a ukázky dávají smysl.

Stejně tak se nedívejte na anotaci @inject jako nějakou odpornou magii, kterou musíte nastudovat, abyste ji mohli používat. Žádná magie tu není. Jde o obyčejné veřejné proměnné a anotace je jen doplňující informace pro programátora, říkající, že objekt vyžaduje tyto proměnné naplnit. (Nutno dodat, že Jakub Vrána reagoval na použití anotací u private proměnných, což magie je.)

V článku o předávání závislostí jsem se používání proměnných širokým obloukem vyhnul, protože mají vážné nedostatky:

  • public proměnné nezajistí typovou kontrolu
  • public proměnné nezajistí neměnnost
  • private proměnné nelze naplnit žádnou jazykovou konstrukcí
  • private proměnné nejsou součástí veřejného API – nejde tedy o deklaraci závislosti!
  • pro protected proměnné platí nevýhody obou

Ještě bych přidal, že anotace nejsou nativní součástí jazyka PHP a jde tedy o nestandardní konvenci, oproti třeba injektáži přes konstruktor.

Poznámka: vstřikování závislostí do privátních proměnných posvětila třeba Java EE 6 a je to skutečně ee. Třída své závislosti tají (private = neveřejný) a nelze ji instancovat jinak, než kontejnerem (závislost na kontejneru). Jde zcela proti smyslu Dependency Injection, jak je popsán v perexu tohoto článku, a také proti základnímu principu OOP, zapouzdření. Označil bych to jako „Inversion of Dependency Injection.“

Pro properly property injection bychom potřebovali once-write-only veřejnou proměnnou s typovou kontrolou. Kdyby tohle PHP umělo, nic by nebránilo je používat. Jenže PHP to neumí.

Emulace inject property

PHP to neumí, ale lze to emulovat!

Emulaci zajistíme pomocí magických metod __set a __get. Jak ale dosáhnout toho, aby se k public proměnné přistupovalo skrze tyto metody? Použijeme trik: v konstruktoru ji unsetneme. Proměnná zmizí a při přístupu k ní se již použijí magické metody.

Příklad implementace ve formě základní třídy Object by mohl vypadat třeba takto:

class Object
{
	private $injects = array();

	function __construct()
	{
		// následující analýza proměnných by se mohla kešovat
		$rc = new ReflectionClass($this);
		foreach ($rc->getProperties() as $prop) {
			if ($prop->isPublic() && strpos($prop->getDocComment(), '@inject')
				&& preg_match('#@var\s+(\S+)#', $prop->getDocComment(), $m)
			) {
				// unset property to pass control to __set() and __get()
				unset($this->{$prop->getName()});
				$this->injects[$prop->getName()] = array('value' => null, 'type' => $m[1]);
			}
		}
	}


	function __set($name, $value)
	{
		if (!isset($this->injects[$name])) {
			throw new Exception("Cannot write to an undeclared property $$name.");

		} elseif ($this->injects[$name]['value']) {
			throw new Exception("Property $$name has already been set.");

		} elseif (!$value instanceof $this->injects[$name]['type']) {
			throw new Exception("Property $$name must be an instance of {$this->injects[$name]['type']}.");

		} else {
			$this->injects[$name]['value'] = $value;
		}
	}


	function __get($name)
	{
		if (!isset($this->injects[$name])) {
			throw new Exception("Cannot read an undeclared property $$name.");
		}
		return $this->injects[$name]['value'];
	}

}

pak stačí deklarovat výše uvedenou třídu Foobar jako potomka Object a vše bude fungovat standardně podle očekávání:

class Foobar extends Object
{
	/** @var HttpRequest @inject */
	public $httpRequest;

	/** @var Router @inject */
	public $router;

}

$fb = new Foobar;
$fb->router = new Router;

Navíc však máme zajištěnou neměnnost a typovou kontrolu:

$fb->router = new Router;
// Exception: Property $router has already been set.

$fb->httpRequest = new Router;
// Exception: Property $httpRequest must be an instance of HttpRequest.");

Čistá cesta nebo prasárna?

Zkusme se zamyslet nad tím, co vlastně anotace @inject představuje: hint pro programátora, že proměnnou má při vytváření objektu nastavit a že ji později nesmí měnit. Anotace @var pak nařizuje typ.

Je na programátorovi, aby dodržel kontrakt. Stejně jako v případě anotace @private v PHP 4 nebo JavaScriptu, či anotace @return v současném PHP. Jde o pravidla, u nichž se předpokládá, že je programátor dodrží, aniž to lze na úrovni interpreteru ověřit.

Třída Object rozšiřuje PHP o schopnost kontroly za běhu, usnadní tedy identifikaci chyb. Je to vychytávka navíc. Z mého pohledu tedy akceptovatelná cesta k použití property injection v PHP. Možná by se dalo uvažovat nad zařazením do Nette\Object a legitimizace této injektáže v Nette.



phpFashion © 2004, 2025 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.