Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.40% covered (danger)
8.40%
10 / 119
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
VendorComposerAutoload
8.40% covered (danger)
8.40%
10 / 119
0.00% covered (danger)
0.00%
0 / 7
466.65
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
 addVendorPrefixedAutoloadToVendorAutoload
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 addAliasesFileToComposer
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
6.75
 isComposerInstalled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isComposerNoDev
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 addAliasesFileToComposerAutoload
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 addVendorPrefixedAutoloadToComposerAutoload
0.00% covered (danger)
0.00%
0 / 46
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\AutoloadConfigInterace;
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 AutoloadConfigInterace $config;
27
28    public function __construct(
29        AutoloadConfigInterace $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        $autoloadPhpFilepath = $this->config->getVendorDirectory() . 'autoload.php';
41
42        if (!$this->fileSystem->fileExists($autoloadPhpFilepath)) {
43            $this->logger->info("No autoload.php found:" . $autoloadPhpFilepath);
44            return;
45        }
46
47        $this->logger->info('Modifying original autoload.php to add `' . $this->config->getTargetDirectory() . '/autoload.php`');
48
49        $composerAutoloadPhpFileString = $this->fileSystem->read($autoloadPhpFilepath);
50
51        $newComposerAutoloadPhpFileString = $this->addVendorPrefixedAutoloadToComposerAutoload($composerAutoloadPhpFileString);
52
53        if ($newComposerAutoloadPhpFileString !== $composerAutoloadPhpFileString) {
54            $this->logger->info('Writing new autoload.php');
55            $this->fileSystem->write($autoloadPhpFilepath, $newComposerAutoloadPhpFileString);
56        } else {
57            $this->logger->debug('No changes to autoload.php');
58        }
59    }
60
61    /**
62     * Given the PHP code string for `vendor/autoload.php`, add a `require_once autoload_aliases.php`
63     * before require autoload_real.php.
64     */
65    public function addAliasesFileToComposer(): void
66    {
67        if ($this->isComposerInstalled()) {
68            $this->logger->info("Strauss installed via Composer, no need to add `autoload_aliases.php` to `vendor/autoload.php`");
69            return;
70        }
71
72        $autoloadPhpFilepath = $this->config->getVendorDirectory() . 'autoload.php';
73
74        if (!$this->fileSystem->fileExists($autoloadPhpFilepath)) {
75            $this->logger->info("No autoload.php found:" . $autoloadPhpFilepath);
76            return;
77        }
78
79        if ($this->isComposerNoDev()) {
80            $this->logger->info("Composer was run with `--no-dev`, no need to add `autoload_aliases.php` to `vendor/autoload.php`");
81            return;
82        }
83
84        $this->logger->info('Modifying original autoload.php to add autoload_aliases.php in ' . $this->config->getVendorDirectory());
85
86        $composerAutoloadPhpFileString = $this->fileSystem->read($autoloadPhpFilepath);
87
88        $newComposerAutoloadPhpFileString = $this->addAliasesFileToComposerAutoload($composerAutoloadPhpFileString);
89
90        if ($newComposerAutoloadPhpFileString !== $composerAutoloadPhpFileString) {
91            $this->logger->info('Writing new autoload.php');
92            $this->fileSystem->write($autoloadPhpFilepath, $newComposerAutoloadPhpFileString);
93        } else {
94            $this->logger->debug('No changes to autoload.php');
95        }
96    }
97
98    /**
99     * Determine is Strauss installed via Composer (otherwise presumably run via phar).
100     */
101    protected function isComposerInstalled(): bool
102    {
103        if (!$this->fileSystem->fileExists($this->config->getVendorDirectory() . 'composer/installed.json')) {
104            return false;
105        }
106
107        $installedJsonArray = json_decode($this->fileSystem->read($this->config->getVendorDirectory() . 'composer/installed.json'), true);
108
109        return isset($installedJsonArray['dev-package-names']['brianhenryie/strauss']);
110    }
111
112    /**
113     * Read `vendor/composer/installed.json` to determine if the composer was run with `--no-dev`.
114     *
115     * {
116     *   "packages": [],
117     *   "dev": true,
118     *   "dev-package-names": []
119     * }
120     */
121    protected function isComposerNoDev(): bool
122    {
123        $installedJson = $this->fileSystem->read($this->config->getVendorDirectory() . 'composer/installed.json');
124        $installedJsonArray = json_decode($installedJson, true);
125        return !$installedJsonArray['dev'];
126    }
127
128    /**
129     * This is a very over-engineered way to do a string replace.
130     *
131     * `require_once __DIR__ . '/composer/autoload_aliases.php';`
132     */
133    protected function addAliasesFileToComposerAutoload(string $code): string
134    {
135        if (false !== strpos($code, '/composer/autoload_aliases.php')) {
136            $this->logger->info('vendor/autoload.php already includes autoload_aliases.php');
137            return $code;
138        }
139
140        $parser = (new ParserFactory())->createForNewestSupportedVersion();
141        try {
142            $ast = $parser->parse($code);
143        } catch (Error $error) {
144            $this->logger->error("Parse error: {$error->getMessage()}");
145            return $code;
146        }
147
148        $traverser = new NodeTraverser();
149        $traverser->addVisitor(new class() extends NodeVisitorAbstract {
150
151            public function leaveNode(Node $node)
152            {
153                if (get_class($node) === \PhpParser\Node\Stmt\Expression::class) {
154                    $prettyPrinter = new Standard();
155                    $maybeRequireAutoloadReal = $prettyPrinter->prettyPrintExpr($node->expr);
156
157                    // Every `vendor/autoload.php` should have this line.
158                    $target = "require_once __DIR__ . '/composer/autoload_real.php'";
159
160                    // If this node isn't the one we want to insert before, continue.
161                    if ($maybeRequireAutoloadReal !== $target) {
162                        return $node;
163                    }
164
165                    $requireOnceAutoloadAliases = new Node\Stmt\Expression(
166                        new \PhpParser\Node\Expr\Include_(
167                            new \PhpParser\Node\Expr\BinaryOp\Concat(
168                                new \PhpParser\Node\Scalar\MagicConst\Dir(),
169                                new \PhpParser\Node\Scalar\String_('/composer/autoload_aliases.php')
170                            ),
171                            \PhpParser\Node\Expr\Include_::TYPE_REQUIRE_ONCE
172                        )
173                    );
174
175                    // Add a blank line. Probably not the correct way to do this.
176                    $node->setAttribute('comments', [new \PhpParser\Comment('')]);
177
178                    return [
179                        $requireOnceAutoloadAliases,
180                        $node
181                    ];
182                }
183                return $node;
184            }
185        });
186
187        $modifiedStmts = $traverser->traverse($ast);
188
189        $prettyPrinter = new Standard();
190
191        return $prettyPrinter->prettyPrintFile($modifiedStmts);
192    }
193
194    /**
195     * `require_once __DIR__ . '/../vendor-prefixed/autoload.php';`
196     */
197    protected function addVendorPrefixedAutoloadToComposerAutoload(string $code): string
198    {
199        if ($this->config->getTargetDirectory() === $this->config->getVendorDirectory()) {
200            $this->logger->info('Vendor directory is target directory, no autoloader to add.');
201            return $code;
202        }
203
204        $targetDirAutoload = '/' . $this->fileSystem->getRelativePath($this->config->getVendorDirectory(), $this->config->getTargetDirectory()) . '/autoload.php';
205
206        if (false !== strpos($code, $targetDirAutoload)) {
207            $this->logger->info('vendor/autoload.php already includes ' . $targetDirAutoload);
208            return $code;
209        }
210
211        $parser = (new ParserFactory())->createForNewestSupportedVersion();
212        try {
213            $ast = $parser->parse($code);
214        } catch (Error $error) {
215            $this->logger->error("Parse error: {$error->getMessage()}");
216            return $code;
217        }
218
219        $traverser = new NodeTraverser();
220        $traverser->addVisitor(new class($targetDirAutoload) extends NodeVisitorAbstract {
221
222            protected bool $added = false;
223            protected ?string $targetDirectoryAutoload;
224            public function __construct(?string $targetDirectoryAutoload)
225            {
226                $this->targetDirectoryAutoload = $targetDirectoryAutoload;
227            }
228
229            public function leaveNode(Node $node)
230            {
231                if ($this->added) {
232                    return $node;
233                }
234
235                if (get_class($node) === \PhpParser\Node\Stmt\Expression::class) {
236                    $prettyPrinter = new Standard();
237                    $nodeText = $prettyPrinter->prettyPrintExpr($node->expr);
238
239                    $targets = [
240                        "require_once __DIR__ . '/composer/autoload_real.php'",
241                        "require_once __DIR__ . '/composer/autoload_aliases.php'"
242                    ];
243
244                    if (!in_array($nodeText, $targets)) {
245                        return $node;
246                    }
247
248                    $requireOnceStraussAutoload = new Node\Stmt\Expression(
249                        new Node\Expr\Include_(
250                            new \PhpParser\Node\Expr\BinaryOp\Concat(
251                                new \PhpParser\Node\Scalar\MagicConst\Dir(),
252                                new Node\Scalar\String_($this->targetDirectoryAutoload)
253                            ),
254                            Node\Expr\Include_::TYPE_REQUIRE_ONCE
255                        )
256                    );
257
258                    // Add a blank line. Probably not the correct way to do this.
259                    $requireOnceStraussAutoload->setAttribute('comments', [new \PhpParser\Comment('')]);
260
261                    $this->added = true;
262
263                    return [
264                        $requireOnceStraussAutoload,
265                        $node
266                    ];
267                }
268                return $node;
269            }
270        });
271
272        $modifiedStmts = $traverser->traverse($ast);
273
274        $prettyPrinter = new Standard();
275
276        return $prettyPrinter->prettyPrintFile($modifiedStmts);
277    }
278}