Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Aliases
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 9
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getTemplate
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 writeAliasesFileForSymbols
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getAliasFilepath
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getModifiedSymbols
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 registerAutoloader
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 buildStringOfAliases
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getAliasesArray
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getFunctionAliasesString
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
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;
15
16use BrianHenryIE\Strauss\Config\AliasesConfigInterface;
17use BrianHenryIE\Strauss\Files\File;
18use BrianHenryIE\Strauss\Helpers\FileSystem;
19use BrianHenryIE\Strauss\Helpers\NamespaceSort;
20use BrianHenryIE\Strauss\Types\AutoloadAliasInterface;
21use BrianHenryIE\Strauss\Types\ClassSymbol;
22use BrianHenryIE\Strauss\Types\ConstantSymbol;
23use BrianHenryIE\Strauss\Types\DiscoveredSymbol;
24use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
25use BrianHenryIE\Strauss\Types\FunctionSymbol;
26use BrianHenryIE\Strauss\Types\NamespaceSymbol;
27use Composer\ClassMapGenerator\ClassMapGenerator;
28use League\Flysystem\StorageAttributes;
29use Psr\Log\LoggerAwareTrait;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
32use ReflectionClass;
33
34class Aliases
35{
36    use LoggerAwareTrait;
37
38    protected AliasesConfigInterface $config;
39
40    protected FileSystem $fileSystem;
41
42    public function __construct(
43        AliasesConfigInterface $config,
44        FileSystem $fileSystem,
45        ?LoggerInterface $logger = null
46    ) {
47        $this->config = $config;
48        $this->fileSystem = $fileSystem;
49        $this->setLogger($logger ?? new NullLogger());
50    }
51
52    protected function getTemplate(array $aliasesArray, ?string $autoloadAliasesFunctionsString): string
53    {
54        $namespace = $this->config->getNamespacePrefix();
55        $autoloadAliases = var_export($aliasesArray, true);
56
57        $globalFunctionsString = !$autoloadAliasesFunctionsString ? ''
58                : <<<GLOBAL
59                // Global functions
60                namespace {
61                    $autoloadAliasesFunctionsString
62                }
63                GLOBAL;
64
65        return <<<TEMPLATE
66                <?php
67                
68                $globalFunctionsString
69                
70                // Everything else – irrelevant that this part is namespaced
71                namespace $namespace {
72                    
73                class AliasAutoloader
74                {
75                    private string \$includeFilePath;
76                
77                    private array \$autoloadAliases = $autoloadAliases;
78                
79                    public function __construct() {
80                        \$this->includeFilePath = __DIR__ . '/autoload_alias.php';
81                    }
82                    
83                    public function autoload(\$class)
84                    {
85                        if (!isset(\$this->autoloadAliases[\$class])) {
86                            return;
87                        }
88                        switch (\$this->autoloadAliases[\$class]['type']) {
89                            case 'class':
90                                \$this->load(
91                                    \$this->classTemplate(
92                                        \$this->autoloadAliases[\$class]
93                                    )
94                                );
95                                break;
96                            case 'interface':
97                                \$this->load(
98                                    \$this->interfaceTemplate(
99                                        \$this->autoloadAliases[\$class]
100                                    )
101                                );
102                                break;
103                            case 'trait':
104                                \$this->load(
105                                    \$this->traitTemplate(
106                                        \$this->autoloadAliases[\$class]
107                                    )
108                                );
109                                break;
110                            default:
111                                // Never.
112                                break;
113                        }
114                    }
115                
116                    private function load(string \$includeFile)
117                    {
118                        file_put_contents(\$this->includeFilePath, \$includeFile);
119                        include \$this->includeFilePath;
120                        file_exists(\$this->includeFilePath) && unlink(\$this->includeFilePath);
121                    }
122                    
123                    // TODO: What if this was a real function in this class that could be used for testing, which would be read and written by php-parser?
124                    private function classTemplate(array \$class): string
125                    {
126                        \$abstract = \$class['isabstract'] ? 'abstract ' : '';
127                        \$classname = \$class['classname'];
128                        if(isset(\$class['namespace'])) {
129                            \$namespace = "namespace {\$class['namespace']};";
130                            \$extends = '\\\\' . \$class['extends'];
131                            \$implements = empty(\$class['implements']) ? ''
132                                : ' implements \\\\' . implode(', \\\\', \$class['implements']);
133                        } else {
134                            \$namespace = '';
135                            \$extends = \$class['extends'];
136                            \$implements = !empty(\$class['implements']) ? ''
137                                : ' implements ' . implode(', ', \$class['implements']);
138                        }
139                        return <<<EOD
140                                <?php
141                                \$namespace
142                                \$abstract class \$classname extends \$extends \$implements {}
143                                EOD;
144                    }
145                    
146                    private function interfaceTemplate(array \$interface): string
147                    {
148                        \$interfacename = \$interface['interfacename'];
149                        \$namespace = isset(\$interface['namespace']) 
150                            ? "namespace {\$interface['namespace']};" : '';
151                        \$extends = isset(\$interface['namespace'])
152                            ? '\\\\' . implode('\\\\ ,', \$interface['extends'])
153                            : implode(', ', \$interface['extends']);
154                        return <<<EOD
155                                <?php
156                                \$namespace
157                                interface \$interfacename extends \$extends {}
158                                EOD;
159                    } 
160                    private function traitTemplate(array \$trait): string
161                    {
162                        \$traitname = \$trait['traitname'];
163                        \$namespace = isset(\$trait['namespace']) 
164                            ? "namespace {\$trait['namespace']};" : '';
165                        \$uses = isset(\$trait['namespace'])
166                            ? '\\\\' . implode(';' . PHP_EOL . '    use \\\\', \$trait['use'])
167                            : implode(';' . PHP_EOL . '    use ', \$trait['use']);
168                        return <<<EOD
169                                <?php
170                                \$namespace
171                                trait \$traitname { 
172                                    use \$uses; 
173                                }
174                                EOD;
175                        }
176                    }
177                    
178                    spl_autoload_register( [ new AliasAutoloader(), 'autoload' ] );
179
180                }
181                TEMPLATE;
182    }
183
184    public function writeAliasesFileForSymbols(DiscoveredSymbols $symbols): void
185    {
186        $modifiedSymbols = $this->getModifiedSymbols($symbols);
187
188        $outputFilepath = $this->getAliasFilepath();
189
190        $fileString = $this->buildStringOfAliases($modifiedSymbols, basename($outputFilepath));
191
192        $this->fileSystem->write($outputFilepath, $fileString);
193    }
194
195    /**
196     * We will create `vendor/composer/autoload_aliases.php` alongside other autoload files, e.g. `autoload_real.php`.
197     */
198    protected function getAliasFilepath(): string
199    {
200        return  sprintf(
201            '%scomposer/autoload_aliases.php',
202            $this->config->getVendorDirectory()
203        );
204    }
205
206    protected function getModifiedSymbols(DiscoveredSymbols $symbols): DiscoveredSymbols
207    {
208        $modifiedSymbols = new DiscoveredSymbols();
209        foreach ($symbols->getAll() as $symbol) {
210            if ($symbol->getOriginalSymbol() !== $symbol->getReplacement()) {
211                $modifiedSymbols->add($symbol);
212            }
213        }
214        return $modifiedSymbols;
215    }
216
217    protected function registerAutoloader(array $classmap): void
218    {
219
220        // Need to autoload the classes for reflection to work (this is maybe just an issue during tests).
221        spl_autoload_register(function (string $class) use ($classmap) {
222            if (isset($classmap[$class])) {
223                $this->logger->debug("Autoloading $class from {$classmap[$class]}");
224                try {
225                    include_once $classmap[$class];
226                } catch (\Throwable $e) {
227                    if (false !== strpos($e->getMessage(), 'PHPUnit')) {
228                        $this->logger->warning("Error autoloading $class from {$classmap[$class]}" . $e->getMessage());
229                    } else {
230                        $this->logger->error("Error autoloading $class from {$classmap[$class]}" . $e->getMessage());
231                    }
232                }
233            }
234        });
235    }
236
237    protected function buildStringOfAliases(DiscoveredSymbols $symbols, string $outputFilename): string
238    {
239        // TODO: When target !== vendor, there should be a test here to ensure the target autoloader is included, with instructions to add it.
240
241        $modifiedSymbols = $this->getModifiedSymbols($symbols);
242
243        $functionSymbols = $modifiedSymbols->getDiscoveredFunctions();
244
245        $autoloadAliasesFunctionsString = count($functionSymbols)>0
246            ? $this->getFunctionAliasesString($functionSymbols)
247            : null;
248        $aliasesArray = $this->getAliasesArray($symbols);
249
250        $autoloadAliasesFileString = $this->getTemplate($aliasesArray, $autoloadAliasesFunctionsString);
251
252        return $autoloadAliasesFileString;
253    }
254
255    /**
256     * @param array<NamespaceSymbol|ClassSymbol> $modifiedSymbols
257     * @param array $sourceDirClassmap
258     * @param array $targetDirClasssmap
259     * @return array{}
260     * @throws \League\Flysystem\FilesystemException
261     */
262    protected function getAliasesArray(DiscoveredSymbols $symbols): array
263    {
264        $result = [];
265
266        foreach ($symbols->getAll() as $originalSymbolFqdn => $symbol) {
267            if ($symbol->getOriginalSymbol() === $symbol->getReplacement()) {
268                continue;
269            }
270            if (!($symbol instanceof AutoloadAliasInterface)) {
271                continue;
272            }
273            $result[$originalSymbolFqdn] = $symbol->getAutoloadAliasArray();
274        }
275
276        return $result;
277    }
278
279    protected function getFunctionAliasesString(array $modifiedSymbols): string
280    {
281        $autoloadAliasesFileString = '';
282
283        foreach ($modifiedSymbols as $symbol) {
284            $aliasesPhpString = '';
285
286            $originalSymbol = $symbol->getOriginalSymbol();
287            $replacementSymbol = $symbol->getReplacement();
288
289//            if (!$symbol->getSourceFile()->isDoDelete()) {
290//                $this->logger->debug("Skipping {$originalSymbol} because it is not marked for deletion.");
291//                continue;
292//            }
293
294            if ($originalSymbol === $replacementSymbol) {
295                $this->logger->debug("Skipping {$originalSymbol} because it is not being changed.");
296                continue;
297            }
298
299            switch (get_class($symbol)) {
300                case FunctionSymbol::class:
301                    // TODO: Do we need to check for `void`? Or will it just be ignored?
302                    // Is it possible to inherit PHPDoc from the original function?
303                    $aliasesPhpString = <<<EOD
304        if(!function_exists('$originalSymbol')){
305            function $originalSymbol(...\$args) { return $replacementSymbol(func_get_args()); }
306        }
307        EOD;
308                    break;
309                case ConstantSymbol::class:
310                    /**
311                     * https://stackoverflow.com/questions/19740621/namespace-constants-and-use-as
312                     */
313                    // Ideally this would somehow be loaded after everything else.
314                    // Maybe some Patchwork style redefining of `define()` to add the alias?
315                    // Does it matter since all references to use the constant should have been updated to the new name anyway.
316                    // TODO: global `const`.
317                    $aliasesPhpString = <<<EOD
318        if(!defined('$originalSymbol') && defined('$replacementSymbol')) { 
319            define('$originalSymbol', $replacementSymbol); 
320        }
321        EOD;
322                    break;
323                default:
324                    /**
325                     * Should be addressed above.
326                     *
327                     * @see self::appendAliasString())
328                     */
329                    break;
330            }
331
332            $autoloadAliasesFileString .= $aliasesPhpString;
333        }
334
335        return $autoloadAliasesFileString;
336    }
337}