Nette PHPStan Rules
Rozšíření nette/phpstan-rules naučí PHPStan lépe rozumět
Nette kódu. Stačí ho nainstalovat a PHPStan začne odvozovat přesné typy tam, kde dříve znal jen obecné. Například:
class HomePresenter extends Presenter
{
protected function createComponentMenu(): MenuControl
{
return new MenuControl;
}
public function renderDefault(): void
{
$menu = $this['menu']; // PHPStan nyní odvodí MenuControl
$menu->setActive('home'); // bez varování o "neznámé metodě na Component"
}
}
Instalace
Nainstalujte přes Composer:
composer require --dev nette/phpstan-rules
Potřebujete PHP 8.1 nebo vyšší a PHPStan 2.1+.
Pokud používáte phpstan/extension-installer, rozšíření se
zaregistruje automaticky. Jinak ho přidejte do phpstan.neon:
includes:
- vendor/nette/phpstan-rules/extension.neon
Většina kontrol funguje bez další konfigurace. Pouze pro funkce popsané v sekci Assety je potřeba malý
konfigurační blok v phpstan.neon (viz níže). Veškerá konfigurace uvedená na této stránce patří do
phpstan.neon, nikoli do common.neon nebo jiných konfiguračních souborů Nette DI.
Nativní PHP funkce
Mnoho nativních PHP funkcí má v deklarovaném návratovém typu string|false nebo array|null,
ačkoli se chybová hodnota objevuje jen za podmínek, které v moderním kódu prakticky nemohou nastat: selhání
getcwd() na funkčním filesystému, selhání json_encode() bez JSON_THROW_ON_ERROR,
selhání preg_split() na konstantním patternu a podobně. Rozšíření tyto chybové hodnoty z návratových
typů odstraní, takže PHPStan přestane vyžadovat ošetření chyb, které nemohou nastat.
Kompletní seznam je v extension-php.neon.
Closures pro runtime kontrolu typů
Běžný PHP idiom pro runtime ověření, že pole obsahuje hodnoty deklarovaného typu, používá typovanou variadickou closure volanou s operátorem spread:
/** @param string[] $items */
public function setItems(array $items): void
{
(function (string ...$items) {})(...$items);
}
PHP vynutí typ string na každém argumentu a vyhodí TypeError, pokud některý prvek není string.
Tělo closure je prázdné a výraz existuje pouze kvůli vedlejšímu efektu. PHPStan by jinak hlásil
expr.resultUnused, toto pravidlo ale daný vzor rozpozná a chybu nevypíše.
Assety
V phpstan.neon (nikoli v konfiguraci Nette DI) nastavte mapování ID mapperů na třídy, aby PHPStan dokázal
zúžit obecný typ Asset na konkrétní třídu:
parameters:
nette:
assets:
mapping:
default: file # Nette\Assets\FilesystemMapper
images: file
vite: vite # Nette\Assets\ViteMapper
custom: App\MyMapper # libovolné FQCN
Hodnoty file a vite jsou zkratky pro vestavěné FilesystemMapper a
ViteMapper. Jakákoli jiná hodnota se považuje za plně kvalifikovaný název vlastní třídy mapperu.
Po nastavení:
Registry::getMapper('vite')vracíViteMappermístoMapper.Registry::getAsset('default:logo.png')vracíImageAsset.tryGetAsset()vracíImageAsset|null.FilesystemMapper::getAsset('button.js')aViteMapper::getAsset()se zúžují stejným způsobem.
Component Model
Rozšíření zúží návratový typ Container::getComponent() a Container::offsetGet() (tedy
$this['name']) podle factory metod createComponent<Name>() deklarovaných na téže třídě.
class HomePresenter extends Presenter
{
protected function createComponentMenu(): MenuControl
{
return new MenuControl;
}
public function renderDefault(): void
{
$menu = $this->getComponent('menu'); // MenuControl
$menu = $this['menu']; // MenuControl
}
}
Pokud odpovídající factory neexistuje nebo název komponenty není konstantní string, ponechá se deklarovaný návratový typ.
Formuláře
Pokud je volání $form->addText('name', …), $form->addSelect(…) apod. ve stejné funkci
nebo metodě jako přístup k $form['name'] (případně $form->getComponent('name')), rozšíření
odvodí typ přístupu z odpovídajícího volání addXxx():
public function createComponentSignInForm(): Form
{
$form = new Form;
$form->addText('username', 'Username');
$form->addPassword('password', 'Password');
$form['username']; // TextInput
$form['password']; // TextInput (Password je potomek)
return $form;
}
Pokud žádné odpovídající volání addXxx() neexistuje, rozšíření se stejně jako Component Model pokusí
najít factory createComponent<Name>().
Vlastnosti event handlerů
Formuláře data převedou na typ deklarovaný v parametru callbacku, ať jde o stdClass, array nebo
vlastní DTO. Callback s užším datovým parametrem, než je deklarovaný union array|object, je proto
v pořádku:
$form->onSuccess[] = function (Form $form, MyDto $data): void {
// …
};
PHPStan by jinak hlásil assign.propertyType, protože MyDto je užší než
array|object. Pravidlo tuto chybu potlačuje u vlastností Form::$onSuccess, $onError,
$onSubmit, $onRender, Container::$onValidate, SubmitButton::$onClick a
$onInvalidClick.
Schema
Rozšíření zúží návratový typ Expect::array() z deklarovaného unionu Structure|Type podle
předaného argumentu:
Expect::array(); // Type
Expect::array(['name' => Expect::string()]); // Structure (všechny hodnoty jsou Schema)
Expect::array(['name' => Expect::string(), 'x']); // Structure|Type (Schema i ne-Schema hodnoty)
Pokud argument obsahuje Schema i ne-Schema hodnoty, deklarovaný union zůstane zachován.
Tester
PHPStan po voláních metod Tester\Assert zúží typ proměnné. Podporované metody: null,
notNull, true, false, truthy, falsey, same,
notSame, type.
function process(?User $user): void
{
Assert::notNull($user);
$user->getName(); // bez varování o volání na null
}
Arrow funkce jako void callbacky
Funkce Testeru test() a Assert::exception() přijímají callbacky typované jako
Closure(): void, ale často se jim předávají arrow funkce ve stylu fn () => throw new MyException.
Arrow funkce vždy vrací nějakou hodnotu, což by PHPStan jinak označil za typovou neshodu. Pravidlo tuto chybu potlačuje
u následujících funkcí a metod: test, testException, testNoError,
Tester\Assert::exception, Tester\Assert::throws, Tester\Assert::error,
Tester\Assert::noError.
Utils
Strings::match(), matchAll(), split(): návratové typy se odvodí
z booleovských flagů (captureOffset, unmatchedAsNull, patternOrder,
lazy):
Strings::match($s, '#(\w+)#'); // array<string>|null
Strings::match($s, '#(\w+)#', captureOffset: true); // array<array{string, int<0, max>}>|null
Strings::match($s, '#(\w+)#', unmatchedAsNull: true); // array<string|null>|null
Strings::matchAll($s, '#(\w+)#', lazy: true); // Generator<int, array<string>>
Pokud flag není konstantní hodnota, zachová se deklarovaný návratový typ.
Arrays::invoke() a Arrays::invokeMethod() vracejí místo deklarovaného
array pole s typem návratové hodnoty volaného callable, resp. metody.
Helpers::falseToNull() zúží návratový typ tak, že odstraní false a přidá
null. Z string|false se tedy stane string|null.
Html magické metody: $el->setClass(…), $el->addData(…),
$el->getHref() a podobné se rozpoznají i bez @method anotací. setXxx() a
addXxx() vrací static (fluent API), getXxx() vrací mixed.