Funkce a konstanty ve jmenných prostorech
Nekvalifikované volání strlen() uvnitř jmenného prostoru (namespace) může v PHP znamenat
dvě zcela různé funkce – a ze samotného kódu jednoho souboru nelze poznat, o kterou z nich jde. Nástroje pro kontrolu
stylu proto u celé řady užitečných oprav pouze hádají a samotné PHP nemůže využít kompilátorové zkratky, se
kterými je takové volání dvakrát až třikrát rychlejší. Obvykle přitom stačí jediný řádek v konfiguraci, kterým
DressCode řeknete, jaké funkce a konstanty v namespaces máte: opravy rázem přestanou být rizikové a kód lze zapsat tak,
aby jej PHP spouštělo tou nejrychlejší možnou cestou.
Jedno jméno, dva významy
Podívejte se na následující kód:
namespace App;
if (is_null($user)) {
// ...
}
Která funkce se zde ve skutečnosti volá? Většina vývojářů automaticky předpokládá globální
is_null(), kterou zná každý. Jenže PHP o tom rozhoduje až za běhu: nejprve ověří, zda náhodou neexistuje
funkce App\is_null(), a teprve když ji nenajde, sáhne po té globální. Bohatě stačí, aby kdekoliv v projektu
jiný soubor deklaroval:
namespace App;
function is_null(mixed $value): bool
{
return $value === null || $value === '';
}
a volání výše najednou provádí něco docela jiného. Na souboru, v němž je volání zapsáno, přitom vůbec nezáleží – rozhoduje výhradně to, zda byl v daném požadavku načten i onen druhý soubor.
Naprosto stejně se chovají konstanty: zápis PHP_EOL v namespace App představuje
App\PHP_EOL, pokud existuje, a teprve v opačném případě globální PHP_EOL. U tříd tohle PHP
nedělá: nekvalifikovaná Exception v namespace App znamená vždy nekompromisně
App\Exception a globální třídu musíte zapsat jako \Exception nebo ji importovat přes
use.
Kus historie
Jmenné prostory přineslo PHP 5.3 a jejich podoba se rodila v těžkých bolestech. Verze měla původně vyjít na podzim
roku 2008, první alfa vyšla v srpnu, ale o tom, jak mají namespaces v praxi vlastně fungovat, se mezi jádrovými
vývojáři PHP bouřlivě diskutovalo ještě v říjnu. Tehdy padlo i slavné rozhodnutí nahradit původně zvažovaný
oddělovač :: zpětným lomítkem \, protože dvojtečky nebylo možné v parseru spolehlivě odlišit
od volání statických metod. PHP 5.3.0 nakonec vyšlo až 30. června 2009 – s devítiměsíčním zpožděním.
Jednou z nejtvrdších otázek byla právě tato: co má znamenat identifikátor, před kterým nestojí žádný jmenný
prostor? Návrh z 30. října 2008 zvolil pro třídy a pro funkce zcela odlišné řešení. U tříd PHP návrat ke
globálnímu názvu nepovoluje, protože ve spojení s autoloadingem by to znamenalo fatální propad výkonu: u každého
new Exception by se PHP muselo nejprve pokusit načíst soubor pro App\Exception. Funkce a konstanty
však autoloading nemají, takže toto riziko nehrozilo. A protože naprostou většinu základní výbavy PHP tvoří právě
globální funkce, dostaly tichý fallback na globální prostor. Kód napsaný před érou jmenných prostorů tak mohl přejít
do namespace, aniž by vývojáři museli před každé strlen() či in_array() vkládat zpětné
lomítko.
Byl to pragmatický kompromis své doby. Jeho daní však je, že o významu nekvalifikovaného volání funkce nerozhoduje obsah konkrétního souboru, ale stav celého běžícího programu.
Proč to pálí nástroje
Statické analyzátory a formátovače kódu pracují s jednotlivými soubory, nikoliv s běžícím procesem. U každého
is_null($x) uvnitř namespace proto stojí před stejnou hádankou jako vy a odpověď s jistotou neznají. Celá
řada velmi žádaných modernizací přitom přímo stojí na předpokladu, že jde o známou vestavěnou funkci PHP:
- přepis
is_null($x)na$x === null, intval($x)na přetypování(int) $x,sizeof($a)nacount($a),dirname(__FILE__)na konstantu__DIR__,get_class()naself::class,- doplnění
use function count;, aby kompilátor mohl volání optimalizovat.
Kdyby šlo o vaši vlastní funkci App\is_null(), každá z těchto úprav by skrytě změnila chování
programu. Zodpovědný nástroj proto musí takové zásahy klasifikovat jako rizikové opravy (risky fixes) a bez výslovného
souhlasu se do nich nepouštět. PHP CS Fixer má mezi rizikovými pravidly například is_null,
no_alias_functions nebo dir_constant – všechna se shodným varováním: jsou riziková, pokud je
funkce předefinovaná.
A nejde o pouhou teoretickou hrozbu. Přesně na tomto mechanismu stojí osvědčený trik v unit testech: testovací
knihovny typu php-mock deklarují přímo v testu funkci App\time() nebo App\file_get_contents(), aby
testovaný kód v namespace App namísto systémového volání použil podvrženou mock funkci.
Buďme pragmatičtí
Kolik vlastních funkcí a konstant v jmenných prostorech reálně máte? Téměř jistě jen naprosté minimum, v mnoha
projektech dokonce čistou nulu. Funkce se v PHP totiž neautoloadují: aby bylo možné funkci zavolat, musí někdo
její soubor předem načíst přes require nebo direktivou files v composer.json. Z toho
důvodu se objektový kód před lety definitivně přesunul do tříd a funkce v namespaces zůstaly okrajovou záležitostí.
Autoloading funkcí byl v komunitě PHP navrhován opakovaně, součástí jazyka se však dosud nestal.
To je současně skvělý důvod, proč na nekvalifikované volání funkcí z vlastního namespace raději vůbec
nespoléhat: když se soubor s funkcí shodou okolností nenačte, PHP neohlásí chybu, ale potichu zavolá globální funkci
stejného jména (pokud existuje). Explicitní import use function App\is_null; soubor sice také nenačte, ale
volání v takovém případě okamžitě a srozumitelně selže na Call to undefined function App\is_null().
Situace je tedy přehledná: teoretická nejistota komplikuje život všem, ale skutečné jmenné funkce v namespaces jsou vzácností. Stačí je jen pojmenovat.
Řekněte DressCode, co je co
V naprosté většině projektů žádné vlastní funkce ani konstanty v jmenných prostorech nedeklarujete. Pak vám v konfiguraci stačí jediný řádek:
nameResolution: certain
Tímto zápisem DressCode potvrdíte, že každé nekvalifikované jméno funkce či konstanty, které daný soubor neimportuje ani sám nedeklaruje, je zaručeně globální. Nikoli „s velkou pravděpodobností“, ale naprosto jistě.
Pokud nějaké vlastní funkce či konstanty v namespaces přece jen máte, jednoduše je vyjmenujete v klíči
namespaces – funkce pod functions a konstanty pod constants. Zápis je shodný se
syntaxí příkazu use, včetně skupinových závorek. Položku se složenou závorkou uveďte v uvozovkách, aby
ji parser NEONu nepovažoval za mapu:
nameResolution: certain
namespaces:
functions:
- 'App\Utils\{format, parse}'
- App\Legacy\helper
constants:
- 'App\{VERSION, DEBUG}'
Funkci uvedenou v tomto seznamu pak DressCode považuje za funkci daného namespace všude tam, kde se v něm volá bez importu. U funkcí nezáleží na velikosti písmen vůbec, u konstant záleží na velikosti písmen pouze v jejich vlastním názvu (v názvu namespace nikoliv) – přesně podle chování samotného PHP.
Seznamy a direktiva nameResolution představují dvě odlišné věci, a proto má každá vlastní klíč. Seznam
deklaruje fakt: tato funkce v projektu existuje. Direktiva nameResolution představuje rozhodnutí: v projektu
neexistuje nic dalšího.
nameResolution |
co znamená |
|---|---|
certain |
uvedené seznamy jsou úplné; cokoliv v nich nefiguruje, je zaručeně globální |
uncertain |
seznamy nemusí být kompletní; chová se stejně, jako když klíč v konfiguraci chybí |
Při nastavení uncertain postupuje DressCode velmi opatrně: nekvalifikovaný název sice vnímá jako
globální, ale jakoukoliv navazující opravu považuje za rizikovou. Oznámí ji jako varování s přesným
odůvodněním na řádku pod chybou (například the namespace may declare `is_null()`) a kód reálně upraví
teprve tehdy, když rozhodnutí povolíte v fixRisky nebo spustíte fix --fix-risky. Proto je
uncertain výchozí hodnotou: samotná přítomnost pravidla v konfiguraci ještě neznamená automatický souhlas
se zásahy do logiky. Jakmile však nastavíte certain, tyto opravy okamžitě přestanou být rizikové.
Každé pravidlo z toho těží
Zásadní je, kde se tato informace uplatňuje. DressCode ji nepředává jednotlivým pravidlům izolovaně, ale
poskytuje ji centrální analýze NameResolver, která překládá názvy napříč celým nástrojem. Ptají se jí
vestavěná pravidla a stejně tak se jí dotazuje vaše vlastní
pravidlo. Znalost leží na jediném místě a žádné pravidlo si nemusí vést vlastní seznam výjimek.
Pravidla kladou analýze dvě klíčové otázky:
- Co je daný prvek zač. Dotaz
isGlobalFunctionCall($node, 'is_null')vrátítruepro globální funkci, zatímcoApp\is_null()ze seznamu za ni nepovažuje. Scertainje odpověď jistá, v opačném případě jde o předpoklad. - Jak jistá tato odpověď je. Dotaz
getUnqualifiedResolution('is_null', SymbolKind::Function, $node)přesně určí, kam nekvalifikovaný název směřuje: jistě do globálního prostoru (Global), jistě do jmenného prostoru (Namespaced), nebo do globálního prostoru pouze za předpokladu, že jej namespace jinde nedeklaruje (Uncertain). Pravidlo pak svou opravu označí za rizikovou výhradně u variantyUncertain– a v projektu scertaintedy nikdy.
Funguje to spolehlivě i v opačném směru, kdy pravidlo samo vkládá název funkce do kódu. Pravidlo
hygiene.aliasFunctions nahrazuje sizeof() za count() a nový název zapíše ve stejném
tvaru jako původní: \sizeof() převede na \count(), holé sizeof() na holé
count(). Pokud však daný soubor importuje či deklaruje vlastní funkci count, nebo pokud její
deklaraci v namespace uvádí konfigurace, zapíše raději \count(), aby opravený kód zaručeně volal stejnou
funkci jako předtím.
Všechna pravidla tak čtou identifikátory naprosto shodně a výsledek nezávisí na tom, které pravidlo se ptá dříve.
Nová funkce neproklouzne
Statický seznam by byl k ničemu, kdyby v čase zastaral. Proto jej hlídá samotný DressCode: jakmile konfigurace obsahuje
nameResolution: certain, aktivuje se interní kontrola pod cestou namespaces.functions a
namespaces.constants. Pokud v kontrolovaném kódu objeví novou funkci deklarovanou v namespace, která v seznamu
chybí, okamžitě na ni upozorní (totéž platí pro const i pro volání define('App\X', ...)).
Počítá přitom i s podmíněnými deklaracemi polyfillů uvnitř if (!function_exists(...)). Jakmile kdokoliv do
projektu přidá například funkci App\Utils\slugify(), příkaz dresscode check vás ihned vyzve
k jejímu doplnění do konfigurace:
src/Utils/helpers.php error 12:10 Function `App\Utils\slugify()` must be listed in `namespaces.functions`. namespaces.functions
Tento mechanismus je naprosto spolehlivý, protože deklaraci lze na rozdíl od volání odhalit přímo: nachází se
v souborech, které DressCode prochází. Pozor si dejte pouze na cesty vyloučené z kontroly. Pokud funkci deklarujete
například ve složce testů, kterou DressCode nekontroluje, a produkční kód ji volá bez importu, zapište ji do seznamu
ručně. Při nastavení uncertain je tento hlídač neaktivní, protože není co ověřovat. S certain
jej nevypne ani přepínač --only: opravy závisející na úplnosti seznamu nesmí přijít o kontrolu, která
tuto úplnost garantuje.
Ani úvodní seznam nemusíte sepisovat ručně. Příkaz dresscode init automaticky projde celý projekt, detekuje
všechny funkce i konstanty deklarované v namespaces a zapíše jejich seznamy rovnou s direktivou
nameResolution: certain. Pokud by některý soubor nešlo naparsovat (takže by nebylo jisté, co deklaruje),
init direktivu jistoty nezapíše a v komentáři problematický soubor výslovně jmenuje.
Do budoucna se přímo nabízí, aby nově přidanou funkci do konfiguračního souboru automaticky doplňoval
samotný příkaz dresscode fix.
Pomocné funkce v testech
Testy si často deklarují pomocné funkce přímo ve svém testovacím namespace – například
App\Tests\createUser(). Zapisovat je do globální konfigurace a hlídat jejich seznam by bylo zbytečně otravné.
Pro testovací složku proto stačí vytvořit lokální přepis konfigurace, který určí, že
v této části projektu seznamy kompletní nejsou:
nameResolution: certain
overrides:
- paths: [tests]
nameResolution: uncertain
Soubory ve složce tests pak DressCode posuzuje obezřetně: opravy vyžadující globální funkci se v nich
pouze nahlásí jako riziková varování a hlídač chybějících deklarací zde mlčí. Produkční kód v src
přitom dál naplno těží ze stoprocentní jistoty. Funguje to spolehlivě proto, že kód v src pomocné funkce
z testů nikdy nevolá, takže na význam identifikátorů v produkčním kódu nemají žádný vliv.
Jedna výjimka však stojí za pozornost: zmíněný trik s mockováním funkcí v testech (viz výše). Pokud test deklaruje funkci App\time(), aby produkční kód
v namespace App zavolal mock, týká se tato změna chování volání v src, nikoliv v testech.
Taková funkce proto do projektového seznamu namespaces.functions rozhodně patří, aby ji pravidla v
src nechala netknutou.
Funkce, které deklaruje framework
Někdy píšete kód přímo do jmenného prostoru frameworku, protože vás k tomu jeho architektura přímo vybízí.
Typickým příkladem je Symfony, které konfiguraci služeb směřuje do souborů začínajících
namespace Symfony\Component\DependencyInjection\Loader\Configurator; – a to právě proto, abyste mohli volat
pomocné funkce service(), param() nebo env() přímo bez otravných importů. Tyto funkce
však deklaruje samotné Symfony uvnitř složky vendor, kam DressCode běžně nenahlíží.
O těchto specifických funkcích DressCode ví automaticky: jakmile má projekt nainstalovaný balíček
symfony/dependency-injection, přidá tyto funkce do seznamu namespaces.functions zcela sám, bez
jediného řádku konfigurace a při použití libovolného standardu. Ve výpisu dresscode config je najdete na
řádku Names s označením původu DressCode for symfony/dependency-injection.
Jiné balíčky mohou vlastní seznamy dodat prostřednictvím klíče namespaces ve svých migračních souborech, které se přirozeně sloučí s vaší
konfigurací. Samotnou direktivu nameResolution však žádný balíček ani preset nastavit nemůže: znají totiž
framework, ale neznají váš vlastní kód.
Rychlejší volání, když je jméno jisté
PHP před spuštěním překládá zdrojový kód do instrukcí pro svůj virtuální stroj (Zend Engine). U desítek funkcí a vestavěných konstant přitom umí výrazně zkrátit cestu: nahradí volání jedinou instrukcí procesoru, spočítá výsledek dopředu při kompilaci nebo funkci zavolá bez režie vytváření zásobníku. Podívejme se detailně na to, co přesně PHP provádí, co mu v optimalizacích nejčastěji brání a jak kód zformátovat, aby běžel maximální rychlostí.
Co PHP s kódem dělá, než ho spustí
PHP nikdy nespouští surový text zdrojového kódu tak, jak jej vidíte v editoru. Nejprve jej přeloží do posloupnosti elementárních instrukcí zvaných opkódy (opcodes), které teprve následně vykonává virtuální stroj. Se zapnutou OPcache (jež je na produkčních serverech naprostým standardem) se přeložené instrukce uloží do sdílené paměti, aby se soubor nemusel kompilovat při každém HTTP požadavku znovu, a před uložením je ještě optimalizuje vestavěný modul OPcache Optimizer.
Běžné volání uživatelské funkce představuje pro virtuální stroj překvapivě dost práce. Například řádek
$len = foo($s); se přeloží přibližně takto:
INIT_FCALL "foo" připrav volání
SEND_VAR $s předej argument
DO_ICALL zavolej funkci
Virtuální stroj musí na zásobníku alokovat nový rámec volání (call frame), zkopírovat do něj argumenty a předat
řízení funkci napsané v C, která si argumenty musí zkontrolovat a přečíst. U složité funkce vykonávající
databázový dotaz je tato režie zanedbatelná. U elementární funkce typu strlen(), která pouze přečte délku
uloženou v hlavičce řetězce v paměti, je však samotná režie volání mnohonásobně dražší než samotný
výpočet.
Proto má PHP pro celou řadu funkcí připravené optimalizační zkratky. Téměř všechny však narážejí na stejnou podmínku: kompilátor musí už při překladu s naprostou jistotou vědět, kterou funkci voláte.
Když PHP ví, co voláte
Kompilátor PHP důvěrně zná zhruba tři desítky základních funkcí a jejich volání dokáže nahradit přímo
vyhrazeným opkódem. Napište \strlen($s) a z celého složitého volání zbyde jediná instrukce:
T1 = STRLEN $s
Žádná alokace rámce na zásobníku, žádné předávání argumentů. Podobné zkratky využívají i další funkce:
- typové kontroly
is_array(),is_string(),is_int(),is_null()a ostatníis_*()se promění v přímou instrukci kontroly typu hodnoty, - funkce
intval(),strval(),boolval()afloatval()se přeloží na přímočaré přetypování (cast), - funkce
count(),gettype(),get_class(),array_key_exists(),defined()čifunc_get_args()se transformují do vlastních vyhrazených instrukcí, - volání
in_array()s polem řetězců nebo čísel zapsaným přímo v kódu kompilátor přetaví do vyhledávání v předem sestavené hash tabulce, takže na délce pole prakticky nezáleží, sprintf()se od PHP 8.4 interně promění v obyčejné spojování řetězců, pokud formátovací řetězec obsahuje pouze zástupné znaky%sa%dv počtu přesně odpovídajícím předaným hodnotám,- volání
strlen('hello')nebochr(65)s konstantním literálem kompilátor vyhodnotí okamžitě a do kódu zapíše rovnou hotový výsledek.
Proč tedy nestačí zapsat prosté strlen($s) bez lomítka? Právě kvůli jmenným prostorům. V souboru
s deklarací namespace App; může nekvalifikované strlen() teoreticky znamenat funkci
App\strlen(), pokud ji někdo deklaruje. Kompilátor to při překladu nemůže vědět, a proto musí vytvořit
neoptimalizované obecné volání, jehož cíl se dohledává až za běhu:
INIT_NS_FCALL_BY_NAME "App\strlen"
SEND_VAR_EX $s
V1 = DO_FCALL_BY_NAME
Kompilátor optimalizační zkratku nasadí pouze tam, kde má stoprocentní jistotu: když funkci zapíšete s úvodním
lomítkem jako \strlen(), když ji explicitně importujete přes use function strlen;, nebo když soubor
žádný jmenný prostor nemá.
Jak velký rozdíl to v praxi přináší? Zde je měření dvaceti milionů volání v těsné smyčce na PHP 8.5 (se zapnutou OPcache a vypnutým JIT):
| volání ve jmenném prostoru | čas |
|---|---|
strlen($s) |
384 ms |
\strlen($s) |
165 ms |
is_array($s) |
473 ms |
\is_array($s) |
151 ms |
Se zkratkou běží volání dvakrát až třikrát rychleji. V absolutních číslech jde zhruba o deset až patnáct nanosekund na jedno volání – při běžném zpracování webového požadavku rozdíl nepostřehnete, ale v cyklu procházejícím statisíce položek už je zrychlení velmi znatelné.
Co spočítá OPcache předem
Optimalizátor v OPcache jde ještě dál. Pokud zná hodnoty všech argumentů už při kompilaci, funkci rovnou v paměti spustí a do výsledných opkódů zapíše pouze hotovou návratovou hodnotu. Následující funkce:
function separator(): string
{
return \str_repeat('-', 3);
}
se v opkódech zredukuje na prosté RETURN "---". Tato optimalizace funguje spolehlivě i přes mezilehlé
proměnné: z kódu $sep = ', '; return \implode($sep, ['a', 'b']); zůstane ve zkompilovaném kódu pouze hotový
řetězec "a, b". PHP 8.4 dokáže tímto způsobem předem vyhodnotit výsledek více než 170 deterministických
funkcí (například str_repeat(), implode(), strtoupper(), substr(),
trim() nebo version_compare()).
Zvláštní péči věnuje OPcache funkcím, které zjišťují stav prostředí: function_exists('mb_strlen'),
extension_loaded('intl'), is_callable('strlen'), defined('PHP_EOL') nebo
constant('PHP_INT_SIZE'). Pokud kompilátor odpověď zná, zapíše rovnou true nebo konkrétní
hodnotu konstanty. U function_exists() to však provede výhradně tehdy, když funkce reálně existuje:
chybějící funkce se totiž může objevit později (např. po načtení dalšího souboru), takže zápornou odpověď musí
PHP vždy prověřovat až za běhu.
I zde platí naprosto stejná podmínka: kompilátor musí mít jistotu, o jakou funkci jde.
Konstanty se dosadí rovnou
Vestavěné konstanty definované přímo v PHP nebo v jeho rozšířeních (jako PHP_EOL,
PHP_INT_MAX, PHP_VERSION_ID či E_ALL) se za běhu nemění. Kompilátor proto namísto
jejich vyhledávání rovnou vloží jejich literální hodnotu, takže výraz 'a' . \PHP_EOL se rovnou přeloží
jako hotový spojený řetězec. Mnohem důležitější než samotné ušetření paměťového přístupu je však to, co tato
optimalizace umožňuje dále: optimalizátor OPcache s touto hodnotou počítá a dokáže zcela eliminovat mrtvé
větve kódu:
if (\PHP_VERSION_ID >= 80400) {
// kód pro nové PHP
} else {
// kód pro starší PHP
}
Při běhu na PHP 8.5 z celé této podmínky zbude v paměti pouze první větev – kód z větve else se
do výsledných opkódů vůbec nedostane. Naprosto stejně dopadne například
if (\PHP_OS_FAMILY === 'Windows').
A opět platí železné pravidlo: název konstanty musí být jednoznačný. Nekvalifikované PHP_VERSION_ID
v namespace totiž může znamenat App\PHP_VERSION_ID (stejně jako
u funkcí). PHP jej proto musí vyhodnocovat až za běhu a celá podmínka včetně mrtvé větve v paměti zůstane.
Pomůže zápis \PHP_VERSION_ID nebo import use const PHP_VERSION_ID;. Jedinou výjimkou jsou klíčová
slova true, false a null, která PHP optimalizuje vždy.
Uživatelské globální konstanty, které si v kódu definujete sami, PHP se zapnutou OPcache přímo nedosazuje, protože jejich hodnota se mezi jednotlivými požadavky může lišit.
Jen tam, kde to kompilátor využije
Nejjednodušším nápadem by bylo opatřit zpětným lomítkem či importem úplně každou konstantu, kterou PHP zná.
Přesně tak to dělají běžné nástroje: lomítko vkládají před každou vestavěnou konstantu hlava nehlava. Jenže
samotné dosazení hodnoty ušetří pouhé vyhledání v hash tabulce konstant (v našem měření zanedbatelné
2,5 nanosekundy). Skutečný užitek přichází teprve ve chvíli, kdy s hodnotou kompilátor dále kalkuluje: v podmínce,
jejíž mrtvou větev dokáže zahodit, nebo ve složeném konstantním výrazu typu ENT_QUOTES | ENT_HTML5.
DressCode proto inteligentně sleduje kontext, v jakém konstanta stojí. Zápis PHP_VERSION_ID v podmínce
if (PHP_VERSION_ID >= 80400) automaticky získá use const, protože díky tomu z paměti zmizí
celá neplatná větev. Naproti tomu konstanta E_USER_DEPRECATED předaná jako argument do
trigger_error() zůstane holá, protože import by v takovém případě nepřinesl nic než zbytečný řádek
navíc v hlavičce souboru.
Tento přístup jsme podrobně změřili na rozsáhlém open-source projektu čítajícím téměř tři tisíce souborů. Holá konstanta PHP se v něm vyskytuje na 627 místech, ale kompilátor dokáže s její hodnotou aktivně počítat pouze na 117 z nich. Plošný import všeho by do hlaviček souborů přidal 457 nových řádků ve 136 souborech. DressCode jich naproti tomu doplní pouze 82 ve 40 souborech (tedy více než pětkrát méně) – a přitom stoprocentně pokryje každé místo, kde optimalizace přináší reálný užitek.
Volání bez rámce
PHP 8.4 přineslo další významnou optimalizaci, tentokrát pro devatenáct nejčastěji využívaných funkcí:
str_contains(), str_starts_with(), strpos(), strstr(), substr(),
str_replace(), strtr(), trim(), implode(), in_array(),
preg_match(), preg_replace(), min(), max(), class_exists(),
property_exists(), dirname(), dechex() a is_numeric(). Každá z nich má
vedle své standardní implementace ještě speciální interní variantu, které virtuální stroj předává parametry napřímo
v registrech procesoru bez alokace zásobníku. V terminologii PHP se jim říká bezrámcové funkce (frameless functions).
Tato optimalizace funguje jako jediná i při nekvalifikovaném zápisu v namespace. Kompilátor totiž připraví obě cesty a rozhodne se mezi nimi při prvním spuštění:
JMP_FRAMELESS "app\str_contains" když App\str_contains() neexistuje, skoč dolů
INIT_NS_FCALL_BY_NAME "App\str_contains" obecné volání
...
V1 = FRAMELESS_ICALL_2(str_contains) $s "x" volání bez rámce
Informaci o tom, zda App\str_contains() v projektu existuje, si PHP po prvním průchodu zapamatuje v OPcache,
takže každé další volání stojí pouze jediný nepodmíněný skok. Explicitní import zde proto rychlostně nepřináší
téměř nic navíc.
Co zkratky vypne
Žádná z uvedených kompilátorových zkratek nefunguje ve chvíli, kdy volání obsahuje pojmenované nebo rozbalené
argumenty (...$args). Pro taková volání musí kompilátor vždy vygenerovat plnohodnotné pomalé volání.
Pojmenované argumenty se píší především kvůli čitelnosti: zápis in_array($role, $roles, strict: true) na
první pohled vysvětluje význam hodnoty true. Jenže PHP musí v takovém případě funkci zavolat obecným
mechanismem a parametr složitě dohledávat podle názvu:
| volání | čas |
|---|---|
\in_array('c', $list, strict: true) |
677 ms |
\in_array('c', $list, true) |
499 ms |
\in_array('c', ['a', 'b', 'c', 'd'], true) |
170 ms |
Druhý řádek představuje bezrámcové volání, třetí pak přímou instrukci s předem sestavenou hashovací tabulkou.
Pojmenovaný argument zdrží běh dokonce více než nekvalifikovaný název: str_contains($s, 'x') bez lomítka
v namespace trvá 304 ms, zatímco \str_contains($s, needle: 'x') zabere 462 ms.
Rozbalené argumenty (např. \strlen(...$args)) optimalizaci vyřadí také, protože kompilátor předem nezná
počet předávaných hodnot. A bezrámcové volání má navíc každá funkce optimalizované pouze pro určitý počet
argumentů: například preg_match($pattern, $s) běží bez rámce, zatímco varianta s předaným polem
odchycených skupin preg_match($pattern, $s, $matches) už vyžaduje standardní volání.
Nečekejte zázraky
Tyto zkratky urychlí jednotlivá volání o jednotky nanosekund. Typická webová aplikace tráví naprostou většinu času
čekáním na databázi, síťovou komunikaci či disk, takže celkovou odezvu stránky tato mikrooptimalizace znatelně
neovlivní. Rozdíl však okamžitě poznáte v kódu, který rotuje v těsných smyčkách: v parserech, šablonovacích
systémech, při zpracování objemných polí nebo v knihovnách volaných stotisíckrát během jediného požadavku. Skvělou
zprávou je, že vás to nestojí vůbec nic: DressCode kód upraví sám a ten zůstane perfektně čitelný – pouze
s několika málo importy navíc a s čistým true namísto strict: true.
Sada compilerOptimizations
Hlídat všechny tyto nuance ručně by bylo neúnosné. Proto tuto práci kompletně přebírá sada compilerOptimizations, kterou stačí
připojit ke svému standardu:
use:
- compilerOptimizations
Žádný standard tuto sadu nezapíná automaticky. Sada importuje funkce, se kterými kompilátor zachází přednostně, a to výhradně tam, kde se některé jejich volání s danými argumenty skutečně zrychlí. Konstanty importuje tam, kde s nimi kompilátor aktivně počítá (u konstant navíc umožňuje použít úvodní lomítko namísto importu). Ostatní globální funkce a konstanty ponechává holé, protože kvalifikovaný zápis by jim žádný výkon navíc nepřinesl. A u volání, která kompilátor optimalizuje, automaticky převede bezpečné pojmenované argumenty zpět na poziční a rozbalené argumenty nahlásí.
Sadu tvoří čtyři klíče ze sekce qualification: function.global: bare,
function.optimized: imported, constant.global: bare a
constant.optimized: [imported, backslashed]. Pokud dáváte přednost úvodnímu lomítku před importem, můžete si
u optimalizovaných názvů nastavit backslashed (podrobnosti přináší následující kapitola).
Volání, která lze přímo nahradit nativní jazykovou konstrukcí (například is_null($x) porovnáním
$x === null nebo intval($x) přetypováním (int) $x), tato sada neřeší. Takové úpravy
spadají pod rozhodnutí sekce hygiene: jazyková konstrukce
žádné kompilátorové zkratky nepotřebuje, je maximálně rychlá ze své podstaty a na jmenném prostoru vůbec nezávisí.
Standard nette je má zapnuté.
Takhle vypadá reálná metoda před opravou v projektu cíleném na PHP 8.4 s aktivním
nameResolution: certain:
use:
- compilerOptimizations
nameResolution: certain
targets:
php: '8.4'
namespace App;
class Access
{
public function isAllowed(array $roles, string $role): bool
{
if (PHP_VERSION_ID < 80400) { // Global constant `PHP_VERSION_ID` must be imported.
trigger_error('Old PHP', E_USER_DEPRECATED);
}
return in_array($role, $roles, strict: true) && strlen($role) > 2; // A named argument disables the compiler optimization of `in_array()`. // Global function `strlen()` must be imported.
}
}
namespace App;
use function strlen;
use const PHP_VERSION_ID;
class Access
{
public function isAllowed(array $roles, string $role): bool
{
if (PHP_VERSION_ID < 80400) {
trigger_error('Old PHP', E_USER_DEPRECATED);
}
return in_array($role, $roles, true) && strlen($role) > 2;
}
}
Import získala funkce strlen(), kterou PHP optimalizuje vždy, a konstanta PHP_VERSION_ID, se kterou
kompilátor kalkuluje v podmínce a může díky ní odstranit mrtvou větev. Funkce in_array() zůstala holá:
import by jí pomohl pouze s polem zapsaným přímo v místě volání (zatímco $roles je proměnná) a
bezrámcové volání z PHP 8.4 spolehlivě funguje i s holým názvem. Pojmenovaný argument strict však
zmizel, protože právě on by bezrámcové volání zablokoval. A konstanta E_USER_DEPRECATED, stojící pouze jako
řadový argument, zůstala netknutá: import by ušetřil pouhé načtení z paměti. Proč je toto chování mnohem čistší
než plošný import všeho, ukázalo měření výše.
Všechny tyto úpravy předpokládají, že jde o oficiální globální funkce PHP. Pokud konfigurace neobsahuje
nameResolution: certain, DressCode je nahlásí jako rizikové opravy a provede je pouze s vaším svolením
(podrobnosti najdete v kapitole Řekněte DressCode, co je co).
Sada přitom nehledí na to, jakou verzi PHP zrovna používáte. Funkci, kterou optimalizuje až novější verze PHP, kvalifikuje s předstihem: starší interpret zkratku pouze nevyužije (nic se nerozbije) a ve chvíli, kdy na novější PHP přejdete, bude kód okamžitě připraven. Pojmenovaný argument DressCode převede výhradně tehdy, když stojí přesně na pozici svého parametru, takže sémantika volání zůstane stoprocentně zachována.
Jak se píšou globální jména
Způsob zápisu globálních funkcí a konstant řídí konfigurační sekce qualification. Každý identifikátor lze zapsat třemi
způsoby a hodnotou každého klíče je název jednoho z nich: bare představuje holý název spoléhající na
runtime fallback PHP, imported vyžaduje import přes use function či use const a
backslashed vynucuje zápis s úvodním zpětným lomítkem.
qualification:
function:
global: bare
optimized: imported
constant:
global: bare
optimized: imported
S touto konfigurací zůstanou běžné globální funkce a konstanty holé a pouze ty, které kompilátor PHP dokáže reálně zrychlit, získají import:
namespace App;
use function count;
use function in_array;
Na první pohled by se to mohlo zdát zbytečné, vždyť count() funguje i bez importu. Důvodem je však
čistý výkon: díky importu kompilátor okamžitě ví, že volá globální funkci, a použije svou optimalizovanou instrukci
(jak rozebírá kapitola o rychlejším volání).
S hodnotou optimized: imported se hlavička souboru může snadno natáhnout do dlouhého sloupce
řádků use function. Elegantně ji zkrátí rozhodnutí imports.grouping.function: combined,
které sloučí importy funkcí do jediného řádku: use function count, in_array, strlen;. Méně řádků znamená
i méně tokenů – AI agent, který čte začátek souboru, se dříve dostane k podstatnému kódu a nemusí se prokousávat
hradbou importů. Standard nette je zapisuje přesně tímto způsobem.
Jednotlivé klíče postupují od obecných ke konkrétním:
| klíč | o jakých jménech rozhoduje |
|---|---|
function.global, constant.global |
o běžných globálních funkcích a konstantách v namespace (mimo ty, které spadají pod klíče
optimized) |
function.optimized |
o funkcích, se kterými kompilátor zachází přednostně, a to na místech, kde volání s danými argumenty skutečně zrychlí |
constant.optimized |
o konstantách, s nimiž kompilátor aktivně počítá: v podmínkách či konstantních výrazech |
function.currentNamespace, constant.currentNamespace |
o funkcích a konstantách vlastního jmenného prostoru daného souboru |
Rozhodnutí function.optimized se u části funkcí řídí konkrétními argumenty: strlen()
optimalizuje PHP vždy, ale dirname(), defined() či function_exists() pouze
s konstantním argumentem; sprintf() jen s konstantním formátem obsahujícím výhradně %s a
%d v počtu odpovídajícím předaným hodnotám; a in_array() pouze s polem zapsaným přímo
v kódu. Funkce se kvalifikuje ve chvíli, kdy ji daný namespace alespoň jednou zavolá tímto optimalizovatelným
způsobem – a od té chvíle se tak v daném namespace zapisuje každé její použití. O funkci, která není takto volána
nikde, rozhoduje obecný klíč function.global, protože import bez výkonnostního přínosu by byl v kódu
zbytečným vizuálním šumem. Klíč constant.optimized se zase neřídí názvem konstanty, ale kontextem jejího
použití. Hodnota bare u těchto klíčů znamená, že projekt dává přednost čistotě holého zápisu před
kompilátorovou optimalizací. Každé jméno spadá pod jediný klíč: pokud je function.optimized nastaveno na
keep, zůstanou optimalizované funkce beze změny, a samotné function.global: bare odstraní lomítko
z \array_map(), ale volání \strlen() a \count() nechá na pokoji.
Import zapíše DressCode přesně v tom stylu, jakým soubor importy už zapisuje: začlení jej do vícepoložkového
příkazu, do existujícího skupinového importu pro daný namespace a na přesné abecední místo, takže navazující pravidla
ze sekce imports nemají důvod cokoliv namítat. Pokud
soubor funkci už importuje pod aliasem, použije tento alias. Kde import přidat nelze (například proto, že je název
v souboru kolizní), zapíše úvodní lomítko. Holý název naopak ponechá pouze tam, kde skutečně odkazuje na globální
funkci, a nepoužívané importy automaticky odstraní.
A tím se vracíme ke jmenným prostorům. Import use function strlen; volání pevně sváže s globální
funkcí. Kdyby projekt obsahoval vlastní App\strlen(), import by změnil chování programu. Při nastavení
uncertain proto DressCode každý přechod mezi holým názvem a importem či lomítkem považuje za rizikový. S
certain naopak s jistotou ví, že App\strlen() neexistuje, a import bezpečně doplní. Funkci
uvedenou v seznamu namespaces.functions samozřejmě nikdy neimportuje.
Třídy těmito starostmi netrpí: u holého názvu třídy v namespace se PHP ke globálnímu prostoru nikdy nevrací.
Klíče class.otherNamespace a class.global tedy u tříd volí pouze mezi importem a lomítkem a
žádná jejich úprava není riziková. Název bez úvodního lomítka (např. Model\User při existujícím
use App\Model;) se ponechává, protože se vyhodnocuje relativně k importu nebo k namespace souboru. V souboru
bez jmenného prostoru je úvodní lomítko vždy zbytečné a pravidlo qualification.leadingBackslash jej
spolehlivě odstraní.
Shrnutí
- Nekvalifikovaná funkce či konstanta v namespace znamená v PHP buď prvek z daného namespace, nebo globální prvek – a z jediného souboru to nelze spolehlivě určit.
- Nastavte v konfiguraci
nameResolution: certain(a případně doplňte seznamy v klíčinamespaces). Chování všech pravidel bude rázem jednoznačné a opravy dříve závislé na odhadu přestanou být rizikové. - Části projektu s lokálními pomocnými funkcemi (např. testy) nastavte v přepisu
nameResolution: uncertain. - Bez tohoto nastavení postupuje DressCode maximálně opatrně a úpravy provádí pouze s vaším výslovným souhlasem v
fixRiskynebo s přepínačem--fix-risky. - Nové deklarace v namespaces DressCode automaticky pohlídá pod cestou
namespaces.functions; výchozí seznamy za vás sestavídresscode init. - Funkce z vlastního namespace vždy volejte přes explicitní
use function. Pokud by se soubor s funkcí náhodou nenačetl, dozvíte se to okamžitě díky chybové hlášce. - PHP před spuštěním překládá kód do instrukcí a u desítek funkcí i konstant nabízí zkratky: nahradí volání instrukcí, spočítá výsledek dopředu nebo funkci zavolá bez alokace rámce zásobníku.
- Téměř všechny tyto zkratky vyžadují jednoznačný název:
\strlen(),use function strlen;nebo soubor bez namespace. Totéž platí pro konstanty typuPHP_VERSION_ID. - Bezrámcové volání (frameless functions) z PHP 8.4 funguje spolehlivě i s nekvalifikovaným názvem v namespace.
- Pojmenované argumenty a rozbalování polí (
...$args) veškeré kompilátorové zkratky spolehlivě vypnou. - Sada
compilerOptimizationskód zformátuje tak, aby PHP mohlo zkratky naplno využít: optimalizované funkce kvalifikuje tam, kde to reálně přináší výkon, a konstanty importuje pouze na místech, kde s nimi kompilátor aktivně počítá.