Funkce a konstanty ve jmenných prostorech
Nekvalifikované strlen() uvnitř jmenného prostoru může znamenat dvě různé funkce a z kódu
se nedá poznat kterou. Nástroje na styl kódu jsou kvůli tomu u celé řady oprav odkázané na odhad. DressCode tu nejistotu
odstraní jedním řádkem konfigurace: řeknete mu, co ve vašich jmenných prostorech je, a každé pravidlo pak přesně ví,
s čím pracuje. Style checker, konečně správně.
Jedno jméno, dva významy
Podívejte se na tenhle kód:
namespace App;
if (is_null($user)) {
// ...
}
Která funkce se tu volá? Nejspíš globální is_null(), kterou zná každý. Jenže PHP to rozhodne až za
běhu: nejdřív se podívá, jestli existuje funkce App\is_null(), a teprve když ne, sáhne po globální. Stačí,
aby ji kdekoli v projektu deklaroval jiný soubor:
namespace App;
function is_null(mixed $value): bool
{
return $value === null || $value === '';
}
a volání nahoře najednou dělá něco jiného. Se souborem, ve kterém je volání zapsané, to nemá nic společného. Rozhoduje, jestli se ten druhý soubor načetl.
Úplně stejně se chovají konstanty: PHP_EOL ve jmenném prostoru App je App\PHP_EOL,
pokud existuje, jinak globální PHP_EOL. Třídy tohle nedělají. Exception ve jmenném prostoru
App je vždy App\Exception a globální třída se píše \Exception nebo se importuje.
Kus historie
Jmenné prostory přišly s PHP 5.3 a jejich podoba se rodila těžce. Verze měla vyjít na podzim 2008, první alfa byla
venku v srpnu, a o tom, jak mají jmenné prostory vlastně fungovat, se mezi vývojáři PHP rozhodovalo ještě v říjnu.
Tehdy padlo i slavné rozhodnutí vyměnit oddělovač :: za zpětné lomítko, protože :: se nedalo
spolehlivě odlišit od volání statické metody. PHP 5.3.0 nakonec vyšlo 30. června 2009, o tři čtvrtě roku
později.
Jedna z nejtěžších otázek byla právě tahle: co znamená jméno, před kterým žádný jmenný prostor není. Návrh
z 30. října 2008 došel k různému řešení pro třídy a pro funkce. Třídy na globální jméno nepadají, protože by
to spolu s autoloadingem byla výkonnostní bomba: PHP by u každého new Exception napřed zkoušelo načíst
App\Exception. Funkce a konstanty autoloading nemají, takže tohle nebezpečí nehrozilo, a protože většinu toho,
co PHP umí, poskytují právě globální funkce, dostaly pád na globální jméno. Kód psaný před jmennými prostory tak
mohl do jmenného prostoru přejít, aniž by před každé strlen() musel přidat zpětné lomítko.
Byl to rozumný kompromis. Jeho cenou je, že význam nekvalifikovaného jména funkce není vlastností souboru, ale celého běžícího programu.
Proč to pálí nástroje
Nástroj na styl kódu vidí soubory, ne běžící program. U každého is_null($x) ve jmenném prostoru proto
stojí před stejnou otázkou jako vy nahoře a odpověď nezná. A přitom celá řada užitečných oprav stojí právě na
tom, že jde o známou globální funkci:
is_null($x)na$x === null,intval($x)na(int) $x,sizeof($a)nacount($a),dirname(__FILE__)na__DIR__,get_class()naself::class,- doplnění
use function count;, aby kompilátor mohl volání optimalizovat.
Kdyby šlo o vaši funkci App\is_null(), každá z těch oprav změní, co program dělá. Poctivý nástroj je
proto musí označit jako rizikové a bez svolení je neudělat. PHP CS Fixer má mezi rizikovými pravidly třeba
is_null, no_alias_functions nebo dir_constant, všechna se stejným zdůvodněním:
riskantní, pokud je funkce přepsaná.
Nejde přitom o teoretickou kličku. Na přesně tomhle chování stojí známý trik z testů: knihovny jako php-mock
deklarují v testu App\time() nebo App\file_get_contents(), aby kód ve jmenném prostoru
App místo skutečné funkce zavolal podvrh.
Buďme pragmatičtí
Kolik funkcí a konstant ve svých jmenných prostorech opravdu máte? Skoro jistě málo, často žádnou. Funkce se totiž
neautoloadují: aby šla zavolat, musí ji někdo načíst přes require nebo záznamem files v
composer.json, a to dřív, než se použije. Proto se kód dávno přesunul do tříd a funkce ve jmenných
prostorech zůstaly okrajovou věcí. Návrh na autoloading funkcí se v PHP objevil opakovaně a součástí jazyka se dosud
nestal.
To je zároveň dobrý důvod, proč na nekvalifikované volání funkce z jmenného prostoru nespoléhat: když se soubor
s funkcí náhodou nenačte, PHP neohlásí chybu, ale potichu zavolá globální funkci stejného jména, pokud nějaká
existuje. Import use function App\is_null; funkci také nenačte, ale v takové chvíli skončí srozumitelnou
chybou Call to undefined function App\is_null().
Obraz je tedy jasný: nejistota se týká všech, ale skutečná funkce ve jmenném prostoru je vzácnost. Stačí ji vyjmenovat.
Řekněte DressCode, co je co
Nejčastější případ je, že váš projekt ve jmenných prostorech žádné funkce ani konstanty nedeklaruje. Pak stačí jediný řádek konfigurace:
nameResolution: certain
Tím DressCode ví, že každé nekvalifikované jméno funkce nebo konstanty, které soubor neimportuje ani sám nedeklaruje, je globální. Ne „nejspíš“, ale jistě.
Když nějaké funkce nebo konstanty máte, vyjmenujete je v klíči namespaces, funkce pod
functions a konstanty pod constants. Zápis je stejný jako v příkazu use, včetně
skupin ve složených závorkách. Položku se skupinou dejte do uvozovek, jinak by ji NEON četl jako mapu:
nameResolution: certain
namespaces:
functions:
- 'App\Utils\{format, parse}'
- App\Legacy\helper
constants:
- 'App\{VERSION, DEBUG}'
Funkci ze seznamu pak DressCode čte jako funkci z jmenného prostoru všude, kde se v tom jmenném prostoru volá bez importu. U funkcí nezáleží na velikosti písmen vůbec, u konstant jen v jejich vlastním jméně, v jmenném prostoru ne, přesně jako v PHP.
Seznamy a nameResolution jsou dvě různé věci, a proto mají každá svůj klíč. Seznam je fakt: tahle funkce
existuje. nameResolution je rozhodnutí: nic dalšího tam není.
nameResolution |
co znamená |
|---|---|
certain |
seznamy jsou úplné, a co v nich není, je globální |
uncertain |
seznamy úplné být nemusejí; totéž, jako když klíč chybí |
S uncertain se DressCode chová opatrně: nekvalifikované jméno bere jako globální, ale opravu, která na tom
stojí, považuje za rizikovou. Ohlásí ji
i s důvodem, třeba
The is_null() call must be written as a comparison with null, unless the namespace declares is_null(), a udělá ji
teprve tehdy, když pravidlo uvedete v fixRisky nebo spustíte fix --fix-risky. Proto je
uncertain výchozí hodnota: zapnout pravidlo ještě neznamená souhlasit s tím, že smí měnit chování kódu.
Jakmile napíšete certain, tyhle opravy rizikové být přestanou.
Každé pravidlo z toho těží
Podstatné je, kde se ta znalost uplatní. DressCode ji nepředává jednotlivým pravidlům, ale analýze
NameResolver, která pro všechna pravidla překládá jména. Ptají se jí vestavěná pravidla a ptá se jí i vaše vlastní pravidlo. Znalost tak leží na jednom místě a
žádné pravidlo si nevede vlastní seznam výjimek.
Pravidla se jí ptají na dvě věci:
- Co je co.
isGlobalFunctionCall($node, 'is_null')vrátítruepro globální funkci aApp\is_null()ze seznamu za ni nepovažuje. Scertainje ta odpověď jistá, jinak jen předpoklad. - Jak jistá je odpověď.
getUnqualifiedResolution('is_null', SymbolKind::Function, $node)řekne, kam nekvalifikované jméno míří: jistě do globálního prostoru (Global), jistě do jmenného prostoru (Namespaced), nebo do globálního jen za předpokladu, že ho jmenný prostor jinde nedeklaruje (Uncertain). Pravidlo, které se takhle zeptá, ohlásí svou opravu jako rizikovou právě u výskytu sUncertain, a v projektu scertaintedy nikdy.
A funguje to i obráceně, když pravidlo jméno funkce do kódu samo zapisuje. dresscode/no-alias-functions
přepisuje sizeof() na count() a nové jméno napíše stejně jako to původní: \sizeof()
na \count(), sizeof() na count(). Když ale soubor importuje nebo deklaruje vlastní
count, nebo když podle konfigurace deklaruje count() jeho jmenný prostor, zapíše
\count(), aby opravený kód volal totéž co původní.
Všechna pravidla tak čtou každé jméno stejně a výsledek nezávisí na tom, které z nich se na něj ptá.
Nová funkce neproklouzne
Seznam by nebyl k ničemu, kdyby zastaral. Proto ho hlídá pravidlo
dresscode/no-unlisted-namespaced-declaration, které zapne sám klíč nameResolution: certain. Ohlásí
funkci deklarovanou ve jmenném prostoru, která v seznamu namespaces.functions chybí, a totéž udělá pro
const a pro define('App\X', ...). Počítá i s deklarací uvnitř podmínky, třeba s polyfillem v
if (!function_exists(...)). Jakmile někdo do projektu přidá funkci App\Utils\slugify(),
dresscode check upozorní, že patří do konfigurace:
src/Utils/helpers.php error 12:10 Function App\Utils\slugify() must be listed in namespaces.functions no-unlisted-namespaced-declaration
Tohle stačí, protože deklaraci na rozdíl od volání poznat jde: stojí v souboru, který DressCode kontroluje. Pozor jen
na cesty vyloučené z kontroly. Funkci deklarovanou třeba v testech, které DressCode vynechává, ohlásit nemůže, a pokud
ji kód volá bez importu, zapište ji do seznamu sami. S uncertain pravidlo mlčí, protože není co hlídat, a
kdybyste ho s certain přesto nechtěli, vypnete ho v klíči rules jako kterékoli jiné.
Ani první seznam nemusíte psát ručně. dresscode init projde všechny soubory
v cestách projektu, najde funkce a konstanty deklarované ve jmenných prostorech a zapíše jejich seznamy spolu s
nameResolution: certain. Když některý soubor nejde naparsovat, co deklaruje, zůstává neznámé, a tak
init jistotu nezapíše a v komentáři ten soubor jmenuje.
Do budoucna se nabízí, aby nový záznam do konfigurace zapsal rovnou dresscode fix, místo aby na
něj pravidlo jen upozornilo.
Pomocné funkce v testech
Testy si často deklarují pomocné funkce přímo ve svém jmenném prostoru, třeba App\Tests\createUser().
Psát je do konfigurace nemá smysl a hlídat je taky ne. Pro takovou část projektu stačí přepis, který řekne, že tam seznamy
úplné nejsou:
nameResolution: certain
overrides:
- paths: [tests]
nameResolution: uncertain
Soubory v tests pak DressCode čte opatrně: opravy, které stojí na tom, že je funkce globální, se tam jen
ohlásí jako rizikové a pravidlo no-unlisted-namespaced-declaration tam mlčí. Zdrojáky v src
přitom dál těží z jistoty. Funguje to proto, že kód v src pomocné funkce testů nevolá, takže na význam
jmen v src nemají vliv.
Jedna výjimka stojí za pozornost: trik s podvržením funkce, o kterém byla řeč výš. Když test deklaruje App\time(), aby kód ve jmenném prostoru
App zavolal podvrh, týká se to volání v src, ne v testech. Taková funkce proto patří do seznamu
namespaces.functions projektu, aby ji pravidla v src nechala být.
Presety, které znají svůj framework
Někdy píšete kód přímo do jmenného prostoru frameworku, protože vás k tomu framework vede. Symfony nechává psát
konfiguraci služeb do souboru, který začíná
namespace Symfony\Component\DependencyInjection\Loader\Configurator;, právě proto, aby šlo volat
service(), param() nebo env() bez importu. Ty funkce deklaruje Symfony ve složce
vendor, kam DressCode nekouká.
Tuhle znalost proto nese dresscode/symfony-configurator, preset bez jediného pravidla, který jen vyjmenuje
funkce, jež Symfony do svého jmenného prostoru deklaruje. Standard dresscode/symfony ho skládá sám. S jiným
standardem ho přidáte do presetů a dresscode init ho navrhne, když najde nainstalovaný balíček
symfony/dependency-injection:
presets:
- per
- symfony-configurator
Vlastní preset dodá takové seznamy klíčem namespaces svého profilu (viz vlastní preset) a seznamy všech presetů se sčítají
s tím, co napíšete vy. Klíč nameResolution ale preset nastavit nemůže: framework zná, váš projekt ne.
Jak se píšou globální jména
S tím souvisejí dvě pravidla, protože jde o dvě různé otázky. Pravidlo dresscode/name-fallback
rozhoduje, jestli globální funkce nebo konstanta smí stát holá a spoléhat na pád do globálního jména. Pravidlo
dresscode/name-notation rozhoduje, jak se píše jméno, které na pád nespoléhá: importem, nebo s úvodním
lomítkem. Obě pravidla samy nerozhodují o ničem: co jim neřeknete, nechají být.
Holé, nebo kvalifikované: name-fallback
Třeba s touhle konfigurací:
rules:
name-fallback:
optimizedFunctions: qualified
doplňuje pravidlo řádky jako tenhle:
namespace App;
use function count;
use function in_array;
Na první pohled zbytečnost, count() by fungovalo i bez importu. Jde ale o rychlost. Když kompilátor PHP
narazí ve jmenném prostoru na nekvalifikované strlen($s), neví, jestli za běhu nebude existovat
App\strlen(), a tak vyrobí obecné volání, které se rozhodne až za běhu. Když ví, že jde o globální
funkci, může u zhruba tří desítek základních funkcí volání vynechat úplně a u několika dalších spočítá
výsledek už při překladu optimalizátor OPcache. Co přesně PHP dělá a jak velký je to rozdíl, popisuje stránka Optimalizace funkcí a konstant.
Klíče jdou od obecného ke zvláštnímu:
| klíč | o jakých jménech rozhoduje |
|---|---|
functions, constants |
o všech globálních funkcích a konstantách |
optimizedFunctions |
o funkcích, se kterými kompilátor nebo OPcache zachází zvlášť, tam, kde je volání s danými argumenty opravdu optimalizuje |
optimizedConstants |
o konstantách, které deklaruje PHP, tam, kde s nimi kompilátor počítá: v podmínce nebo v konstantním výrazu |
Hodnota qualified říká, že jméno na pád spoléhat nesmí, fallback, že má stát holé, a
keep, že se o jménu nerozhoduje. Klíč optimizedConstants bere jen qualified nebo
keep, protože se neřídí jménem, ale tím, jak konstanta v kódu stojí. Podobně
optimizedFunctions se u části funkcí řídí argumenty: strlen() optimalizuje PHP vždy, ale
dirname(), defined() nebo function_exists() jen s konstantním argumentem,
sprintf() jen s konstantním formátem a in_array() jen s polem zapsaným přímo v kódu. Funkce se
kvalifikuje, když ji jmenný prostor aspoň jednou takhle volá. assert() mezi optimalizovanými funkcemi není,
protože ho PHP přeloží zvlášť, ať je importovaný, nebo ne.
Jméno, které kvalifikuje, zapíše pravidlo tvarem, jaký chce name-notation, a když to o jménu nic
neříká, importem. Import zapíše tak, jak soubor importy už píše: do příkazu, který jich vyjmenovává víc, a na
místo, kam patří podle abecedy, takže pravidla o tvaru a pořadí importů pak nemají co hlásit. Když soubor funkci už
importuje pod jiným jménem, použije ten import. Kde import přidat nejde, třeba proto, že je jméno zabrané, napíše
úvodní lomítko, a jen když o import výslovně žádá name-notation a soubor na něj kvůli HTML kolem kódu
nemá místo, ohlásí ho k dopsání ručně. Holé naopak udělá jméno jen tam, kde holé jméno opravdu dosáhne na
globální funkci, a import, který už nikdo nepotřebuje, odebere.
A tady se vracíme ke jmenným prostorům. Import use function strlen; přišpendlí volání ke globální
funkci. Kdyby projekt měl vlastní App\strlen(), import by změnil chování. S uncertain proto
DressCode každý přechod mezi holým jménem a importem nebo zpětným lomítkem považuje za rizikový. S certain
ví, že App\strlen() neexistuje, a pravidlo import prostě doplní. Funkci ze seznamu přirozeně neimportuje.
Import, nebo lomítko: name-notation
Tohle pravidlo rozhoduje o jménech, která jsou jistá: o třídách, o jménech z jiných jmenných prostorů a
o globálních funkcích a konstantách, které už kvalifikované jsou. Oba tvary znamenají totéž, takže žádná jeho
oprava riziková není. Holou globální funkci ani konstantu nechá být, ta patří pravidlu name-fallback.
| klíč | o jakých jménech rozhoduje |
|---|---|
classes, functions, constants |
o všech třídách, funkcích a konstantách |
globalClasses, globalFunctions, globalConstants |
jen o těch z globálního jmenného prostoru |
Hodnotou klíče je tvar, jakým se jméno píše: import, backslash pro \strlen() a
keep, když se o jménu nerozhoduje.
U obou pravidel můžete místo prosté hodnoty dát mapu ze jména nebo ze vzoru s hvězdičkou na hodnotu:
rules:
name-notation:
classes: import # use App\Model\User; ... User
globalClasses: backslash # \Exception
globalFunctions: import # use function strlen; ... strlen()
name-fallback:
optimizedFunctions: qualified
constants:
'PHP_*': qualified # use const PHP_EOL; ... PHP_EOL
Když o jménu mluví víc klíčů, rozhoduje nejkonkrétnější odpověď: přesně napsané jméno má přednost před
vzorem, delší vzor před kratším a obojí před prostou hodnotou klíče. Při shodě vyhraje konkrétnější klíč, a tak
v ukázce dostane \Exception lomítko, přestože classes říká import. Na pořadí zápisu
nezáleží: když preset kvalifikuje 'PHP_*' a váš projekt napíše PHP_EOL: fallback, přesné
jméno vyhraje. Mapa se s hodnotou vrstvy pod sebou slévá, jako by ta hodnota byla vzor *, takže projekt nad
presetem se globalFunctions: backslash může napsat jen globalFunctions: {strlen: import} a platí
obojí. Prostá hodnota naopak vrstvu pod sebou nahradí celou i s jejími vzory. Samotné name-notation: true nebo
name-fallback: true nedělá nic a DressCode na to upozorní.
Jméno bez úvodního lomítka, třeba Model\User s use App\Model;, pravidla nechají být, protože
se čte relativně k importu nebo k jmennému prostoru souboru. Soubor bez jmenného prostoru nechají celý: úvodní lomítko
tam odebere pravidlo no-leading-backslash-in-global-namespace a jméno zůstane kvalifikované.
Shrnutí
- Nekvalifikovaná funkce nebo konstanta ve jmenném prostoru znamená podle PHP buď tu z jmenného prostoru, nebo globální, a z jednoho souboru se to poznat nedá.
- Napište do konfigurace
nameResolution: certaina případně seznamy v klíčinamespaces. Chování všech pravidel je pak jednoznačné a opravy, které stály na odhadu, přestanou být rizikové. - Část projektu, kde seznamy úplné nejsou, třeba testy s pomocnými funkcemi, dostane v přepisu
nameResolution: uncertain. - Bez toho je DressCode opatrný a takové opravy dělá jen se svolením v
fixRiskynebo s--fix-risky. - Novou deklaraci ve jmenném prostoru ohlásí
no-unlisted-namespaced-declaration; první seznamy napíšedresscode init. - Funkci z jmenného prostoru volejte přes
use function. Když se její soubor nenačte, dozvíte se to hned.