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) na count($a),
  • dirname(__FILE__) na __DIR__,
  • get_class() na self::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í true pro globální funkci a App\is_null() ze seznamu za ni nepovažuje. S certain je 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 s Uncertain, a v projektu s certain tedy 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: certain a případně seznamy v klíči namespaces. 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 fixRisky nebo s --fix-risky.
  • Novou deklaraci ve jmenném prostoru ohlásí no-unlisted-namespaced-declaration; první seznamy napíše dresscode init.
  • Funkci z jmenného prostoru volejte přes use function. Když se její soubor nenačte, dozvíte se to hned.