Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.50% covered (danger)
6.50%
8 / 123
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cleanup
6.50% covered (danger)
6.50%
8 / 123
25.00% covered (danger)
25.00%
2 / 8
1036.18
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
3
 deleteFiles
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 cleanupVendorInstalledJson
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 rebuildVendorAutoloader
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
6
 isOptimizeAutoloaderEnabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 deleteEmptyDirectories
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
 doIsDeleteVendorPackages
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 doIsDeleteVendorFiles
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Deletes source files and empty directories.
4 */
5
6namespace BrianHenryIE\Strauss\Pipeline\Cleanup;
7
8use BrianHenryIE\Strauss\Composer\ComposerPackage;
9use BrianHenryIE\Strauss\Config\CleanupConfigInterface;
10use BrianHenryIE\Strauss\Config\OptimizeAutoloaderConfigInterface;
11use BrianHenryIE\Strauss\Files\DiscoveredFiles;
12use BrianHenryIE\Strauss\Files\FileBase;
13use BrianHenryIE\Strauss\Helpers\FileSystem;
14use BrianHenryIE\Strauss\Pipeline\Autoload\DumpAutoload;
15use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
16use Composer\Autoload\AutoloadGenerator;
17use Composer\Factory;
18use Composer\IO\NullIO;
19use Composer\Json\JsonFile;
20use Composer\Repository\InstalledFilesystemRepository;
21use Exception;
22use League\Flysystem\FilesystemException;
23use Psr\Log\LoggerAwareTrait;
24use Psr\Log\LoggerInterface;
25use Seld\JsonLint\ParsingException;
26
27/**
28 * @phpstan-import-type InstalledJsonArray from InstalledJson
29 */
30class Cleanup
31{
32    use LoggerAwareTrait;
33
34    protected Filesystem $filesystem;
35
36    protected bool $isDeleteVendorFiles;
37    protected bool $isDeleteVendorPackages;
38
39    protected CleanupConfigInterface $config;
40
41    public function __construct(
42        CleanupConfigInterface $config,
43        Filesystem $filesystem,
44        LoggerInterface $logger
45    ) {
46        $this->config = $config;
47        $this->logger = $logger;
48
49        $this->isDeleteVendorFiles = $config->isDeleteVendorFiles() && $config->getAbsoluteTargetDirectory() !== $config->getAbsoluteVendorDirectory();
50        $this->isDeleteVendorPackages = $config->isDeleteVendorPackages() && $config->getAbsoluteTargetDirectory() !== $config->getAbsoluteVendorDirectory();
51
52        $this->filesystem = $filesystem;
53    }
54
55    /**
56     * Maybe delete the source files that were copied (depending on config),
57     * then delete empty directories.
58     *
59     * @param array<string,ComposerPackage> $flatDependencyTree
60     *
61     * @throws FilesystemException
62     */
63    public function deleteFiles(array $flatDependencyTree, DiscoveredFiles $discoveredFiles): void
64    {
65        if (!$this->isDeleteVendorPackages && !$this->isDeleteVendorFiles) {
66            $this->logger->info('No cleanup required.');
67            return;
68        }
69
70        $this->logger->info('Beginning cleanup.');
71
72        if ($this->isDeleteVendorPackages) {
73            $this->doIsDeleteVendorPackages($flatDependencyTree, $discoveredFiles);
74        }
75
76        if ($this->isDeleteVendorFiles) {
77            $this->doIsDeleteVendorFiles($discoveredFiles->getFiles());
78        }
79
80        $this->deleteEmptyDirectories($discoveredFiles->getFiles());
81    }
82
83    /** @param array<string,ComposerPackage> $flatDependencyTree
84     * @throws Exception
85     * @throws FilesystemException
86     */
87    public function cleanupVendorInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
88    {
89        $installedJson = new InstalledJson(
90            $this->config,
91            $this->filesystem,
92            $this->logger
93        );
94
95        if (!$this->config->isTargetDirectoryVendor()
96            && !$this->config->isDeleteVendorFiles()
97            && !$this->config->isDeleteVendorPackages()
98        ) {
99            $installedJson->cleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols);
100        } elseif (!$this->config->isTargetDirectoryVendor()
101            && ($this->config->isDeleteVendorFiles() || $this->config->isDeleteVendorPackages())
102        ) {
103            $installedJson->cleanTargetDirInstalledJson($flatDependencyTree, $discoveredSymbols);
104            $installedJson->cleanupVendorInstalledJson($flatDependencyTree, $discoveredSymbols);
105        } elseif ($this->config->isTargetDirectoryVendor()) {
106            $installedJson->cleanupVendorInstalledJson($flatDependencyTree, $discoveredSymbols);
107        }
108    }
109
110    /**
111     * After packages or files have been deleted, the autoloader still contains references to them, in particular
112     * `files` are `require`d on boot (whereas classes are on demand) so that must be fixed.
113     *
114     * Assumes {@see Cleanup::cleanupVendorInstalledJson()} has been called first.
115     *
116     * TODO refactor so this object is passed around rather than reloaded.
117     *
118     * Shares a lot of code with {@see DumpAutoload::generatedPrefixedAutoloader()} but I've done lots of work
119     * on that in another branch so I don't want to cause merge conflicts.
120     * @throws ParsingException
121     */
122    public function rebuildVendorAutoloader(): void
123    {
124        if ($this->config->isDryRun()) {
125            return;
126        }
127
128        $projectComposerJson = new JsonFile(
129            $this->filesystem->makeAbsolute(
130                $this->config->getProjectAbsolutePath() . '/composer.json' // Factory::getComposerFile();
131            )
132        );
133        $projectComposerJsonArray = $projectComposerJson->read();
134        $composer = Factory::create(new NullIO(), $projectComposerJsonArray);
135        $installationManager = $composer->getInstallationManager();
136        $package = $composer->getPackage();
137        $config = $composer->getConfig();
138        $generator = new AutoloadGenerator($composer->getEventDispatcher());
139        $isOptimize = $this->isOptimizeAutoloaderEnabled();
140        $generator->setClassMapAuthoritative($isOptimize);
141        $generator->setRunScripts(false);
142//        $generator->setApcu($apcu, $apcuPrefix);
143//        $generator->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input));
144        $installedJson = new JsonFile(
145            $this->filesystem->makeAbsolute(
146                $this->config->getAbsoluteVendorDirectory() . '/composer/installed.json'
147            )
148        );
149        $localRepo = new InstalledFilesystemRepository($installedJson);
150        $strictAmbiguous = false; // $input->getOption('strict-ambiguous')
151        /** @var InstalledJsonArray $installedJsonArray */
152        $installedJsonArray = $installedJson->read();
153        $generator->setDevMode($installedJsonArray['dev'] ?? false);
154
155        // This will output the autoload_static.php etc. files to `vendor/composer`.
156        $generator->dump(
157            $config,
158            $localRepo,
159            $package,
160            $installationManager,
161            'composer',
162            $isOptimize,
163            null,
164            $composer->getLocker(),
165            $strictAmbiguous
166        );
167    }
168
169    /**
170     * Keep backward compatibility with configs implementing only CleanupConfigInterface.
171     */
172    protected function isOptimizeAutoloaderEnabled(): bool
173    {
174        return $this->config instanceof OptimizeAutoloaderConfigInterface
175            ? $this->config->isOptimizeAutoloader()
176            : true;
177    }
178
179    /**
180     * @param FileBase[] $files
181     * @throws FilesystemException
182     */
183    protected function deleteEmptyDirectories(array $files): void
184    {
185        $this->logger->info('Deleting empty directories.');
186
187        $sourceFiles = array_map(
188            fn($file) => $file->getSourcePath(),
189            $files
190        );
191
192        // Get the root folders of the moved files.
193        $rootSourceDirectories = [];
194        foreach ($sourceFiles as $sourceFile) {
195            $arr = explode("/", $sourceFile, 2);
196            $dir = $arr[0];
197            $rootSourceDirectories[ $dir ] = $dir;
198        }
199        $rootSourceDirectories = array_map(
200            function (string $path): string {
201                return $this->config->getAbsoluteVendorDirectory() . '/' . $path;
202            },
203            array_keys($rootSourceDirectories)
204        );
205
206        foreach ($rootSourceDirectories as $rootSourceDirectory) {
207            if (!$this->filesystem->directoryExists($rootSourceDirectory) || is_link($rootSourceDirectory)) {
208                continue;
209            }
210
211            $dirList = $this->filesystem->listContents($rootSourceDirectory, true);
212
213            $allFilePaths = array_map(
214                fn($file) => $file->path(),
215                $dirList->toArray()
216            );
217
218            // Sort by longest path first, so subdirectories are deleted before the parent directories are checked.
219            usort(
220                $allFilePaths,
221                fn($a, $b) => count(explode('/', $b)) - count(explode('/', $a))
222            );
223
224            foreach ($allFilePaths as $filePath) {
225                if ($this->filesystem->directoryExists($filePath)
226                    && $this->filesystem->isDirectoryEmpty($filePath)
227                ) {
228                    $this->logger->debug('Deleting empty directory ' . $filePath);
229                    $this->filesystem->deleteDirectory($filePath);
230                }
231            }
232        }
233
234//        foreach ($this->filesystem->listContents($this->getAbsoluteVendorDir()) as $dirEntry) {
235//            if ($dirEntry->isDir() && $this->dirIsEmpty($dirEntry->path()) && !is_link($dirEntry->path())) {
236//                $this->logger->info('Deleting empty directory ' .  $dirEntry->path());
237//                $this->filesystem->deleteDirectory($dirEntry->path());
238//            } else {
239//                $this->logger->debug('Skipping non-empty directory ' . $dirEntry->path());
240//            }
241//        }
242        $this->logger->debug('Finished Cleanup::deleteEmptyDirectories()');
243    }
244
245    /**
246     * @param array<string,ComposerPackage> $flatDependencyTree
247     * @throws FilesystemException
248     */
249    protected function doIsDeleteVendorPackages(array $flatDependencyTree, DiscoveredFiles $discoveredFiles): void
250    {
251        $this->logger->info('Deleting original vendor packages.');
252
253//        if ($this->isDeleteVendorPackages) {
254//            foreach ($flatDependencyTree as $packageName => $package) {
255//                if ($package->isDoDelete()) {
256//                    $this->filesystem->deleteDirectory($package->getPackageAbsolutePath());
257//                    $package->setDidDelete(true);
258////                $files = $package->getFiles();
259////                foreach($files as $file){
260////                    $file->setDidDelete(true);
261////                }
262//                }
263//            }
264//        }
265
266        foreach ($flatDependencyTree as $package) {
267            // Skip packages excluded from copy - they should remain in vendor/
268            if (in_array($package->getPackageName(), $this->config->getExcludePackagesFromCopy(), true)) {
269                $this->logger->debug('Skipping deletion of excluded package: ' . $package->getPackageName());
270                continue;
271            }
272
273            // Normal package.
274            $this->logger->info('Deleting ' . $package->getPackageAbsolutePath());
275
276            $this->filesystem->deleteDirectory($package->getPackageAbsolutePath());
277
278            $package->setDidDelete(true);
279
280            $packageParentDir = dirname($package->getPackageAbsolutePath());
281            if ($this->filesystem->isDirectoryEmpty($packageParentDir)) {
282                $this->logger->info('Deleting empty directory ' . $packageParentDir);
283                $this->filesystem->deleteDirectory($packageParentDir);
284            }
285        }
286    }
287
288    /**
289     * @param FileBase[] $files
290     *
291     * @throws FilesystemException
292     */
293    public function doIsDeleteVendorFiles(array $files): void
294    {
295        $this->logger->info('Deleting original vendor files.');
296
297        foreach ($files as $file) {
298            if (! $file->isDoDelete()) {
299                $this->logger->debug('Skipping/preserving ' . $file->getSourcePath());
300                continue;
301            }
302
303            $sourceRelativePath = $file->getSourcePath();
304
305            $this->logger->info('Deleting ' . $sourceRelativePath);
306
307            // TODO: is this relative or absolute?
308            $this->filesystem->delete($file->getSourcePath());
309
310            $file->setDidDelete(true);
311        }
312    }
313}