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