|
1 | | -Nette PHPStan Rules |
2 | | -******************* |
3 | | - |
4 | | -.[perex] |
5 | | -Rozšíření `nette/phpstan-rules` naučí [PHPStan |https://phpstan.org] 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: |
6 | | - |
7 | | -```php |
8 | | -class HomePresenter extends Presenter |
9 | | -{ |
10 | | - protected function createComponentMenu(): MenuControl |
11 | | - { |
12 | | - return new MenuControl; |
13 | | - } |
14 | | - |
15 | | - public function renderDefault(): void |
16 | | - { |
17 | | - $menu = $this['menu']; // PHPStan nyní odvodí MenuControl |
18 | | - $menu->setActive('home'); // bez varování o "neznámé metodě na Component" |
19 | | - } |
20 | | -} |
21 | | -``` |
22 | | - |
23 | | - |
24 | | -Instalace |
25 | | -========= |
26 | | - |
27 | | -Nainstalujte přes Composer: |
28 | | - |
29 | | -```shell |
30 | | -composer require --dev nette/phpstan-rules |
31 | | -``` |
32 | | - |
33 | | -Potřebujete PHP 8.1 nebo vyšší a PHPStan 2.1+. |
34 | | - |
35 | | -Pokud používáte [phpstan/extension-installer |https://github.com/phpstan/extension-installer], rozšíření se zaregistruje automaticky. Jinak ho přidejte do `phpstan.neon`: |
36 | | - |
37 | | -```neon |
38 | | -includes: |
39 | | - - vendor/nette/phpstan-rules/extension.neon |
40 | | -``` |
41 | | - |
42 | | -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. |
43 | | - |
44 | | - |
45 | | -Nativní PHP funkce |
46 | | -================== |
47 | | - |
48 | | -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. |
49 | | - |
50 | | -Kompletní seznam je v [extension-php.neon |https://github.com/nette/phpstan-rules/blob/master/extension-php.neon]. |
51 | | - |
52 | | - |
53 | | -Closures pro runtime kontrolu typů |
54 | | ----------------------------------- |
55 | | - |
56 | | -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: |
57 | | - |
58 | | -```php |
59 | | -/** @param string[] $items */ |
60 | | -public function setItems(array $items): void |
61 | | -{ |
62 | | - (function (string ...$items) {})(...$items); |
63 | | -} |
64 | | -``` |
65 | | - |
66 | | -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. |
67 | | - |
68 | | - |
69 | | -Assety |
70 | | -====== |
71 | | - |
72 | | -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: |
73 | | - |
74 | | -```neon |
75 | | -parameters: |
76 | | - nette: |
77 | | - assets: |
78 | | - mapping: |
79 | | - default: file # Nette\Assets\FilesystemMapper |
80 | | - images: file |
81 | | - vite: vite # Nette\Assets\ViteMapper |
82 | | - custom: App\MyMapper # libovolné FQCN |
83 | | -``` |
84 | | - |
85 | | -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. |
86 | | - |
87 | | -Po nastavení: |
88 | | - |
89 | | -- `Registry::getMapper('vite')` vrací `ViteMapper` místo `Mapper`. |
90 | | -- `Registry::getAsset('default:logo.png')` vrací `ImageAsset`. `tryGetAsset()` vrací `ImageAsset|null`. |
91 | | -- `FilesystemMapper::getAsset('button.js')` a `ViteMapper::getAsset()` se zúžují stejným způsobem. |
92 | | - |
93 | | - |
94 | | -Component Model |
95 | | -=============== |
96 | | - |
97 | | -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ě. |
98 | | - |
99 | | -```php |
100 | | -class HomePresenter extends Presenter |
101 | | -{ |
102 | | - protected function createComponentMenu(): MenuControl |
103 | | - { |
104 | | - return new MenuControl; |
105 | | - } |
106 | | - |
107 | | - public function renderDefault(): void |
108 | | - { |
109 | | - $menu = $this->getComponent('menu'); // MenuControl |
110 | | - $menu = $this['menu']; // MenuControl |
111 | | - } |
112 | | -} |
113 | | -``` |
114 | | - |
115 | | -Pokud odpovídající factory neexistuje nebo název komponenty není konstantní string, ponechá se deklarovaný návratový typ. |
116 | | - |
117 | | - |
118 | | -Formuláře |
119 | | -========= |
120 | | - |
121 | | -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()`: |
122 | | - |
123 | | -```php |
124 | | -public function createComponentSignInForm(): Form |
125 | | -{ |
126 | | - $form = new Form; |
127 | | - $form->addText('username', 'Username'); |
128 | | - $form->addPassword('password', 'Password'); |
129 | | - |
130 | | - $form['username']; // TextInput |
131 | | - $form['password']; // TextInput (Password je potomek) |
132 | | - return $form; |
133 | | -} |
134 | | -``` |
135 | | - |
136 | | -Pokud žádné odpovídající volání `addXxx()` neexistuje, rozšíření se stejně jako Component Model pokusí najít factory `createComponent<Name>()`. |
137 | | - |
138 | | - |
139 | | -Vlastnosti event handlerů |
140 | | -------------------------- |
141 | | - |
142 | | -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: |
143 | | - |
144 | | -```php |
145 | | -$form->onSuccess[] = function (Form $form, MyDto $data): void { |
146 | | - // … |
147 | | -}; |
148 | | -``` |
149 | | - |
150 | | -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`. |
151 | | - |
152 | | - |
153 | | -Schema |
154 | | -====== |
155 | | - |
156 | | -Rozšíření zúží návratový typ `Expect::array()` z deklarovaného unionu `Structure|Type` podle předaného argumentu: |
157 | | - |
158 | | -```php |
159 | | -Expect::array(); // Type |
160 | | -Expect::array(['name' => Expect::string()]); // Structure (všechny hodnoty jsou Schema) |
161 | | -Expect::array(['name' => Expect::string(), 'x']); // Structure|Type (Schema i ne-Schema hodnoty) |
162 | | -``` |
163 | | - |
164 | | -Pokud argument obsahuje Schema i ne-Schema hodnoty, deklarovaný union zůstane zachován. |
165 | | - |
166 | | - |
167 | | -Tester |
168 | | -====== |
169 | | - |
170 | | -PHPStan po voláních metod `Tester\Assert` zúží typ proměnné. Podporované metody: `null`, `notNull`, `true`, `false`, `truthy`, `falsey`, `same`, `notSame`, `type`. |
171 | | - |
172 | | -```php |
173 | | -function process(?User $user): void |
174 | | -{ |
175 | | - Assert::notNull($user); |
176 | | - $user->getName(); // bez varování o volání na null |
177 | | -} |
178 | | -``` |
179 | | - |
180 | | -Arrow funkce jako void callbacky |
181 | | --------------------------------- |
182 | | - |
183 | | -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`. |
184 | | - |
185 | | - |
186 | | -Utils |
187 | | -===== |
188 | | - |
189 | | -**`Strings::match()`, `matchAll()`, `split()`**: návratové typy se odvodí z booleovských flagů (`captureOffset`, `unmatchedAsNull`, `patternOrder`, `lazy`): |
190 | | - |
191 | | -```php |
192 | | -Strings::match($s, '#(\w+)#'); // array<string>|null |
193 | | -Strings::match($s, '#(\w+)#', captureOffset: true); // array<array{string, int<0, max>}>|null |
194 | | -Strings::match($s, '#(\w+)#', unmatchedAsNull: true); // array<string|null>|null |
195 | | -Strings::matchAll($s, '#(\w+)#', lazy: true); // Generator<int, array<string>> |
196 | | -``` |
197 | | - |
198 | | -Pokud flag není konstantní hodnota, zachová se deklarovaný návratový typ. |
199 | | - |
200 | | -**`Arrays::invoke()`** a **`Arrays::invokeMethod()`** vracejí místo deklarovaného `array` pole s typem návratové hodnoty volaného callable, resp. metody. |
201 | | - |
202 | | -**`Helpers::falseToNull()`** zúží návratový typ tak, že odstraní `false` a přidá `null`. Z `string|false` se tedy stane `string|null`. |
203 | | - |
204 | | -**`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`. |
| 1 | +{{redirect: tools:cs:phpstan-rules}} |
0 commit comments