|
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 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 | + |
9 | 19 | /** |
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. |
20 | 21 | * |
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. |
22 | 25 | * |
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. |
28 | 28 | * |
29 | 29 | * @see ServiceMap for service and component map for Yii Application static analysis. |
30 | 30 | * |
|
33 | 33 | */ |
34 | 34 | final class StubFilesExtension implements \PHPStan\PhpDoc\StubFilesExtension |
35 | 35 | { |
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 | 36 | /** |
45 | 37 | * @param ServiceMap $serviceMap Service and component map for Yii Application static analysis. |
46 | 38 | */ |
47 | 39 | public function __construct(private readonly ServiceMap $serviceMap) {} |
48 | 40 |
|
49 | 41 | /** |
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. |
51 | 43 | * |
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. |
54 | 46 | * |
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. |
57 | 48 | * |
58 | | - * @return string[] Array containing the absolute path to the resolved stub file for PHPStan analysis. |
| 49 | + * @phpstan-return string[] |
59 | 50 | */ |
60 | 51 | public function getFiles(): array |
61 | 52 | { |
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; |
64 | 74 |
|
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 | + } |
67 | 78 |
|
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; |
69 | 98 | } |
70 | 99 |
|
71 | 100 | /** |
72 | | - * Retrieves the absolute path to the stub files directory for PHPStan analysis. |
| 101 | + * Generates a stub file for the specified application type. |
73 | 102 | * |
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. |
76 | 107 | * |
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. |
78 | 109 | * |
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. |
80 | 113 | */ |
81 | | - private function getStubsDirectory(): string |
| 114 | + private function generateStub(string $applicationType): string |
82 | 115 | { |
83 | 116 | $ds = DIRECTORY_SEPARATOR; |
84 | 117 |
|
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; |
86 | 154 | } |
87 | 155 | } |
0 commit comments