Skip to content

Commit 5b1fb99

Browse files
committed
Replace static stub files with dynamic stub generation for Yii::$app type inference, adding support for custom application types.
1 parent a2fe54e commit 5b1fb99

11 files changed

Lines changed: 350 additions & 120 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.4.1 Under development
44

55
- Bug #81: Update Rector command in `composer.json` to remove unnecessary 'src' argument (@terabytesoftw)
6+
- Enh: Replace static stub files with dynamic stub generation for `Yii::$app` type inference, adding support for custom application types (@terabytesoftw)
67

78
## 0.4.0 January 26, 2026
89

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ parameters:
1010
excludePaths:
1111
analyse:
1212
- tests/console/
13+
- tests/custom/
1314

1415
ignoreErrors:
1516
- '#Calling PHPStan\\Reflection\\Annotations\\AnnotationsPropertiesClassReflectionExtension\:\:(has|get)Property\(\) is not covered.+#'

src/StubFilesExtension.php

Lines changed: 110 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@
44

55
namespace yii2\extensions\phpstan;
66

7+
use RuntimeException;
78
use yii\base\Application;
89

10+
use function file_exists;
11+
use function file_put_contents;
12+
use function ltrim;
13+
use function md5;
14+
use function sprintf;
15+
use function strrpos;
16+
use function substr;
17+
use function sys_get_temp_dir;
18+
919
/**
10-
* Provides stub file resolution for PHPStan analysis based on a Yii Application type.
11-
*
12-
* Determines the appropriate stub file to use for static analysis by inspecting the application type from the provided
13-
* {@see ServiceMap} instance.
14-
*
15-
* This enables PHPStan to reflect the correct set of Yii Application properties and methods for web, console, or base
16-
* application contexts.
17-
*
18-
* The class supports mapping of application types to their corresponding stub files and falls back to a default stub if
19-
* the application type is not explicitly mapped.
20+
* Provides dynamic stub file generation for PHPStan analysis based on the configured Yii Application type.
2021
*
21-
* Stub files are resolved from the `stub/` directory relative to the extension source.
22+
* Generates a stub file at runtime that overrides the `Yii::$app` property type annotation to match the application
23+
* type specified in the project configuration. This enables PHPStan to infer the correct application class for web,
24+
* console, or custom application contexts without requiring separate static stub files.
2225
*
23-
* Key features:
24-
* - Maps Yii Application types to specific stub files for accurate static analysis.
25-
* - Provides a fallback stub for unknown or custom application types.
26-
* - Resolves stub file paths relative to the extension directory.
27-
* - Supports web, console, and base application contexts.
26+
* The generated stub is written to a deterministic temporary file path, cached across PHPStan runs for the same
27+
* application type.
2828
*
2929
* @see ServiceMap for service and component map for Yii Application static analysis.
3030
*
@@ -33,55 +33,123 @@
3333
*/
3434
final class StubFilesExtension implements \PHPStan\PhpDoc\StubFilesExtension
3535
{
36-
private const APPLICATION_TYPE_STUBS = [
37-
Application::class => 'BaseYii.stub',
38-
\yii\console\Application::class => 'BaseYiiConsole.stub',
39-
\yii\web\Application::class => 'BaseYiiWeb.stub',
40-
];
41-
42-
private const DEFAULT_STUB = 'BaseYiiWeb.stub';
43-
4436
/**
4537
* @param ServiceMap $serviceMap Service and component map for Yii Application static analysis.
4638
*/
4739
public function __construct(private readonly ServiceMap $serviceMap) {}
4840

4941
/**
50-
* Retrieves the appropriate stub file path for PHPStan analysis based on the Yii Application type.
42+
* Retrieves the dynamically generated stub file path for PHPStan analysis.
5143
*
52-
* Determines the stub file to use for static analysis by inspecting the application type provided by the
53-
* {@see ServiceMap} instance.
44+
* Generates a stub file with the correct `@var` type annotation for `Yii::$app` based on the configured application
45+
* type from the {@see ServiceMap} instance.
5446
*
55-
* This enables PHPStan to reflect the correct set of Yii Application properties and methods for web, console, or
56-
* base application contexts.
47+
* @return array Array containing the absolute path to the generated stub file for PHPStan analysis.
5748
*
58-
* @return string[] Array containing the absolute path to the resolved stub file for PHPStan analysis.
49+
* @phpstan-return string[]
5950
*/
6051
public function getFiles(): array
6152
{
62-
$stubsDirectory = $this->getStubsDirectory();
63-
$applicationType = $this->serviceMap->getApplicationType();
53+
return [$this->generateStub($this->serviceMap->getApplicationType())];
54+
}
55+
56+
/**
57+
* Builds the application type class declaration block for the stub.
58+
*
59+
* Generates the necessary namespace and class declarations to satisfy PHPStan stub type resolution for the
60+
* configured application type. Includes the base `\yii\base\Application` declaration and, if the configured type
61+
* differs, an additional declaration for the specific application class.
62+
*
63+
* @param string $applicationType Fully qualified class name of the application type (without leading backslash).
64+
*
65+
* @return string PHP namespace block declarations for the stub file.
66+
*/
67+
private function buildApplicationTypeDeclaration(string $applicationType): string
68+
{
69+
$baseDeclaration = <<<PHP
70+
namespace yii\base {
71+
abstract class Application {}
72+
}
73+
PHP;
6474

65-
$stubFileName = self::APPLICATION_TYPE_STUBS[$applicationType] ?? self::DEFAULT_STUB;
66-
$stubFilePath = $stubsDirectory . DIRECTORY_SEPARATOR . $stubFileName;
75+
if ($applicationType === Application::class) {
76+
return $baseDeclaration;
77+
}
6778

68-
return [$stubFilePath];
79+
$lastSeparator = strrpos($applicationType, '\\');
80+
81+
if ($lastSeparator === false) {
82+
$namespace = '';
83+
$className = $applicationType;
84+
} else {
85+
$namespace = substr($applicationType, 0, $lastSeparator);
86+
$className = substr($applicationType, $lastSeparator + 1);
87+
}
88+
89+
$namespaceBlock = $namespace !== '' ? "namespace {$namespace}" : 'namespace';
90+
91+
return <<<PHP
92+
{$baseDeclaration}
93+
94+
{$namespaceBlock} {
95+
class {$className} extends \yii\base\Application {}
96+
}
97+
PHP;
6998
}
7099

71100
/**
72-
* Retrieves the absolute path to the stub files directory for PHPStan analysis.
101+
* Generates a stub file for the specified application type.
73102
*
74-
* Resolves the path to the `stub/` directory relative to the extension source directory, ensuring that stub files
75-
* are correctly located regardless of the current working directory or environment.
103+
* Creates a PHP stub that overrides the `BaseYii::$app` property type annotation to match the configured
104+
* application type. Includes necessary class declarations for PHPStan stub type resolution. The stub is written to
105+
* a deterministic temporary file path based on the application type hash, providing natural caching across PHPStan
106+
* runs.
76107
*
77-
* This method is used internally to construct absolute paths for stub file resolution in static analysis.
108+
* @param string $applicationType Fully qualified class name of the application type.
78109
*
79-
* @return string Absolute path to the stub files directory.
110+
* @throws RuntimeException If the stub file can't be written to the temporary directory.
111+
*
112+
* @return string Absolute path to the generated stub file.
80113
*/
81-
private function getStubsDirectory(): string
114+
private function generateStub(string $applicationType): string
82115
{
83116
$ds = DIRECTORY_SEPARATOR;
84117

85-
return dirname(__DIR__) . "{$ds}stub";
118+
$stubPath = sys_get_temp_dir() . "{$ds}yii2-phpstan-stub-" . md5($applicationType) . '.stub';
119+
120+
if (file_exists($stubPath)) {
121+
return $stubPath;
122+
}
123+
124+
$escapedType = ltrim($applicationType, '\\');
125+
$typeDeclaration = $this->buildApplicationTypeDeclaration($escapedType);
126+
127+
$content = <<<PHP
128+
<?php
129+
130+
{$typeDeclaration}
131+
132+
namespace yii {
133+
class BaseYii
134+
{
135+
/**
136+
* @var \\{$escapedType}
137+
*/
138+
public static \$app;
139+
}
140+
}
141+
142+
namespace {
143+
class Yii extends \yii\BaseYii {}
144+
}
145+
PHP;
146+
147+
if (file_put_contents($stubPath, $content) === false) {
148+
throw new RuntimeException(
149+
sprintf("Failed to write stub file to '%s'. Ensure the temporary directory is writable.", $stubPath),
150+
);
151+
}
152+
153+
return $stubPath;
86154
}
87155
}

stub/BaseYii.stub

Lines changed: 0 additions & 11 deletions
This file was deleted.

stub/BaseYiiConsole.stub

Lines changed: 0 additions & 23 deletions
This file was deleted.

stub/BaseYiiWeb.stub

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)