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