Nette PHPStan Rules
The nette/phpstan-rules extension makes PHPStan smarter about
Nette code. Install it and PHPStan starts inferring precise types where it previously had only generic ones. For example:
class HomePresenter extends Presenter
{
protected function createComponentMenu(): MenuControl
{
return new MenuControl;
}
public function renderDefault(): void
{
$menu = $this['menu']; // PHPStan now infers MenuControl
$menu->setActive('home'); // no "unknown method on Component" warning
}
}
Installation
Install via Composer:
composer require --dev nette/phpstan-rules
Requirements: PHP 8.1 or higher and PHPStan 2.1+.
If you use phpstan/extension-installer, the extension is
registered automatically. Otherwise add it to your phpstan.neon:
includes:
- vendor/nette/phpstan-rules/extension.neon
Most checks work without any further setup. Only the Assets section need a small configuration block in
phpstan.neon (described below). Note that all configuration shown on this page belongs in phpstan.neon,
not in your application's common.neon or other Nette DI configuration files.
Native PHP Functions
Many native PHP functions declare a return type like string|false or array|null, even though the
error value only occurs under conditions that practically cannot happen in modern code: getcwd() failing on a sane
filesystem, json_encode() failing without JSON_THROW_ON_ERROR, preg_split() failing on a
compile-time constant pattern, and so on. The extension removes the impossible parts of these return types, so PHPStan stops
asking you to handle errors that cannot occur.
The full list is in extension-php.neon.
Runtime type validation closures
A common PHP idiom for runtime checking that an array contains items of a declared type uses a typed variadic closure called with the spread operator:
/** @param string[] $items */
public function setItems(array $items): void
{
(function (string ...$items) {})(...$items);
}
PHP enforces the string type on each spread argument and throws TypeError if any item is not a
string. The closure body is empty, the expression exists only for its side effect. PHPStan would normally report
expr.resultUnused; this rule recognises the pattern and stays silent.
Assets
In phpstan.neon (not in your Nette DI config), configure the mapping of mapper IDs to mapper classes so PHPStan
can narrow the generic Asset type to a concrete asset class:
parameters:
nette:
assets:
mapping:
default: file # Nette\Assets\FilesystemMapper
images: file
vite: vite # Nette\Assets\ViteMapper
custom: App\MyMapper # any FQCN
The values file and vite are shortcuts for the built-in FilesystemMapper and
ViteMapper. Any other value is treated as a fully qualified class name of a custom mapper.
After configuration:
Registry::getMapper('vite')returnsViteMapperinstead ofMapper.Registry::getAsset('default:logo.png')returnsImageAsset.tryGetAsset()returnsImageAsset|null.FilesystemMapper::getAsset('button.js')andViteMapper::getAsset()are narrowed the same way.
Component Model
Narrows the return type of Container::getComponent() and Container::offsetGet() (i.e.
$this['name']) based on createComponent<Name>() factory methods declared on the same class.
class HomePresenter extends Presenter
{
protected function createComponentMenu(): MenuControl
{
return new MenuControl;
}
public function renderDefault(): void
{
$menu = $this->getComponent('menu'); // MenuControl
$menu = $this['menu']; // MenuControl
}
}
When no matching factory exists or the component name is not a compile-time string, the declared return type is kept.
Forms
When $form->addText('name', …), $form->addSelect(…) and similar are called in the same
function or method as the access to $form['name'] (or $form->getComponent('name')), the extension
infers the access type from the corresponding addXxx() call:
public function createComponentSignInForm(): Form
{
$form = new Form;
$form->addText('username', 'Username');
$form->addPassword('password', 'Password');
$form['username']; // TextInput
$form['password']; // TextInput (Password is a subclass)
return $form;
}
If no matching addXxx() call is found, the extension falls back to createComponent<Name>()
factory lookup, just like the Component Model extension.
Event-handler properties
Forms coerce the data to the type declared in the callback's parameter, be it stdClass, array, or a
custom DTO. So a callback whose data parameter is narrower than the declared array|object union is valid at
runtime:
$form->onSuccess[] = function (Form $form, MyDto $data): void {
// …
};
PHPStan would normally report assign.propertyType because MyDto is narrower than
array|object. The rule suppresses that error on Form::$onSuccess, $onError,
$onSubmit, $onRender, Container::$onValidate, SubmitButton::$onClick, and
$onInvalidClick.
Schema
Narrows the return type of Expect::array() from the declared Structure|Type union based on the
argument:
Expect::array(); // Type
Expect::array(['name' => Expect::string()]); // Structure (all values are Schema)
Expect::array(['name' => Expect::string(), 'x']); // Structure|Type (mixed Schema and non-Schema)
When the argument mixes Schema and non-Schema values, the declared union is kept.
Tester
PHPStan understands type narrowing after Tester\Assert calls. Supported methods: null,
notNull, true, false, truthy, falsey, same,
notSame, type.
function process(?User $user): void
{
Assert::notNull($user);
$user->getName(); // no "called on null" warning
}
Arrow functions as void callbacks
Tester's test() and Assert::exception() accept callbacks typed as Closure(): void, but
it is common to pass arrow functions like fn () => throw new MyException. An arrow function always has a return
value, which PHPStan would normally flag as a type mismatch. The rule suppresses that error for the following functions and
methods: test, testException, testNoError, Tester\Assert::exception,
Tester\Assert::throws, Tester\Assert::error, Tester\Assert::noError.
Utils
Strings::match(), matchAll(), split(): return types are inferred from the boolean
flags (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>>
When a flag is not a compile-time constant, the declared return type is kept.
Arrays::invoke() and Arrays::invokeMethod() return an array of the callable / method
return type instead of the declared array.
Helpers::falseToNull() narrows the return type by removing false and adding null.
Thus string|false becomes string|null.
Html magic methods: $el->setClass(…), $el->addData(…),
$el->getHref() and similar are resolved without @method annotations. setXxx() and
addXxx() return static (fluent API), getXxx() returns mixed.