Skip to content

Commit f8db53a

Browse files
committed
feat: add Yii::$app->params type inference from configuration for precise array shape typing.
1 parent 8caeb88 commit f8db53a

13 files changed

Lines changed: 461 additions & 8 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- fix: update Rector command in `composer.json` to remove unnecessary 'src' argument.
1111
- feat: replace static stub files with dynamic stub generation for `Yii::$app` type inference, adding support for custom application types.
1212
- chore: remove `sync-metadata` script and `docs/development.md`, update documentation links.
13+
- feat: add `Yii::$app->params` type inference from configuration for precise array shape typing.
1314

1415
## 0.4.0 January 26, 2026
1516

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ Create a PHPStan-specific config file (`config/phpstan-config.php`).
7070
declare(strict_types=1);
7171

7272
return [
73+
// Application params: enables precise type inference for Yii::$app->params
74+
'params' => [
75+
'turnstile.siteKey' => '',
76+
'adminEmail' => 'admin@example.com',
77+
'maxItems' => 100,
78+
],
7379
// PHPStan only: used by this extension for behavior property/method type inference
7480
'behaviors' => [
7581
app\models\User::class => [
@@ -136,6 +142,25 @@ if (Yii::$app->user->isGuest === false) {
136142
}
137143
```
138144

145+
#### Application params
146+
147+
```php
148+
// Types are inferred from the values in your phpstan-config.php 'params' key
149+
150+
// ✅ Typed as array{turnstile.siteKey: string, adminEmail: string, maxItems: int}
151+
$params = Yii::$app->params;
152+
153+
// ✅ Typed as string
154+
$email = Yii::$app->params['adminEmail'];
155+
156+
// ✅ Typed as int
157+
$maxItems = Yii::$app->params['maxItems'];
158+
159+
// ✅ Nested arrays are also supported
160+
// 'nested' => ['db' => ['host' => 'localhost', 'port' => 3306]]
161+
$host = Yii::$app->params['nested']['db']['host']; // string
162+
```
163+
139164
#### Behaviors
140165

141166
```php

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"ecs": "./vendor/bin/ecs --fix",
5353
"rector": "./vendor/bin/rector process",
5454
"static": "./vendor/bin/phpstan --memory-limit=-1",
55-
"tests": "./vendor/bin/phpunit"
55+
"tests": "php -d memory_limit=-1 ./vendor/bin/phpunit"
5656
},
5757
"repositories": [
5858
{

src/ServiceMap.php

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
* container?: array{
5555
* definitions?: array<array-key, DefinitionType>,
5656
* singletons?: array<array-key, DefinitionType>,
57-
* }
57+
* },
58+
* params?: array<string, mixed>,
5859
* }
5960
*
6061
* @copyright Copyright (C) 2023 Terabytesoftw.
@@ -97,6 +98,13 @@ final class ServiceMap
9798
*/
9899
private array $componentsDefinitions = [];
99100

101+
/**
102+
* Application params for PHPStan type inference.
103+
*
104+
* @phpstan-var array<string, mixed>
105+
*/
106+
private array $params = [];
107+
100108
/**
101109
* Service definitions map for Yii Application analysis.
102110
*
@@ -133,6 +141,7 @@ public function __construct(string $configPath = '')
133141
$this->processBehaviors($config);
134142
$this->processComponents($config);
135143
$this->processDefinition($config);
144+
$this->processParams($config);
136145
$this->processSingletons($config);
137146
}
138147

@@ -237,6 +246,19 @@ public function getComponentDefinitionById(string $id): array
237246
return is_array($definition) ? $definition : [];
238247
}
239248

249+
/**
250+
* Retrieves the application params map for PHPStan type inference.
251+
*
252+
* Returns the `params` key-value pairs extracted from the Yii Application configuration file, enabling static
253+
* analysis tools to infer precise array shape types for `Yii::$app->params` access.
254+
*
255+
* @return array<string, mixed> Params key-value pairs from configuration.
256+
*/
257+
public function getParams(): array
258+
{
259+
return $this->params;
260+
}
261+
240262
/**
241263
* Retrieves the fully qualified class name of a Yii Service by its identifier.
242264
*
@@ -306,6 +328,10 @@ private function loadConfig(string $configPath): array
306328
$this->throwErrorWhenConfigFileIsNotArray($configPath, 'components');
307329
}
308330

331+
if (isset($config['params']) && is_array($config['params']) === false) {
332+
$this->throwErrorWhenConfigFileIsNotArray($configPath, 'params');
333+
}
334+
309335
if (isset($config['container'])) {
310336
if (is_array($config['container']) === false) {
311337
$this->throwErrorWhenConfigFileIsNotArray($configPath, 'container');
@@ -517,6 +543,23 @@ private function processDefinition(array $config): void
517543
}
518544
}
519545

546+
/**
547+
* Processes application params from the Yii Application configuration array.
548+
*
549+
* Extracts the `params` section and stores it for type inference of `Yii::$app->params` array access.
550+
*
551+
* @param array $config Yii Application configuration array containing params definitions.
552+
*
553+
* @phpstan-import-type ServiceType from ServiceMap
554+
* @phpstan-param ServiceType $config
555+
*/
556+
private function processParams(array $config): void
557+
{
558+
if ($config !== []) {
559+
$this->params = $config['params'] ?? [];
560+
}
561+
}
562+
520563
/**
521564
* Processes singleton service definitions from the Yii Application configuration array.
522565
*
@@ -584,7 +627,9 @@ private function throwErrorWhenConfigFileIsNotArray(string ...$args): never
584627
*/
585628
private function throwErrorWhenIsNotString(string ...$args): never
586629
{
587-
throw new RuntimeException(sprintf("'%s': '%s' must be a 'string', got '%s'.", ...$args));
630+
throw new RuntimeException(
631+
sprintf("'%s': '%s' must be a 'string', got '%s'.", ...$args),
632+
);
588633
}
589634

590635
/**
@@ -602,6 +647,8 @@ private function throwErrorWhenIsNotString(string ...$args): never
602647
*/
603648
private function throwErrorWhenUnsupportedDefinition(string $id): never
604649
{
605-
throw new RuntimeException(sprintf("Unsupported definition for '%s'.", $id));
650+
throw new RuntimeException(
651+
sprintf("Unsupported definition for '%s'.", $id),
652+
);
606653
}
607654
}

src/StubFilesExtension.php

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@
77
use RuntimeException;
88
use yii\base\Application;
99

10+
use function array_keys;
1011
use function file_exists;
1112
use function file_put_contents;
1213
use function getmypid;
14+
use function implode;
15+
use function is_array;
16+
use function is_bool;
17+
use function is_float;
18+
use function is_int;
19+
use function is_string;
1320
use function ltrim;
1421
use function md5;
22+
use function preg_match;
1523
use function rename;
1624
use function sprintf;
1725
use function strrpos;
@@ -76,8 +84,14 @@ public function getFiles(): array
7684
*/
7785
private function buildApplicationTypeDeclaration(string $applicationType): string
7886
{
87+
$paramsProperty = $this->buildParamsPropertyDeclaration();
88+
7989
$baseDeclaration = <<<PHP
8090
namespace yii\base {
91+
class Module
92+
{{$paramsProperty}
93+
}
94+
8195
abstract class Application {}
8296
}
8397
PHP;
@@ -107,6 +121,33 @@ class {$className} extends \yii\base\Application {}
107121
PHP;
108122
}
109123

124+
/**
125+
* Builds the `$params` property declaration for the stub file.
126+
*
127+
* Generates a PHPDoc `@var` annotation with an array shape type inferred from the configured application params.
128+
* Returns an empty string if no params are configured, allowing the native `array` type to remain unchanged.
129+
*
130+
* @return string PHP property declaration block with array shape annotation, or empty string if no params.
131+
*/
132+
private function buildParamsPropertyDeclaration(): string
133+
{
134+
$params = $this->serviceMap->getParams();
135+
136+
if ($params === []) {
137+
return '';
138+
}
139+
140+
$typeString = $this->inferTypeString($params);
141+
142+
return <<<PHP
143+
144+
/**
145+
* @var {$typeString}
146+
*/
147+
public \$params;
148+
PHP;
149+
}
150+
110151
/**
111152
* Builds the full stub content for the specified application type.
112153
*
@@ -178,9 +219,11 @@ private function generateStub(string $applicationType): string
178219
);
179220
}
180221

181-
// @codeCoverageIgnoreStart
182-
// atomic publish: rename within the same filesystem is atomic on POSIX. This fallback handles Windows (where
183-
// rename fails if target exists) and other non-POSIX edge cases during concurrent PHPStan runs.
222+
/**
223+
* @codeCoverageIgnoreStart
224+
* atomic publish: rename within the same filesystem is atomic on POSIX. This fallback handles Windows (where
225+
* rename fails if target exists) and other non-POSIX edge cases during concurrent PHPStan runs.
226+
*/
184227
if (!@rename($temporaryPath, $stubPath)) {
185228
@unlink($temporaryPath);
186229

@@ -192,8 +235,81 @@ private function generateStub(string $applicationType): string
192235
sprintf("Failed to write stub file to '%s'. Ensure the temporary directory is writable.", $stubPath),
193236
);
194237
}
195-
// @codeCoverageIgnoreEnd
238+
/** @codeCoverageIgnoreEnd */
196239

197240
return $stubPath;
198241
}
242+
243+
/**
244+
* Infers a PHPStan type string from a PHP value.
245+
*
246+
* Converts PHP scalar values and arrays to their corresponding PHPStan type annotation strings. Supports `string`,
247+
* `int`, `float`, `bool`, `null`, associative arrays (array shapes), and list arrays.
248+
*
249+
* @param mixed $value PHP value to infer a type string from.
250+
*
251+
* @return string PHPStan type annotation string.
252+
*/
253+
private function inferTypeString(mixed $value): string
254+
{
255+
if ($value === null) {
256+
return 'null';
257+
}
258+
259+
if (is_string($value)) {
260+
return 'string';
261+
}
262+
263+
if (is_int($value)) {
264+
return 'int';
265+
}
266+
267+
if (is_float($value)) {
268+
return 'float';
269+
}
270+
271+
if (is_bool($value)) {
272+
return 'bool';
273+
}
274+
275+
if (is_array($value)) {
276+
if ($value === []) {
277+
return 'array<mixed, mixed>';
278+
}
279+
280+
$allStringKeys = true;
281+
282+
foreach (array_keys($value) as $k) {
283+
if (is_string($k) === false) {
284+
$allStringKeys = false;
285+
286+
break;
287+
}
288+
}
289+
290+
if ($allStringKeys) {
291+
$entries = [];
292+
293+
foreach ($value as $k => $v) {
294+
/** @phpstan-var string $k */
295+
$formattedKey = preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $k) === 1
296+
? $k
297+
: "'$k'";
298+
$entries[] = $formattedKey . ': ' . $this->inferTypeString($v);
299+
}
300+
301+
return 'array{' . implode(', ', $entries) . '}';
302+
}
303+
304+
$entries = [];
305+
306+
foreach ($value as $v) {
307+
$entries[] = $this->inferTypeString($v);
308+
}
309+
310+
return 'array{' . implode(', ', $entries) . '}';
311+
}
312+
313+
return 'mixed';
314+
}
199315
}

0 commit comments

Comments
 (0)