Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
77.52% |
100 / 129 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
Aliases | |
77.52% |
100 / 129 |
|
70.00% |
7 / 10 |
50.72 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTemplate | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
2 | |||
writeAliasesFileForSymbols | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getAliasFilepath | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getModifiedSymbols | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
registerAutoloader | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
buildStringOfAliases | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getAliasesArray | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getFunctionAliasesString | |
81.48% |
44 / 54 |
|
0.00% |
0 / 1 |
16.43 | |||
aliasedFunctionTemplate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * When replacements are made in-situ in the vendor directory, add aliases for the original class fqdns so |
4 | * dev dependencies can still be used. |
5 | * |
6 | * We could make the replacements in the dev dependencies but it is preferable not to edit files unnecessarily. |
7 | * Composer would warn of changes before updating (although it should probably do that already). |
8 | * This approach allows symlinked dev dependencies to be used. |
9 | * It also should work without knowing anything about the dev dependencies |
10 | * |
11 | * @package brianhenryie/strauss |
12 | */ |
13 | |
14 | namespace BrianHenryIE\Strauss\Pipeline\Aliases; |
15 | |
16 | use BrianHenryIE\Strauss\Config\AliasesConfigInterface; |
17 | use BrianHenryIE\Strauss\Helpers\FileSystem; |
18 | use BrianHenryIE\Strauss\Types\AutoloadAliasInterface; |
19 | use BrianHenryIE\Strauss\Types\ClassSymbol; |
20 | use BrianHenryIE\Strauss\Types\ConstantSymbol; |
21 | use BrianHenryIE\Strauss\Types\DiscoveredSymbols; |
22 | use BrianHenryIE\Strauss\Types\FunctionSymbol; |
23 | use Psr\Log\LoggerAwareTrait; |
24 | use Psr\Log\LoggerInterface; |
25 | use Psr\Log\NullLogger; |
26 | |
27 | class Aliases |
28 | { |
29 | use LoggerAwareTrait; |
30 | |
31 | protected AliasesConfigInterface $config; |
32 | |
33 | protected FileSystem $fileSystem; |
34 | |
35 | public function __construct( |
36 | AliasesConfigInterface $config, |
37 | FileSystem $fileSystem, |
38 | ?LoggerInterface $logger = null |
39 | ) { |
40 | $this->config = $config; |
41 | $this->fileSystem = $fileSystem; |
42 | $this->setLogger($logger ?? new NullLogger()); |
43 | } |
44 | |
45 | protected function getTemplate(array $aliasesArray, ?string $autoloadAliasesFunctionsString): string |
46 | { |
47 | $namespace = $this->config->getNamespacePrefix(); |
48 | $autoloadAliases = var_export($aliasesArray, true); |
49 | |
50 | $globalFunctionsString = !$autoloadAliasesFunctionsString ? '' |
51 | : <<<GLOBAL |
52 | // Functions and constants |
53 | $autoloadAliasesFunctionsString |
54 | GLOBAL; |
55 | |
56 | $template = file_get_contents(__DIR__ . '/autoload_aliases.template.php'); |
57 | |
58 | $template = str_replace( |
59 | '// FunctionsAndConstants', |
60 | $globalFunctionsString, |
61 | $template |
62 | ); |
63 | |
64 | $template = str_replace( |
65 | 'namespace BrianHenryIE\Strauss {', |
66 | 'namespace ' . trim($namespace, '\\') . ' {', |
67 | $template |
68 | ); |
69 | |
70 | $template = str_replace( |
71 | 'private array $autoloadAliases = [];', |
72 | "private array \$autoloadAliases = $autoloadAliases;", |
73 | $template |
74 | ); |
75 | |
76 | return $template; |
77 | } |
78 | |
79 | public function writeAliasesFileForSymbols(DiscoveredSymbols $symbols): void |
80 | { |
81 | // $modifiedSymbols = $this->getModifiedSymbols($symbols); |
82 | |
83 | $outputFilepath = $this->getAliasFilepath(); |
84 | |
85 | $fileString = $this->buildStringOfAliases($symbols, basename($outputFilepath)); |
86 | |
87 | $this->fileSystem->write($outputFilepath, $fileString); |
88 | } |
89 | |
90 | /** |
91 | * We will create `vendor/composer/autoload_aliases.php` alongside other autoload files, e.g. `autoload_real.php`. |
92 | */ |
93 | protected function getAliasFilepath(): string |
94 | { |
95 | return sprintf( |
96 | '%scomposer/autoload_aliases.php', |
97 | $this->config->getVendorDirectory() |
98 | ); |
99 | } |
100 | |
101 | protected function getModifiedSymbols(DiscoveredSymbols $symbols): DiscoveredSymbols |
102 | { |
103 | $modifiedSymbols = new DiscoveredSymbols(); |
104 | foreach ($symbols->getAll() as $symbol) { |
105 | if ($symbol->getOriginalSymbol() !== $symbol->getReplacement()) { |
106 | $modifiedSymbols->add($symbol); |
107 | } |
108 | if ($symbol instanceof FunctionSymbol) { |
109 | $functionNamespace = $symbols->getNamespaceSymbolByString($symbol->getNamespace()); |
110 | $isFunctionHasChangedNamespace = $functionNamespace->isChangedNamespace(); |
111 | |
112 | if ($isFunctionHasChangedNamespace || $symbol->getOriginalSymbol() !== $symbol->getReplacement() |
113 | ) { |
114 | $modifiedSymbols->add($symbol); |
115 | } |
116 | } |
117 | } |
118 | return $modifiedSymbols; |
119 | } |
120 | |
121 | protected function registerAutoloader(array $classmap): void |
122 | { |
123 | |
124 | // Need to autoload the classes for reflection to work (this is maybe just an issue during tests). |
125 | spl_autoload_register(function (string $class) use ($classmap) { |
126 | if (isset($classmap[$class])) { |
127 | $this->logger->debug("Autoloading $class from {$classmap[$class]}"); |
128 | try { |
129 | include_once $classmap[$class]; |
130 | } catch (\Throwable $e) { |
131 | if (false !== strpos($e->getMessage(), 'PHPUnit')) { |
132 | $this->logger->warning("Error autoloading $class from {$classmap[$class]}: " . $e->getMessage()); |
133 | } else { |
134 | $this->logger->error("Error autoloading $class from {$classmap[$class]}: " . $e->getMessage()); |
135 | } |
136 | } |
137 | } |
138 | }); |
139 | } |
140 | |
141 | protected function buildStringOfAliases(DiscoveredSymbols $modifiedSymbols, string $outputFilename): string |
142 | { |
143 | // TODO: When target !== vendor, there should be a test here to ensure the target autoloader is included, with instructions to add it. |
144 | |
145 | $autoloadAliasesFunctionsString = $this->getFunctionAliasesString($modifiedSymbols); |
146 | |
147 | $aliasesArray = $this->getAliasesArray($modifiedSymbols); |
148 | |
149 | $autoloadAliasesFileString = $this->getTemplate($aliasesArray, $autoloadAliasesFunctionsString); |
150 | |
151 | return $autoloadAliasesFileString; |
152 | } |
153 | |
154 | /** |
155 | * @param ClassSymbol $modifiedSymbols |
156 | * @param array $sourceDirClassmap |
157 | * @param array $targetDirClasssmap |
158 | * |
159 | * @return array{} |
160 | * @throws \League\Flysystem\FilesystemException |
161 | */ |
162 | protected function getAliasesArray(DiscoveredSymbols $symbols): array |
163 | { |
164 | $result = []; |
165 | |
166 | foreach ($symbols->getAll() as $originalSymbolFqdn => $symbol) { |
167 | if ($symbol->getOriginalSymbol() === $symbol->getReplacement()) { |
168 | continue; |
169 | } |
170 | if (!($symbol instanceof AutoloadAliasInterface)) { |
171 | continue; |
172 | } |
173 | $result[$originalSymbolFqdn] = $symbol->getAutoloadAliasArray(); |
174 | } |
175 | |
176 | return $result; |
177 | } |
178 | |
179 | protected function getFunctionAliasesString(DiscoveredSymbols $discoveredSymbols): string |
180 | { |
181 | $modifiedSymbols = $discoveredSymbols->getSymbols(); |
182 | |
183 | $autoloadAliasesFileString = ''; |
184 | |
185 | $symbolsByNamespace = ['\\' => []]; |
186 | foreach ($modifiedSymbols as $symbol) { |
187 | if ($symbol instanceof FunctionSymbol) { |
188 | if (!isset($symbolsByNamespace[$symbol->getNamespace()])) { |
189 | $symbolsByNamespace[$symbol->getNamespace()] = []; |
190 | } |
191 | $symbolsByNamespace[$symbol->getNamespace()][] = $symbol; |
192 | } |
193 | /** |
194 | * "define() will define constants exactly as specified. So, if you want to define a constant in a |
195 | * namespace, you will need to specify the namespace in your call to define(), even if you're calling |
196 | * define() from within a namespace." |
197 | * @see https://www.php.net/manual/en/function.define.php |
198 | */ |
199 | if ($symbol instanceof ConstantSymbol) { |
200 | $symbolsByNamespace['\\'][] = $symbol; |
201 | } |
202 | } |
203 | |
204 | if (!empty($symbolsByNamespace['\\'])) { |
205 | $globalAliasesPhpString = 'namespace {' . PHP_EOL; |
206 | |
207 | /** @var FunctionSymbol & ConstantSymbol $symbol */ |
208 | foreach ($symbolsByNamespace['\\'] as $symbol) { |
209 | $aliasesPhpString = ''; |
210 | |
211 | $originalLocalSymbol = $symbol->getOriginalSymbol(); |
212 | $replacementSymbol = $symbol->getReplacement(); |
213 | |
214 | if ($originalLocalSymbol === $replacementSymbol) { |
215 | continue; |
216 | } |
217 | |
218 | switch (get_class($symbol)) { |
219 | case FunctionSymbol::class: |
220 | // TODO: Do we need to check for `void`? Or will it just be ignored? |
221 | // Is it possible to inherit PHPDoc from the original function? |
222 | $aliasesPhpString = $this->aliasedFunctionTemplate($originalLocalSymbol, $replacementSymbol); |
223 | break; |
224 | case ConstantSymbol::class: |
225 | /** |
226 | * https://stackoverflow.com/questions/19740621/namespace-constants-and-use-as |
227 | */ |
228 | // Ideally this would somehow be loaded after everything else. |
229 | // Maybe some Patchwork style redefining of `define()` to add the alias? |
230 | // Does it matter since all references to use the constant should have been updated to the new name anyway. |
231 | // TODO: global `const`. |
232 | $aliasesPhpString = <<<EOD |
233 | if(!defined('$originalLocalSymbol') && defined('$replacementSymbol')) { |
234 | define('$originalLocalSymbol', $replacementSymbol); |
235 | } |
236 | EOD; |
237 | break; |
238 | default: |
239 | /** |
240 | * Should be addressed above. |
241 | * |
242 | * @see self::appendAliasString()) |
243 | */ |
244 | break; |
245 | } |
246 | |
247 | $globalAliasesPhpString .= $aliasesPhpString; |
248 | } |
249 | |
250 | $globalAliasesPhpString .= PHP_EOL . '}' . PHP_EOL; // Close global namespace. |
251 | |
252 | $autoloadAliasesFileString = $autoloadAliasesFileString . PHP_EOL . $globalAliasesPhpString; |
253 | } |
254 | |
255 | unset($symbolsByNamespace['\\']); |
256 | foreach ($symbolsByNamespace as $namespaceSymbol => $symbols) { |
257 | $aliasesPhpString = "namespace $namespaceSymbol {" . PHP_EOL; |
258 | |
259 | foreach ($symbols as $symbol) { |
260 | $originalLocalSymbol = $symbol->getOriginalLocalName(); |
261 | |
262 | $namespaceSymbol = $discoveredSymbols->getNamespaceSymbolByString($symbol->getNamespace()); |
263 | |
264 | if (!($symbol instanceof FunctionSymbol |
265 | && |
266 | $namespaceSymbol->isChangedNamespace()) |
267 | ) { |
268 | $this->logger->debug("Skipping {$originalLocalSymbol} because it is not being changed."); |
269 | continue; |
270 | } |
271 | |
272 | $unNamespacedOriginalSymbol = trim(str_replace($symbol->getNamespace(), '', $originalLocalSymbol), '\\'); |
273 | $namespacedOriginalSymbol = $symbol->getNamespace() . '\\' . $unNamespacedOriginalSymbol; |
274 | |
275 | $replacementSymbol = str_replace( |
276 | $namespaceSymbol->getOriginalSymbol(), |
277 | $namespaceSymbol->getReplacement(), |
278 | $namespacedOriginalSymbol |
279 | ); |
280 | |
281 | $aliasesPhpString .= $this->aliasedFunctionTemplate( |
282 | $namespacedOriginalSymbol, |
283 | $replacementSymbol, |
284 | ); |
285 | } |
286 | $aliasesPhpString .= "}" . PHP_EOL; // Close namespace. |
287 | |
288 | $autoloadAliasesFileString .= $aliasesPhpString; |
289 | } |
290 | |
291 | return $autoloadAliasesFileString; |
292 | } |
293 | |
294 | /** |
295 | * Returns the PHP for `if(!function_exists...` for an aliased function. |
296 | * |
297 | * Ensures the correct leading backslashes. |
298 | * |
299 | * @param string $namespacedOriginalFunction |
300 | * @param string $namespacedReplacementFunction |
301 | */ |
302 | protected function aliasedFunctionTemplate( |
303 | string $namespacedOriginalFunction, |
304 | string $namespacedReplacementFunction |
305 | ): string { |
306 | $namespacedOriginalFunction = '\\\\' . trim($namespacedOriginalFunction, '\\'); |
307 | $namespacedOriginalFunction = preg_replace('/\\\\+/', '\\\\\\\\', $namespacedOriginalFunction); |
308 | |
309 | $localOriginalFunction = array_reverse(explode('\\', $namespacedOriginalFunction))[0]; |
310 | |
311 | $namespacedReplacementFunction = '\\' . trim($namespacedReplacementFunction, '\\'); |
312 | $namespacedReplacementFunction = preg_replace('/\\\\+/', '\\', $namespacedReplacementFunction); |
313 | |
314 | return <<<EOD |
315 | if(!function_exists('$namespacedOriginalFunction')){ |
316 | function $localOriginalFunction(...\$args) { |
317 | return $namespacedReplacementFunction(...func_get_args()); |
318 | } |
319 | } |
320 | EOD . PHP_EOL; |
321 | } |
322 | } |