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