Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
16.22% covered (danger)
16.22%
18 / 111
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
DumpAutoload
16.22% covered (danger)
16.22%
18 / 111
16.67% covered (danger)
16.67%
1 / 6
112.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 generatedPrefixedAutoloader
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 generatedMainAutoloader
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
30
 createInstalledVersionsFiles
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
2.00
 prefixNewAutoloader
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
6
 getSuffix
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BrianHenryIE\Strauss\Pipeline\Autoload;
4
5use BrianHenryIE\Strauss\Files\File;
6use BrianHenryIE\Strauss\Config\AutoloadConfigInterface;
7use BrianHenryIE\Strauss\Helpers\FileSystem;
8use BrianHenryIE\Strauss\Pipeline\FileEnumerator;
9use BrianHenryIE\Strauss\Pipeline\Prefixer;
10use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
11use BrianHenryIE\Strauss\Types\NamespaceSymbol;
12use Composer\Autoload\AutoloadGenerator;
13use Composer\Config;
14use Composer\Factory;
15use Composer\IO\NullIO;
16use Composer\Json\JsonFile;
17use Composer\Repository\InstalledFilesystemRepository;
18use Psr\Log\LoggerAwareTrait;
19use Psr\Log\LoggerInterface;
20use Psr\Log\NullLogger;
21
22class DumpAutoload
23{
24    use LoggerAwareTrait;
25
26    protected AutoloadConfigInterface $config;
27
28    protected FileSystem $filesystem;
29    
30    protected Prefixer $projectReplace;
31
32    protected FileEnumerator $fileEnumerator;
33
34    public function __construct(
35        AutoloadConfigInterface $config,
36        Filesystem $filesystem,
37        LoggerInterface $logger,
38        Prefixer $projectReplace,
39        FileEnumerator $fileEnumerator
40    ) {
41        $this->config = $config;
42        $this->filesystem = $filesystem;
43        $this->setLogger($logger ?? new NullLogger());
44
45        $this->projectReplace = $projectReplace;
46
47        $this->fileEnumerator = $fileEnumerator;
48    }
49
50    /**
51     * Create `autoload.php` and the `vendor-prefixed/composer` directory.
52     */
53    public function generatedPrefixedAutoloader(): void
54    {
55        $this->generatedMainAutoloader();
56
57        $this->createInstalledVersionsFiles();
58
59        $this->prefixNewAutoloader();
60    }
61
62    /**
63     * Uses `vendor/composer/installed.json` to output autoload files to `vendor-prefixed/composer`.
64     */
65    protected function generatedMainAutoloader(): void
66    {
67        /**
68         * Unfortunately, `::dump()` creates the target directories if they don't exist, even though it otherwise respects `::setDryRun()`.
69         *
70         * {@see https://github.com/composer/composer/pull/12396} might fix this.
71         */
72        if ($this->config->isDryRun()) {
73            return;
74        }
75
76        $relativeTargetDir = $this->filesystem->getRelativePath(
77            $this->config->getProjectDirectory(),
78            $this->config->getTargetDirectory()
79        );
80
81        $defaultVendorDirBefore = Config::$defaultConfig['vendor-dir'];
82        Config::$defaultConfig['vendor-dir'] = $relativeTargetDir;
83
84        $projectComposerJson = new JsonFile($this->config->getProjectDirectory() . Factory::getComposerFile());
85        $projectComposerJsonArray = $projectComposerJson->read();
86        if (isset($projectComposerJsonArray['config'], $projectComposerJsonArray['config']['vendor-dir'])) {
87            $projectComposerJsonArray['config']['vendor-dir'] = $relativeTargetDir;
88        }
89
90        // Do not include the autoload section from the project composer.json in the vendor-prefixed autoloader.
91        if (isset($projectComposerJsonArray['autoload'])) {
92            $projectComposerJsonArray['autoload'] = [];
93        }
94
95        $composer = Factory::create(new NullIO(), $projectComposerJsonArray);
96        $installationManager = $composer->getInstallationManager();
97        $package = $composer->getPackage();
98
99        /**
100         * Cannot use `$composer->getConfig()`, need to create a new one so the vendor-dir is correct.
101         */
102        $config = new \Composer\Config(false, $this->config->getProjectDirectory());
103
104        $config->merge([
105            'config' => $projectComposerJsonArray['config'] ?? []
106        ]);
107
108        $generator = new ComposerAutoloadGenerator(
109            $this->config->getNamespacePrefix(),
110            $composer->getEventDispatcher()
111        );
112
113        $generator->setDryRun($this->config->isDryRun());
114        $generator->setClassMapAuthoritative(true);
115        $generator->setRunScripts(false);
116//        $generator->setApcu($apcu, $apcuPrefix);
117//        $generator->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input));
118        $optimize = true; // $input->getOption('optimize') || $config->get('optimize-autoloader');
119
120        $installedJsonFile = new JsonFile($this->config->getTargetDirectory() . 'composer/installed.json');
121        $installedJson = $installedJsonFile->read();
122        $localRepo = new InstalledFilesystemRepository($installedJsonFile);
123
124        /**
125         * If the target directory is different to the vendor directory, then we do not want to include dev
126         * dependencies, but if it is vendor, then unless composer install was run with --no-dev, we do want them.
127         */
128        if ($this->config->getVendorDirectory() !== $this->config->getTargetDirectory()) {
129            $isDevMode = false;
130        } else {
131            $isDevMode = $installedJson['dev'];
132        }
133        $generator->setDevMode($isDevMode);
134
135        $strictAmbiguous = false; // $input->getOption('strict-ambiguous')
136
137        // This will output the autoload_static.php etc. files to `vendor-prefixed/composer`.
138        $generator->dump(
139            $config,
140            $localRepo,
141            $package,
142            $installationManager,
143            'composer',
144            $optimize,
145            $this->getSuffix(),
146            $composer->getLocker(),
147            $strictAmbiguous
148        );
149
150        /**
151         * Tests fail if this is absent.
152         *
153         * Arguably this should be in ::setUp() and tearDown() in the test classes, but if other tools run after Strauss
154         * then they might expect it to be unmodified.
155         */
156        Config::$defaultConfig['vendor-dir'] = $defaultVendorDirBefore;
157    }
158
159    /**
160     * Create `InstalledVersions.php` and `installed.php`.
161     *
162     * This file is copied in all Composer installations.
163     * It is added always in `ComposerAutoloadGenerator::dump()`, called above.
164     * If the file does not exist, its entry in the classmap will not be prefixed and will cause autoloading issues for the real class.
165     *
166     * The accompanying `installed.php` is unique per install. Copy it and filter its packages to the packages that was copied.
167     */
168    protected function createInstalledVersionsFiles(): void
169    {
170        if ($this->config->getVendorDirectory() === $this->config->getTargetDirectory()) {
171            return;
172        }
173
174        $this->filesystem->copy($this->config->getVendorDirectory() . '/composer/InstalledVersions.php', $this->config->getTargetDirectory() . 'composer/InstalledVersions.php');
175
176        // This is just `<?php return array(...);`
177        $installedPhpString = $this->filesystem->read($this->config->getVendorDirectory() . '/composer/installed.php');
178        $installed = eval(str_replace('<?php', '', $installedPhpString));
179
180        $targetPackages = $this->config->getPackagesToCopy();
181        $targetPackagesNames = array_keys($targetPackages);
182
183        $installed['versions'] = array_filter($installed['versions'], function ($packageName) use ($targetPackagesNames) {
184            return in_array($packageName, $targetPackagesNames);
185        }, ARRAY_FILTER_USE_KEY);
186
187        $installedArrayString = var_export($installed, true);
188
189        $newInstalledPhpString = "<?php return $installedArrayString;";
190
191        // Update `__DIR__` which was evaluated during the `include`/`eval`.
192        $newInstalledPhpString = preg_replace('/(\'install_path\' => )(.*)(\/\.\..*)/', "$1__DIR__ . '$3", $newInstalledPhpString);
193
194        $this->filesystem->write($this->config->getTargetDirectory() . '/composer/installed.php', $newInstalledPhpString);
195    }
196
197    protected function prefixNewAutoloader(): void
198    {
199        if ($this->config->getVendorDirectory() === $this->config->getTargetDirectory()) {
200            return;
201        }
202
203        $this->logger->debug('Prefixing the new Composer autoloader.');
204
205        $projectFiles = $this->fileEnumerator->compileFileListForPaths([
206            $this->config->getTargetDirectory() . 'composer',
207        ]);
208
209        $phpFiles = array_filter(
210            $projectFiles->getFiles(),
211            fn($file) => $file->isPhpFile()
212        );
213
214        $phpFilesAbsolutePaths = array_map(
215            fn($file) => $file->getSourcePath(),
216            $phpFiles
217        );
218
219        $sourceFile = new File(__DIR__);
220        $composerAutoloadNamespaceSymbol = new NamespaceSymbol(
221            'Composer\\Autoload',
222            $sourceFile
223        );
224        $composerAutoloadNamespaceSymbol->setReplacement(
225            $this->config->getNamespacePrefix() . '\\Composer\\Autoload'
226        );
227        $composerNamespaceSymbol = new NamespaceSymbol(
228            'Composer',
229            $sourceFile
230        );
231        $composerNamespaceSymbol->setReplacement(
232            $this->config->getNamespacePrefix() . '\\Composer'
233        );
234
235        $discoveredSymbols = new DiscoveredSymbols();
236        $discoveredSymbols->add(
237            $composerNamespaceSymbol
238        );
239        $discoveredSymbols->add(
240            $composerAutoloadNamespaceSymbol
241        );
242
243        $this->projectReplace->replaceInProjectFiles($discoveredSymbols, $phpFilesAbsolutePaths);
244    }
245
246    /**
247     * If there is an existing autoloader, it will use the same suffix. If there is not, it pulls the suffix from
248     * {Composer::getLocker()} and clashes with the existing autoloader.
249     *
250     * @see AutoloadGenerator::dump() 412:431
251     * @see https://github.com/composer/composer/blob/ae208dc1e182bd45d99fcecb956501da212454a1/src/Composer/Autoload/AutoloadGenerator.php#L429
252     */
253    protected function getSuffix(): ?string
254    {
255        return !$this->filesystem->fileExists($this->config->getTargetDirectory() . 'autoload.php')
256            ? bin2hex(random_bytes(16))
257            : null;
258    }
259}