Skip to content

Commit 79d8441

Browse files
Replace static stub files with dynamic stub generation for Yii::$app type inference, adding support for custom application types. (#83)
1 parent a2fe54e commit 79d8441

14 files changed

Lines changed: 577 additions & 122 deletions

.github/workflows/ecs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ permissions:
1919
jobs:
2020
easy-coding-standard:
2121
uses: yii2-framework/actions/.github/workflows/ecs.yml@v1
22+
with:
23+
php-version: '["8.3"]'

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: 156 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@
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 getmypid;
13+
use function ltrim;
14+
use function md5;
15+
use function rename;
16+
use function sprintf;
17+
use function strrpos;
18+
use function substr;
19+
use function sys_get_temp_dir;
20+
use function unlink;
21+
22+
use const DIRECTORY_SEPARATOR;
23+
use const LOCK_EX;
24+
925
/**
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.
26+
* Provides dynamic stub file generation for PHPStan analysis based on the configured Yii Application type.
2027
*
21-
* Stub files are resolved from the `stub/` directory relative to the extension source.
28+
* Generates a stub file at runtime that overrides the `Yii::$app` property type annotation to match the application
29+
* type specified in the project configuration. This enables PHPStan to infer the correct application class for web,
30+
* console, or custom application contexts without requiring separate static stub files.
2231
*
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.
32+
* The generated stub is written atomically to a deterministic temporary file path, cached across PHPStan runs for the
33+
* same content.
2834
*
2935
* @see ServiceMap for service and component map for Yii Application static analysis.
3036
*
@@ -33,55 +39,161 @@
3339
*/
3440
final class StubFilesExtension implements \PHPStan\PhpDoc\StubFilesExtension
3541
{
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-
4442
/**
4543
* @param ServiceMap $serviceMap Service and component map for Yii Application static analysis.
44+
* @param string $stubDirectory Directory for generated stub files (default: system temporary directory).
4645
*/
47-
public function __construct(private readonly ServiceMap $serviceMap) {}
46+
public function __construct(
47+
private readonly ServiceMap $serviceMap,
48+
private readonly string $stubDirectory = '',
49+
) {}
4850

4951
/**
50-
* Retrieves the appropriate stub file path for PHPStan analysis based on the Yii Application type.
52+
* Retrieves the dynamically generated stub file path for PHPStan analysis.
5153
*
52-
* Determines the stub file to use for static analysis by inspecting the application type provided by the
53-
* {@see ServiceMap} instance.
54+
* Generates a stub file with the correct `@var` type annotation for `Yii::$app` based on the configured application
55+
* type from the {@see ServiceMap} instance.
5456
*
55-
* This enables PHPStan to reflect the correct set of Yii Application properties and methods for web, console, or
56-
* base application contexts.
57+
* @return array Array containing the absolute path to the generated stub file for PHPStan analysis.
5758
*
58-
* @return string[] Array containing the absolute path to the resolved stub file for PHPStan analysis.
59+
* @phpstan-return string[]
5960
*/
6061
public function getFiles(): array
6162
{
62-
$stubsDirectory = $this->getStubsDirectory();
63-
$applicationType = $this->serviceMap->getApplicationType();
63+
return [$this->generateStub($this->serviceMap->getApplicationType())];
64+
}
65+
66+
/**
67+
* Builds the application type class declaration block for the stub.
68+
*
69+
* Generates the necessary namespace and class declarations to satisfy PHPStan stub type resolution for the
70+
* configured application type. Includes the base `\yii\base\Application` declaration and, if the configured type
71+
* differs, an additional declaration for the specific application class.
72+
*
73+
* @param string $applicationType Fully qualified class name of the application type (without leading backslash).
74+
*
75+
* @return string PHP namespace block declarations for the stub file.
76+
*/
77+
private function buildApplicationTypeDeclaration(string $applicationType): string
78+
{
79+
$baseDeclaration = <<<PHP
80+
namespace yii\base {
81+
abstract class Application {}
82+
}
83+
PHP;
84+
85+
if ($applicationType === Application::class) {
86+
return $baseDeclaration;
87+
}
88+
89+
$lastSeparator = strrpos($applicationType, '\\');
90+
91+
if ($lastSeparator === false) {
92+
$namespace = '';
93+
$className = $applicationType;
94+
} else {
95+
$namespace = substr($applicationType, 0, $lastSeparator);
96+
$className = substr($applicationType, $lastSeparator + 1);
97+
}
98+
99+
$namespaceBlock = $namespace !== '' ? "namespace {$namespace}" : 'namespace';
100+
101+
return <<<PHP
102+
{$baseDeclaration}
103+
104+
{$namespaceBlock} {
105+
class {$className} extends \yii\base\Application {}
106+
}
107+
PHP;
108+
}
109+
110+
/**
111+
* Builds the full stub content for the specified application type.
112+
*
113+
* Assembles the PHP stub content including class declarations for PHPStan stub type resolution, the `BaseYii`
114+
* class with the `$app` property type annotation, and the `Yii` class extending `BaseYii`.
115+
*
116+
* @param string $applicationType Fully qualified class name of the application type (without leading backslash).
117+
*
118+
* @return string Complete PHP stub file content.
119+
*/
120+
private function buildStubContent(string $applicationType): string
121+
{
122+
$typeDeclaration = $this->buildApplicationTypeDeclaration($applicationType);
123+
124+
return <<<PHP
125+
<?php
64126
65-
$stubFileName = self::APPLICATION_TYPE_STUBS[$applicationType] ?? self::DEFAULT_STUB;
66-
$stubFilePath = $stubsDirectory . DIRECTORY_SEPARATOR . $stubFileName;
127+
{$typeDeclaration}
67128
68-
return [$stubFilePath];
129+
namespace yii {
130+
class BaseYii
131+
{
132+
/**
133+
* @var \\{$applicationType}
134+
*/
135+
public static \$app;
136+
}
137+
}
138+
139+
namespace {
140+
class Yii extends \yii\BaseYii {}
141+
}
142+
PHP;
69143
}
70144

71145
/**
72-
* Retrieves the absolute path to the stub files directory for PHPStan analysis.
146+
* Generates a stub file for the specified application type.
147+
*
148+
* Creates a PHP stub that overrides the `BaseYii::$app` property type annotation to match the configured
149+
* application type. Includes necessary class declarations for PHPStan stub type resolution. The cache key is
150+
* derived from the generated content, preventing stale files after generator changes. The stub is written
151+
* atomically using a temporary file and `rename()` to prevent concurrent PHPStan runs from reading half-written
152+
* files.
73153
*
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.
154+
* @param string $applicationType Fully qualified class name of the application type.
76155
*
77-
* This method is used internally to construct absolute paths for stub file resolution in static analysis.
156+
* @throws RuntimeException If the stub file can't be written to the temporary directory.
78157
*
79-
* @return string Absolute path to the stub files directory.
158+
* @return string Absolute path to the generated stub file.
80159
*/
81-
private function getStubsDirectory(): string
160+
private function generateStub(string $applicationType): string
82161
{
83-
$ds = DIRECTORY_SEPARATOR;
162+
$escapedType = ltrim($applicationType, '\\');
163+
164+
$content = $this->buildStubContent($escapedType);
165+
166+
$directory = $this->stubDirectory !== '' ? $this->stubDirectory : sys_get_temp_dir();
167+
$stubPath = $directory . DIRECTORY_SEPARATOR . 'yii2-phpstan-stub-' . md5($content) . '.stub';
168+
169+
if (file_exists($stubPath)) {
170+
return $stubPath;
171+
}
172+
173+
$temporaryPath = $stubPath . '.' . getmypid() . '.tmp';
174+
175+
if (@file_put_contents($temporaryPath, $content, LOCK_EX) === false) {
176+
throw new RuntimeException(
177+
sprintf("Failed to write stub file to '%s'. Ensure the temporary directory is writable.", $stubPath),
178+
);
179+
}
180+
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.
184+
if (!@rename($temporaryPath, $stubPath)) {
185+
@unlink($temporaryPath);
186+
187+
if (file_exists($stubPath)) {
188+
return $stubPath;
189+
}
190+
191+
throw new RuntimeException(
192+
sprintf("Failed to write stub file to '%s'. Ensure the temporary directory is writable.", $stubPath),
193+
);
194+
}
195+
// @codeCoverageIgnoreEnd
84196

85-
return dirname(__DIR__) . "{$ds}stub";
197+
return $stubPath;
86198
}
87199
}

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)