Yield a další novinky PHP 5.5

Školení, která pořádám

Největší novinkou právě vydané verze PHP je operátor yield, který dovoluje pozastavit provádění funkce a od stejného místa pokračovat později. Kromě toho přibylo několik příjemných syntaktických novinek a řada nových funkcí. Některé zátěže minulosti byly také označeny jako zastaralé nebo byly přímo odstraněny.

Článek vyšel na serveru Zdroják.

Operátor yield

Operátor yield dovoluje vytvářet generátory – funkce generující hodnoty, které můžeme používat v kódu, ze kterého generátor voláme. Klasickou ukázkou použití generátorů je funkce xrange(). Ta stejně jako vestavěná funkce range() vrací hodnoty z nějakého rozsahu, ale na rozdíl od této funkce to dělá průběžně – nepotřebuje výsledek uložit do paměti:

<?php
function xrange($start, $end, $step = 1) {
	for ($i = $start; $i <= $end; $i += $step) {
		yield $i;
	}
}

foreach (xrange(1, 5) as $i) {
	echo "$i\n";
}

// Po dobu 0.1 sekundy vypisuje čísla.
$start = microtime(true);
foreach (xrange(1, INF) as $i) {
	if (microtime(true) - $start > .1) {
		break;
	}
	echo "$i\n";
}
?>

Možnosti využití tohoto operátoru jsou mnohem bohatší. Řekněme, že chceme spustit sadu dlouhotrvajících testů a jejich výsledky vypisovat. Logiku pro provedení testu a vypsání jeho výsledku máme samozřejmě oddělenou. Nejjednodušší řešení je spustit všechny testy, výsledky si uložit do pole a to na konec projít a výsledky vypsat. Nevýhody jsou zřejmé – na první výsledek čekáme, až dokud nedoběhnou všechny testy, a potřebujeme paměť na uložení všech výsledků. S operátorem yield můžeme výsledky vracet a vypisovat průběžně. Samozřejmě to není jediné možné řešení – funkci pro výpis výsledků můžeme např. předat do spouštěče testů a volat ji z něj. Řešení s operátorem yield je ale elegantnější.

Operátor yield je také jedním z důležitých prvků pro výkon Facebooku. V zásadě všechny funkce, které potřebují nějaká data, před jejich získáním „yieldnou“. Všechny požadavky na data se sdruží, vyřídí najednou a rozdají zpátky funkcím. To dovoluje dramaticky snížit počet komunikací s úložištěm, který díky tomu závisí jen na počtu úrovní na sobě závislých požadavků, nikoliv na celkovém počtu míst, kde nějaká data potřebujeme. Facebook tento operátor používá už dlouhou dobu díky implementaci v kompilátoru HipHop for PHP.

Syntaxe je bohatší, z generátorů lze vracet i klíče a do generátoru můžeme zvenku poslat data, detaily naleznete v PHP manuálu nebo v RFC. Generátor lze detekovat metodou ReflectionFunctionAbstract::isGenerator.

Blok finally

Blok finally se používá při ošetřování výjimek a spustí se, ať už k výjimce dojde nebo ne. PHP se bez něj dlouho obešlo, protože se v tomto bloku nejčastěji uvolňuje zabraná paměť a provádí další úklid, který PHP obvykle dělá automaticky. Ale ne o všechen úklid se PHP stará automaticky, takže nyní můžeme tento blok konečně použít.

<?php
try {
	$errorMode = $pdo->getAttribute(PDO::ATTR_ERRMODE);
	$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	return $pdo->query($sql);
} catch (RecoverableException $ex) {
	// Tady můžeme ošetřit výjimky.
	error_log($ex->getMessage());
} finally {
	$pdo->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
}
?>

Blok se dal v minulosti emulovat pomocí callbacků, nebo bez nich poněkud krkolomně:

<?php
$caught = null;
try {
	$errorMode = $pdo->getAttribute(PDO::ATTR_ERRMODE);
	$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	$return = $pdo->query($sql);
} catch (Exception $caught) {
	// Tady můžeme ošetřit výjimky.
	if ($caught instanceof RecoverableException) {
		error_log($caught->getMessage());
		$caught = null;
	}
}

// Toto je blok finally.
$pdo->setAttribute(PDO::ATTR_ERRMODE, $errorMode);

if ($caught) {
	throw $caught;
}
return $return;
?>

Detaily lze opět nalézt v PHP manuálu nebo v RFC.

Název třídy v ::class

Příjemnou novinkou, která se hodí při předávání názvů tříd funkcím je konstanta class. Užitečná je hlavně u kódu používajícího jmenné prostory.

<?php
namespace N {
	class C {
	}
	f(C::class); // Předá 'N\C'
}
?>

Konstanta nekontroluje, jestli daná třída skutečně existuje, takže ani nespouští autoloading. Detaily v RFC.

list ve foreach

Cyklus foreach nyní podporuje konstrukci list. Když jsem s PHP začínal, tak mě tuhle kombinaci napadlo použít a byl jsem trochu překvapen, že ji PHP nepodporuje. Hodí se tehdy, když se strukturovaná data předávají v číselně indexovaných polích. To po pravdě řečeno není moc často, ale pokud se tak stane, tak si můžeme ušetřit jednu dočasnou proměnnou.

<?php
$fields = array(
	array('article', 'id'),
);
foreach ($fields as list($table, $column)) {
	echo "$table.$column\n";
}
?>

Detaily opět v manuálu nebo v RFC. Jsem zvědav, jestli PHP někdy bude podporovat i preg_match('/^([^=]+)=(.*)/', $line, list(, $name, $value)), i když tato konstrukce už je poněkud divoká.

Neskalární klíče ve foreach

U normálních polí PHP dovoluje pro klíče použít pouze celá čísla nebo řetězce. Konstrukce foreach proto také podporovala pouze přiřazování čísel nebo řetězců do klíče. U iterovatelných objektů ale omezení na klíče neexistuje a mohou v nich být libovolné hodnoty. Na co se něco takového dá použít? Např. NotORM dovoluje k řádkům v tabulce přistupovat pomocí primárního klíče: $db->article[$id]. Co když je ale primární klíč vícesloupcový? NotORM pro tento případ nabízí syntaxi $db->article_tag[array("article_id" => $article_id, "tag_id" => $tag_id)]. Někomu to může připadat moc magické, někdo pro to najde smysluplné využití.

Jak novinka vypadá v praxi?

<?php
// AnyKeyArray - https://gist.github.com/vrana/5581788
$article_tags = new AnyKeyArray();

// Tohle šlo už dřív.
$article_tags[array("article_id" => $article_id, "tag_id" => $tag_id)] = $article_tag;
echo $article_tags[array("article_id" => $article_id, "tag_id" => $tag_id)];

// Tohle funguje až v PHP 5.5.
foreach ($article_tags as $key => $val) {
	print_r($key);
}

?>

Detaily v RFC. Osobně bych se přimlouval k tomu, aby pole mohlo být klíčem i v normálním poli – neexistuje žádný technický důvod, proč by to tak nemohlo být.

empty() s výrazy

Konstrukce empty() testuje, zda je proměnná nenastavená nebo se vyhodnotí jako false. To je užitečné hlavně u proměnných, které mohou být nenastavené a kde by přístup mimo tuto konstrukci (a mimo isset()) způsobil chybu. U obecných výrazů používání empty() není potřeba, protože ty jsou vždycky nastavené a pro test pravdivosti stačí použít obyčejný operátor ! (negace). To je i důvod, proč tuto konstrukci v minulosti bylo možné používat jen s proměnnými.

PHP 5.5 nicméně dovoluje použít empty() i s obecným výrazem, protože programátoři neznalí těchto interních detailů nechápou, proč empty($var) funguje a empty(f()) ne. Někomu se kód používající empty() místo ! může zdát i čitelnější, to je ale subjektivní. Konstrukci isset() zůstalo původní chování a lze ji i nadále používat jen s proměnnými. Dokumentace v manuálu nebo v RFC.

Já osobně se konstrukci empty() budu až na výjimky i nadále vyhýbat. Důvodem je právě potlačení chyby v případě neexistence proměnné:

<?php
$array = array(null);

$false = empty($aray[0]); // Na tento překlep nás PHP neupozorní.
$false = !$aray[0]; // O tomto překlepu se dozvíme.

$null = isset($aray[0]); // O tomto překlepu se nedozvíme.
$null = ($aray[0] === null); // O tomto překlepu se dozvíme.

$empty = empty($aray[-1]); // Nedozvíme se.
$empty = !array_key_exists(-1, $aray) || !$aray[-1]; // Dozvíme se.
?>

Poslední příkaz je nicméně poněkud krkolomný a empty() bych pro něj použil raději. Kontrolu neexistujících proměnných je ostatně lepší nechat nezávislé statické analýze, protože PHP to stejně dělá v mnoha směrech nedokonale.

Přístup k prvkům konstantních polí a bajtům řetězce

Droboučkou změnou bez valného smyslu a využití je povolení operátoru [] pro přístup k prvkům pole a bajtům řetězce i u konstantních hodnot.

<?php
$a = array(1, 2)[0];
?>

Nenapadá mě, proč by někdo chtěl něco takového dělat, na druhou stranu ale ani není žádný vážný důvod, proč by to mělo být zakázané. Detaily v RFC.

Práce s hesly

Pro ukládání hesel se obvykle používá hašování. Z několika důvodů to ale není tak jednoduché, jak to vypadá. V první řadě je potřeba k heslu přidat náhodnou sůl, aby nebylo poznat, že dva uživatelé mají stejné heslo. Další problém je v tom, že běžné hašovací funkce jako md5 a sha1 jsou moc rychlé, takže dovolují hrubou silou prozkoumat velké množství hašů, ať už jsou osolené nebo ne. Aplikace to obvykle řeší tak, že své uživatele týrají požadavky na velmi dlouhá a složitá hesla. Uživatelé si potom heslo nepamatují, napíšou si ho na papírek a ten přilepí na monitor. Protože jsou počítače pořád rychlejší a přístup k obrovskému výpočetnímu výkonu je čím dál jednodušší, tak by aplikace správně měly požadovat čím dál složitější hesla.

Lepší řešení je použít pomalou hašovací funkci jako např. Blowfish, která navíc dovoluje náročnost výpočtu stanovit parametrem. Generování náhodné soli také není úplně triviální. PHP 5.5 proto přináší dvě jednoduché funkce password_hash a password_verify, které heslo zahašují resp. haš zkontrolují. Detaily jsou v manuálu nebo v RFC.

<?php
$password = 'Passw0rd';

$start = microtime(true);
$hash = password_hash($password, PASSWORD_DEFAULT);
echo (microtime(true) - $start) . "\n";

$start = microtime(true);
var_dump(password_verify($password, $hash));
echo (microtime(true) - $start) . "\n";
?>

K dispozici je i uživatelská knihovna pro PHP >= 5.3.7, takže lze tyto funkce začít ihned používat.

V oblasti hašování je ještě jedna novinka – funkce hash_pbkdf2 a openssl_pbkdf2 používající algoritmus PBKDF2 schválený úřadem NIST.

Funkce array_column

Funkce array_column ze seznamu polí vytáhne jen určité sloupce a vrátí je v novém poli. Používá se typicky u dat vrácených z databáze, ze kterých chceme vytvořit seznam hodnot nebo číselník.

<?php
$articles = $pdo->query("SELECT * FROM article")->fetchAll();

// Vytvoří pole [ $id, ... ].
$ids = array_column($articles, 'id');

// Vytvoří pole [ $id => $title, ... ].
$titles = array_column($articles, 'title', 'id');

// Vytvoří pole [ $id => $article, ... ].
$articles = array_column($articles, null, 'id');
?>

Důležité je podotknout, že pokud nám stačí jen jeden nebo dva sloupce, tak si můžeme vytáhnout jenom je příkazem $statement->fetchAll(PDO::FETCH_COLUMN) resp. $statement->fetchAll(PDO::FETCH_KEY_PAIR). Pokud nicméně potřebujeme pracovat s kompletními záznamy a k tomu i jen s určitým sloupcem, tak je tato funkce velmi užitečná. Nebo samozřejmě v případě, kdy API pro získání dat není tak bohaté jako PDO.

Detaily v manuálu nebo v RFC.

Další přidané funkce a třídy

Zend OPcache

PHP 5.5 obsahuje extenzi Zend OPcache, která kešuje a optimalizuje zkompilovaný mezikód. Dělá tedy část toho, co extenze APC (ta navíc dovoluje pracovat se sdílenou pamětí), podle benchmarků to ale dělá o něco lépe.

Kromě toho přibylo ještě několik drobnějších optimalizací, jak už je ve vývoji PHP zvykem.

Zastarání extenze MySQL

Extenze MySQL je označena jako zastaralá ve prospěch extenzí MySQLi a PDO. O jejím zastarání se mluví už od uvedení PHP 5 (před téměř 9 lety), proto někoho možná překvapí, že velké projekty jako WordPress nebo MediaWiki ji stále používají jako výchozí a často i jediný oficiální způsob připojení k MySQL. I Facebook vesele běží nad touto extenzí.

Extenze MySQLi zkrátka nepřinesla žádnou killer feature – vázání proměnných je v této extenzi zpackané, perzistentní připojení ve srovnání s MySQL naopak dlouho chybělo, vícenásobné dotazy moc programátorů nepotřebuje a v PHP 5.3 uvedené asynchronní dotazy potřebují na každý dotaz jedno otevřené připojení, takže ve většině případů žádnou úsporu nepřinesou. Všechny tyto novinky navíc mohly být snadno doplněny i do extenze MySQL. Plán „přejmenujeme všechny funkce a zpřeházíme jim parametry“ moc uživatelů zkrátka neoslovil.

PDO je na tom o něco lépe, podpora více databázových systémů a rozumnější objektové rozhraní už je přeci jen smysluplná hodnota. I když třeba vázání proměnných má opět vážné nedostatky, takže pro bezproblémové využití je potřeba si stejně postavit nějakou nadstavbu. A když už takovou nadstavbu máme, tak je celkem jedno, co je pod ní.

Detaily a hlasování (mimochodem ne tak jednoznačné jako v ostatních případech) v RFC.

Další zastaralé a odstraněné obraty

Co se nevešlo

Závěr

Novou verzi považuji za nekontroverzní a logické pokračování vývoje PHP. Syntaktické novinky jsou příjemné, nové funkce jsou užitečné, yield může zcela změnit architekturu vysoce zatížených PHP aplikací, zastaralé a odstraněné obraty vesměs nebudou nikomu chybět.

Smutnou skutečností zůstává, že řada projektů bude moci tyto novinky využít až tak za tři až pět let. Většina hostingů nabízí „časem prověřené“ verze PHP a i na vlastním serveru řada uživatelů volí verzi z distribuce, jejichž správci také bývají velmi konzervativní. Aplikace určené pro nasazení v mnoha instalacích (typicky open source) jsou na tom ještě hůř, protože musí podporovat všechny verze svých uživatelů. Některé projekty teprve teď opatrně odstraňují podporu pro PHP 5.2 (ve prospěch PHP 5.3 uvedeného před čtyřmi lety). Snad se PHP 5.5 uchytí rychleji.

Jakub Vrána, Seznámení s oblastí, 21.6.2013, diskuse: 6 (nové: 0)

Diskuse

Michal:

Článek ještě nevyšel na serveru Zdroják :-) :-)

ikona Jakub Vrána OpenID:

Už je tam.

Michal:

Super. Rád si totiž čtu články zčerstva hned po vydání :-)

trestná smradlavice:

Mám volně související otázku. Je podle vás dobré po vydání nové verze jako je tato hned v tom začít psát - jak kód specifický pro ten který projekt, tak obecné, znovupoužitelné funkce? díky

ikona Jakub Vrána OpenID:

Ano, já jeden projekt pro PHP 5.5 chystám.

O:

Rek bych, ze kdo dokaze vyuzit yield, uz davno v PHP nedela. :-)
Jeste par verzi a uz to bude alfa verze normalniho jazyka ^^

Vložit příspěvek

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2016 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.