Yield a další novinky PHP 5.5

PHP logo

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.

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

  • K funkcím intval, strval a podobným přibyla i funkce boolval. Ta se může hodit třeba ve funkci array_map nebo obecně všude, kam potřebujeme předat callable. Jinde se dalo už dřív použít přetypování (bool).
  • Třída DateTimeImmutable je velmi podobná třídě DateTime. Jediným rozdílem je, že nová třída nikdy nemění svou hodnotu a místo toho vrací naklonovaný objekt.
  • Do extenze cURL přibyly nové funkce, především curl_share_init, a třída pro práci s uploadovanými soubory CURLFile. API pro uploadování souborů byla dříve katastrofa – pokud jste volbě CURLOPT_POSTFIELDS předali data začínající zavináčem, tak se část za zavináčem interpretovala jako název souboru. Kromě toho, že se nedala poslat data začínající zavináčem, to vedlo i k bezpečnostním problémům, proto PHP 5.2 vyžaduje předání tohoto řetězce v poli a verze 5.5 to konečně označuje jako zastaralé právě ve prospěch třídy CURLFile.
  • Do extenze MySQLi přibyla funkce pro zajájení transakce mysqli_begin_transaction a funkce pro práci se savepointy. Funkce mysqli_commit a mysqli_rollback přijímají nové parametry. Smysl mají tyto funkce při použití extenze mysqlnd_ms pro rozkládání zátěže, jinak se dá zůstat u normálního $mysqli->query("COMMIT").
  • K funkci json_last_error vracející kód chyby přibyla funkce json_last_error_msg. Funkce json_encode po vzoru json_decode přijímá parametr $depth.
  • V extenzi PostgreSQL přibyly funkce pg_escape_literal (kterou je vhodné používat místo pg_escape_string, protože se v dotazu neuvádí apostrofy) a pg_escape_identifier. Já jsem se obdobu druhé jmenované pokusil přidat do PDO, ale nedotáhnul jsem to do konce.
  • Do extenze pro práci s obrázky přibyly nové funkce, především pro oříznutí a změnu velikosti obrázku.
  • Funkce set_error_handler přijímá null a funkce set_exception_handler vrací předchozí handler i při předání null.
  • V CLI lze změnit a získat název procesu funkcemi cli_set_process_title a cli_get_process_title.

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

  • Funkce php_logo_guid, php_egg_logo_guid, php_real_logo_guid a zend_logo_guid dříve používané pro vykreslení loga na stránce phpinfo už nejsou potřeba, protože loga se vykreslují pomocí protokolu data:. Tyhle funkce ostatně nemusely být viditelné nikdy, protože stránka phpinfo si je volala interně.
  • Modifikátor /e funkce preg_replace je nově označen jako zastaralý. S tímto modifikátorem je spousta legrace, protože při jeho použití se druhý parametr interpretuje jako PHP kód. Pokud tedy někoho napadne pro argumenty této funkce použít uživatelský vstup, je o bezpečnost aplikace vystaráno. Problém dokonce až donedávna nastal i při použití uživatelského vstupu jen pro část mezi oddělovači, protože všechno za nulový bajtem se ořízlo. Komplikované je chování i v případě, kdy „matchneme“ uvozovky nebo apostrofy. Přítomnost tohoto modifikátoru považuji za „perlismus“, navíc nikdy nebyl potřeba, protože jeho činnost lépe zastane funkce preg_replace_callback. Detaily v RFC.
  • Z extenze cURL bylo odstraněno volitelné převzetí obsluhy URL v běžných PHP funkcích. Pokud zavoláte např. readfile("http://example.com"), tak PHP naváže komunikaci se vzdáleným serverem. Volbou --with-curlwrappers šlo nastavit, aby to dělalo cURL. Pokud tuto vlastnost někdo používal, tak většinou omylem.
  • Windows XP a 2003 už nejsou podporované. Naopak přibyly oficiální buildy pro 64bitová Windows, ty ale bohužel nesmyslně používají čtyřbajtový int místo osmibajtového.

Co se nevešlo

  • Accessors – byl předložen návrh, který by dovolil přetížit přístup k vlastnostem objektu. Metody __get a spol. to umožňují jen pro neexistující vlastnosti, takže v praxi se používají hlavně uživatelské metody get* a set* nadefinované pro každou vlastnost, které kód zanáší spoustem balastu. Syntaxe této novinky byla nicméně trochu podivná. Hlasovalo pro ni 60 % vývojářů, ale pro změnu jazyka je potřeba 2/3 hlasů.
  • Scalar type hints – PHP dovoluje v deklaraci funkce specifikovat požadovaný typ parametrů, jde to ale jen pro objekty, pole a callbacky. Hodnotu null lze předat, pokud je parametr volitelný a jeho výchozí hodnota je null. Skalární typy (jako řetězce nebo čísla) ani nadále vynutit nejde. HipHop for PHP touto vlastností oplývá a celkem jsem si na ni zvykl.
  • Return type hints – ani návratový typ funkcí nadále nejde určit. Při návrhu API by se to docela hodilo – interface by mohl určit, jaké očekává návratové hodnoty, a implementace by to musely dodržet. Facebook tohle opět podporuje, i když v jiné syntaxi (návratový typ se uvádí za parametry a dvojtečkou).

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.

Autor pracuje jako Software Engineer v týmu Gmail Security. V minulosti se zabýval především PHP, o kterém napsal knihu a podílel se na oficiální dokumentaci. Je autorem nástroje pro správu databáze Adminer. Poznámky si zapisuje na weblog PHP triky.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 20

Přehled komentářů

Jan Novák
jan.tvrdik Doplnění k článku
koubel 5.4 už tu také nebude moc dlouho
DavidDvorak Re: Doplnění k článku
Martin Prokeš a slepenec se valí dál
Čelo Re: a slepenec se valí dál
Martin Prokeš Re: a slepenec se valí dál
yac Patlani rtenky na prase
honzamarek88 skalární typehinty
David Grudl Re: skalární typehinty
Mastodont Podpora Unicode?
Jakub Vrána Re: Podpora Unicode?
michal.kolesa Jak zpracovat hostng, pro nasazení
DavidDvorak Re: Jak zpracovat hostng, pro nasazení
ONEbit hosting Re: Jak zpracovat hostng, pro nasazení
DavidDvorak yield
Jenda Re: yield
Jakub Vrána Re: yield
DavidDvorak Re: yield
Jakub Vrána Re: yield
Zdroj: https://www.zdrojak.cz/?p=9006