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