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í brát jako rizikové opravy (risky fixes) 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 jako varování
i s důvodem na řádku pod porušením, třeba the namespace may declare `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. noAliasFunctions 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 noUnlistedNamespacedDeclarations, 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` noUnlistedNamespacedDeclarations
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 noUnlistedNamespacedDeclarations 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.
Funkce, které deklaruje 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á.
Tyhle funkce proto DressCode zná sám: když má projekt nainstalovaný balíček symfony/dependency-injection,
přidá je do seznamu namespaces.functions bez jediného řádku konfigurace a s jakýmkoli standardem. Ve výpisu
dresscode config je najdete pod řádkem Names, každou se zdrojem
DressCode for symfony/dependency-injection.
Jiné balíčky dodají takové seznamy klíčem namespaces svého souboru dat, vlastní preset klíčem namespaces svého profilu
(viz vlastní preset). Všechny se sčítají s tím, co
napíšete vy. Klíč nameResolution ale balíček ani preset nastavit nemůže: framework znají, 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 nameFallback rozhoduje, jestli
globální funkce nebo konstanta smí stát holá a spoléhat na pád do globálního jména. Pravidlo nameNotation
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é: nameFallback
Třeba s touhle konfigurací:
rules:
nameFallback:
optimizedFunction: 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 |
|---|---|
function, constant |
o všech globálních funkcích a konstantách |
optimizedFunction |
o funkcích, se kterými kompilátor nebo OPcache zachází zvlášť, tam, kde je volání s danými argumenty opravdu optimalizuje |
optimizedConstant |
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íč optimizedConstant bere jen qualified nebo
keep, protože se neřídí jménem, ale tím, jak konstanta v kódu stojí. Podobně optimizedFunction
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, který nemá jiné zástupné znaky než %s a %d a má jich právě tolik,
kolik hodnot volání předává, 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 nameNotation, 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, do skupinového
importu, který pod jmenným prostorem toho jména už stojí, 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á
nameNotation 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: nameNotation
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 nameFallback.
| klíč | o jakých jménech rozhoduje |
|---|---|
class, function, constant |
o všech třídách, funkcích a konstantách |
globalClass, globalFunction, globalConstant |
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. Výjimkou jsou
optimizedFunction a optimizedConstant, které berou jen prostou hodnotu:
rules:
nameNotation:
class: import # use App\Model\User; ... User
globalClass: backslash # \Exception
globalFunction: import # use function strlen; ... strlen()
nameFallback:
optimizedFunction: qualified
constant:
'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 class ří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 globalFunction: backslash může napsat jen globalFunction: {strlen: import} a platí
obojí. Prostá hodnota naopak vrstvu pod sebou nahradí celou i s jejími vzory. Samotné nameNotation: true nebo
nameFallback: true nedělá nic a DressCode na to upozorní
(No key such as function or optimizedFunction is given, so every name stays as it is.).
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 uselessBackslashInGlobalNamespace 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í
noUnlistedNamespacedDeclarations; 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.