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