Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
41.56% covered (danger)
41.56%
64 / 154
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
VendorComposerAutoload
41.56% covered (danger)
41.56%
64 / 154
28.57% covered (danger)
28.57%
2 / 7
160.93
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
 addVendorPrefixedAutoloadToVendorAutoload
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 addAliasesFileToComposer
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
6.75
 isComposerInstalled
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 isComposerNoDev
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addAliasesFileToComposerAutoload
90.00% covered (success)
90.00%
45 / 50
0.00% covered (danger)
0.00%
0 / 1
5.03
 addVendorPrefixedAutoloadToComposerAutoload
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Edit vendor/autoload.php to also load the vendor/composer/autoload_aliases.php file and the vendor-prefixed/autoload.php file.
4 */
5
6namespace BrianHenryIE\Strauss\Pipeline\Autoload;
7
8use BrianHenryIE\Strauss\Composer\Extra\StraussConfig;
9use BrianHenryIE\Strauss\Config\AutoloadConfigInterface;
10use BrianHenryIE\Strauss\Helpers\FileSystem;
11use PhpParser\Error;
12use PhpParser\Node;
13use PhpParser\NodeTraverser;
14use PhpParser\NodeVisitorAbstract;
15use PhpParser\ParserFactory;
16use PhpParser\PrettyPrinter\Standard;
17use Psr\Log\LoggerAwareTrait;
18use Psr\Log\LoggerInterface;
19
20class VendorComposerAutoload
21{
22    use LoggerAwareTrait;
23
24    protected FileSystem $fileSystem;
25
26    protected AutoloadConfigInterface $config;
27
28    public function __construct(
29        AutoloadConfigInterface $config,
30        Filesystem             $filesystem,
31        LoggerInterface        $logger
32    ) {
33        $this->config = $config;
34        $this->fileSystem = $filesystem;
35        $this->setLogger($logger);
36    }
37
38    public function addVendorPrefixedAutoloadToVendorAutoload(): void
39    {
40        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
41            $this->logger->info("Target dir is source dir, no autoload.php to add.");
42            return;
43        }
44
45        $composerAutoloadPhpFilepath = $this->config->getVendorDirectory() . 'autoload.php';
46
47        if (!$this->fileSystem->fileExists($composerAutoloadPhpFilepath)) {
48            $this->logger->info("No autoload.php found:" . $composerAutoloadPhpFilepath);
49            return;
50        }
51
52        $newAutoloadPhpFilepath = $this->config->getTargetDirectory() . 'autoload.php';
53
54        if (!$this->fileSystem->fileExists($newAutoloadPhpFilepath)) {
55            $this->logger->warning("No new autoload.php found: " . $newAutoloadPhpFilepath);
56        }
57
58        $this->logger->info('Modifying original autoload.php to add `' . $newAutoloadPhpFilepath);
59
60        $composerAutoloadPhpFileString = $this->fileSystem->read($composerAutoloadPhpFilepath);
61
62        $newComposerAutoloadPhpFileString = $this->addVendorPrefixedAutoloadToComposerAutoload($composerAutoloadPhpFileString);
63
64        if ($newComposerAutoloadPhpFileString !== $composerAutoloadPhpFileString) {
65            $this->logger->info('Writing new autoload.php');
66            $this->fileSystem->write($composerAutoloadPhpFilepath, $newComposerAutoloadPhpFileString);
67        } else {
68            $this->logger->debug('No changes to autoload.php');
69        }
70    }
71
72    /**
73     * Given the PHP code string for `vendor/autoload.php`, add a `require_once autoload_aliases.php`
74     * before require autoload_real.php.
75     */
76    public function addAliasesFileToComposer(): void
77    {
78        if ($this->isComposerInstalled()) {
79            $this->logger->info("Strauss installed via Composer, no need to add `autoload_aliases.php` to `vendor/autoload.php`");
80            return;
81        }
82
83        $composerAutoloadPhpFilepath = $this->config->getVendorDirectory() . 'autoload.php';
84
85        if (!$this->fileSystem->fileExists($composerAutoloadPhpFilepath)) {
86            // No `vendor/autoload.php` file to add `autoload_aliases.php` to.
87            $this->logger->error("No autoload.php found: " . $composerAutoloadPhpFilepath);
88            // TODO: Should probably throw an exception here.
89            return;
90        }
91
92        if ($this->isComposerNoDev()) {
93            $this->logger->notice("Composer was run with `--no-dev`, no need to add `autoload_aliases.php` to `vendor/autoload.php`");
94            return;
95        }
96
97        $this->logger->info('Modifying original autoload.php to add autoload_aliases.php in ' . $this->config->getVendorDirectory());
98
99        $composerAutoloadPhpFileString = $this->fileSystem->read($composerAutoloadPhpFilepath);
100
101        $newComposerAutoloadPhpFileString = $this->addAliasesFileToComposerAutoload($composerAutoloadPhpFileString);
102
103        if ($newComposerAutoloadPhpFileString !== $composerAutoloadPhpFileString) {
104            $this->logger->info('Writing new autoload.php');
105            $this->fileSystem->write($composerAutoloadPhpFilepath, $newComposerAutoloadPhpFileString);
106        } else {
107            $this->logger->debug('No changes to autoload.php');
108        }
109    }
110
111    /**
112     * Determine is Strauss installed via Composer (otherwise presumably run via phar).
113     */
114    protected function isComposerInstalled(): bool
115    {
116        if (!$this->fileSystem->fileExists($this->config->getVendorDirectory() . 'composer/installed.json')) {
117            return false;
118        }
119
120        $installedJsonArray = json_decode($this->fileSystem->read($this->config->getVendorDirectory() . 'composer/installed.json'), true);
121
122        return isset($installedJsonArray['dev-package-names']['brianhenryie/strauss']);
123    }
124
125    /**
126     * Read `vendor/composer/installed.json` to determine if the composer was run with `--no-dev`.
127     *
128     * {
129     *   "packages": [],
130     *   "dev": true,
131     *   "dev-package-names": []
132     * }
133     */
134    protected function isComposerNoDev(): bool
135    {
136        $installedJson = $this->fileSystem->read($this->config->getVendorDirectory() . 'composer/installed.json');
137        $installedJsonArray = json_decode($installedJson, true);
138        return !$installedJsonArray['dev'];
139    }
140
141    /**
142     * This is a very over-engineered way to do a string replace.
143     *
144     * `require_once __DIR__ . '/composer/autoload_aliases.php';`
145     */
146    protected function addAliasesFileToComposerAutoload(string $code): string
147    {
148        if (false !== strpos($code, '/composer/autoload_aliases.php')) {
149            $this->logger->info('vendor/autoload.php already includes autoload_aliases.php');
150            return $code;
151        }
152
153        $parser = (new ParserFactory())->createForNewestSupportedVersion();
154        try {
155            $ast = $parser->parse($code);
156        } catch (Error $error) {
157            $this->logger->error("Parse error: {$error->getMessage()}");
158            return $code;
159        }
160
161        $traverser = new NodeTraverser();
162        $traverser->addVisitor(new class() extends NodeVisitorAbstract {
163
164            public function leaveNode(Node $node)
165            {
166                if (get_class($node) === \PhpParser\Node\Stmt\Expression::class) {
167                    $prettyPrinter = new Standard();
168                    $maybeRequireAutoloadReal = $prettyPrinter->prettyPrintExpr($node->expr);
169
170                    // Every `vendor/autoload.php` should have this line.
171                    $target = "require_once __DIR__ . '/composer/autoload_real.php'";
172
173                    // If this node isn't the one we want to insert before, continue.
174                    if ($maybeRequireAutoloadReal !== $target) {
175                        return $node;
176                    }
177
178                    // __DIR__ . '/composer/autoload_aliases.php'
179                    $path = new \PhpParser\Node\Expr\BinaryOp\Concat(
180                        new \PhpParser\Node\Scalar\MagicConst\Dir(),
181                        new \PhpParser\Node\Scalar\String_('/composer/autoload_aliases.php')
182                    );
183
184                    // require_once
185                    $requireOnceAutoloadAliases = new Node\Stmt\Expression(
186                        new \PhpParser\Node\Expr\Include_(
187                            $path,
188                            \PhpParser\Node\Expr\Include_::TYPE_REQUIRE_ONCE
189                        )
190                    );
191
192                    // if(file_exists()){}
193                    $ifFileExistsRequireOnceAutoloadAliases = new \PhpParser\Node\Stmt\If_(
194                        new \PhpParser\Node\Expr\FuncCall(
195                            new \PhpParser\Node\Name('file_exists'),
196                            [
197                                new \PhpParser\Node\Arg($path)
198                            ],
199                        ),
200                        [
201                            'stmts' => [
202                                $requireOnceAutoloadAliases
203                            ],
204                        ]
205                    );
206
207                    // Add a blank line. Probably not the correct way to do this.
208                    $node->setAttribute('comments', [new \PhpParser\Comment('')]);
209                    $ifFileExistsRequireOnceAutoloadAliases->setAttribute('comments', [new \PhpParser\Comment('')]);
210
211                    return [
212                        $ifFileExistsRequireOnceAutoloadAliases,
213                        $node
214                    ];
215                }
216                return $node;
217            }
218        });
219
220        $modifiedStmts = $traverser->traverse($ast);
221
222        $prettyPrinter = new Standard();
223
224        return $prettyPrinter->prettyPrintFile($modifiedStmts);
225    }
226
227    /**
228     * `require_once __DIR__ . '/../vendor-prefixed/autoload.php';`
229     */
230    protected function addVendorPrefixedAutoloadToComposerAutoload(string $code): string
231    {
232        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
233            $this->logger->info('Vendor directory is target directory, no autoloader to add.');
234            return $code;
235        }
236
237        $targetDirAutoload = '/' . $this->fileSystem->getRelativePath($this->config->getVendorDirectory(), $this->config->getTargetDirectory()) . 'autoload.php';
238
239        if (false !== strpos($code, $targetDirAutoload)) {
240            $this->logger->info('vendor/autoload.php already includes ' . $targetDirAutoload);
241            return $code;
242        }
243
244        $parser = (new ParserFactory())->createForNewestSupportedVersion();
245        try {
246            $ast = $parser->parse($code);
247        } catch (Error $error) {
248            $this->logger->error("Parse error: {$error->getMessage()}");
249            return $code;
250        }
251
252        $traverser = new NodeTraverser();
253        $traverser->addVisitor(new class($targetDirAutoload) extends NodeVisitorAbstract {
254
255            protected bool $added = false;
256            protected ?string $targetDirectoryAutoload;
257            public function __construct(?string $targetDirectoryAutoload)
258            {
259                $this->targetDirectoryAutoload = $targetDirectoryAutoload;
260            }
261
262            public function leaveNode(Node $node)
263            {
264                if ($this->added) {
265                    return $node;
266                }
267
268                if (get_class($node) === \PhpParser\Node\Stmt\Expression::class) {
269                    $prettyPrinter = new Standard();
270                    $nodeText = $prettyPrinter->prettyPrintExpr($node->expr);
271
272                    $targets = [
273                        "require_once __DIR__ . '/composer/autoload_real.php'",
274                    ];
275
276                    if (!in_array($nodeText, $targets)) {
277                        return $node;
278                    }
279
280                    // __DIR__ . '../vendor-prefixed/autoload.php'
281                    $path = new \PhpParser\Node\Expr\BinaryOp\Concat(
282                        new \PhpParser\Node\Scalar\MagicConst\Dir(),
283                        new Node\Scalar\String_($this->targetDirectoryAutoload)
284                    );
285
286                    // require_once
287                    $requireOnceStraussAutoload = new Node\Stmt\Expression(
288                        new Node\Expr\Include_(
289                            $path,
290                            Node\Expr\Include_::TYPE_REQUIRE_ONCE
291                        )
292                    );
293
294                    // if(file_exists()){}
295                    $ifFileExistsRequireOnceStraussAutoload = new \PhpParser\Node\Stmt\If_(
296                        new \PhpParser\Node\Expr\FuncCall(
297                            new \PhpParser\Node\Name('file_exists'),
298                            [
299                                new \PhpParser\Node\Arg($path)
300                            ],
301                        ),
302                        [
303                            'stmts' => [
304                                $requireOnceStraussAutoload
305                            ],
306                        ]
307                    );
308
309                    // Add a blank line. Probably not the correct way to do this.
310                    $node->setAttribute('comments', [new \PhpParser\Comment('')]);
311                    $ifFileExistsRequireOnceStraussAutoload->setAttribute('comments', [new \PhpParser\Comment('')]);
312
313                    $this->added = true;
314
315                    return [
316                        $ifFileExistsRequireOnceStraussAutoload,
317                        $node
318                    ];
319                }
320                return $node;
321            }
322        });
323
324        $modifiedStmts = $traverser->traverse($ast);
325
326        $prettyPrinter = new Standard();
327
328        return $prettyPrinter->prettyPrintFile($modifiedStmts);
329    }
330}