Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.52% covered (warning)
77.52%
100 / 129
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Aliases
77.52% covered (warning)
77.52%
100 / 129
70.00% covered (warning)
70.00%
7 / 10
50.72
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
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
2
 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\ClassSymbol;
20use BrianHenryIE\Strauss\Types\ConstantSymbol;
21use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
22use BrianHenryIE\Strauss\Types\FunctionSymbol;
23use Psr\Log\LoggerAwareTrait;
24use Psr\Log\LoggerInterface;
25use Psr\Log\NullLogger;
26
27class 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}