|
4 | 4 |
|
5 | 5 | namespace yii2\extensions\phpstan; |
6 | 6 |
|
| 7 | +use RuntimeException; |
7 | 8 | use yii\base\Application; |
8 | 9 |
|
| 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 | + |
9 | 25 | /** |
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. |
20 | 27 | * |
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. |
22 | 31 | * |
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. |
28 | 34 | * |
29 | 35 | * @see ServiceMap for service and component map for Yii Application static analysis. |
30 | 36 | * |
|
33 | 39 | */ |
34 | 40 | final class StubFilesExtension implements \PHPStan\PhpDoc\StubFilesExtension |
35 | 41 | { |
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 | | - |
44 | 42 | /** |
45 | 43 | * @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). |
46 | 45 | */ |
47 | | - public function __construct(private readonly ServiceMap $serviceMap) {} |
| 46 | + public function __construct( |
| 47 | + private readonly ServiceMap $serviceMap, |
| 48 | + private readonly string $stubDirectory = '', |
| 49 | + ) {} |
48 | 50 |
|
49 | 51 | /** |
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. |
51 | 53 | * |
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. |
54 | 56 | * |
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. |
57 | 58 | * |
58 | | - * @return string[] Array containing the absolute path to the resolved stub file for PHPStan analysis. |
| 59 | + * @phpstan-return string[] |
59 | 60 | */ |
60 | 61 | public function getFiles(): array |
61 | 62 | { |
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 |
64 | 126 |
|
65 | | - $stubFileName = self::APPLICATION_TYPE_STUBS[$applicationType] ?? self::DEFAULT_STUB; |
66 | | - $stubFilePath = $stubsDirectory . DIRECTORY_SEPARATOR . $stubFileName; |
| 127 | + {$typeDeclaration} |
67 | 128 |
|
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; |
69 | 143 | } |
70 | 144 |
|
71 | 145 | /** |
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. |
73 | 153 | * |
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. |
76 | 155 | * |
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. |
78 | 157 | * |
79 | | - * @return string Absolute path to the stub files directory. |
| 158 | + * @return string Absolute path to the generated stub file. |
80 | 159 | */ |
81 | | - private function getStubsDirectory(): string |
| 160 | + private function generateStub(string $applicationType): string |
82 | 161 | { |
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 |
84 | 196 |
|
85 | | - return dirname(__DIR__) . "{$ds}stub"; |
| 197 | + return $stubPath; |
86 | 198 | } |
87 | 199 | } |
0 commit comments