Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 130 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
Cleanup | |
0.00% |
0 / 130 |
|
0.00% |
0 / 5 |
1806 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
cleanup | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
380 | |||
dirIsEmpty | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
cleanupInstalledJson | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
132 | |||
cleanupFilesAutoloader | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | /** |
3 | * Deletes source files and empty directories. |
4 | */ |
5 | |
6 | namespace BrianHenryIE\Strauss; |
7 | |
8 | use BrianHenryIE\Strauss\Composer\Extra\StraussConfig; |
9 | use Composer\Json\JsonFile; |
10 | use FilesystemIterator; |
11 | use League\Flysystem\Filesystem; |
12 | use League\Flysystem\Local\LocalFilesystemAdapter; |
13 | use RecursiveDirectoryIterator; |
14 | use RecursiveIteratorIterator; |
15 | |
16 | class Cleanup |
17 | { |
18 | |
19 | /** @var Filesystem */ |
20 | protected Filesystem $filesystem; |
21 | |
22 | protected string $workingDir; |
23 | |
24 | protected bool $isDeleteVendorFiles; |
25 | protected bool $isDeleteVendorPackages; |
26 | |
27 | protected string $vendorDirectory = 'vendor'. DIRECTORY_SEPARATOR; |
28 | protected string $targetDirectory; |
29 | |
30 | public function __construct(StraussConfig $config, string $workingDir) |
31 | { |
32 | $this->vendorDirectory = $config->getVendorDirectory(); |
33 | $this->targetDirectory = $config->getTargetDirectory(); |
34 | $this->workingDir = $workingDir; |
35 | |
36 | $this->isDeleteVendorFiles = $config->isDeleteVendorFiles() && $config->getTargetDirectory() !== $config->getVendorDirectory(); |
37 | $this->isDeleteVendorPackages = $config->isDeleteVendorPackages() && $config->getTargetDirectory() !== $config->getVendorDirectory(); |
38 | |
39 | $this->filesystem = new Filesystem(new LocalFilesystemAdapter($workingDir)); |
40 | } |
41 | |
42 | /** |
43 | * Maybe delete the source files that were copied (depending on config), |
44 | * then delete empty directories. |
45 | * |
46 | * @param string[] $sourceFiles Relative filepaths. |
47 | */ |
48 | public function cleanup(array $sourceFiles): void |
49 | { |
50 | if (!$this->isDeleteVendorPackages && !$this->isDeleteVendorFiles) { |
51 | return; |
52 | } |
53 | |
54 | if ($this->isDeleteVendorPackages) { |
55 | $package_dirs = array_unique(array_map(function (string $relativeFilePath): string { |
56 | list( $vendor, $package ) = explode('/', $relativeFilePath); |
57 | return "{$vendor}/{$package}"; |
58 | }, $sourceFiles)); |
59 | |
60 | foreach ($package_dirs as $package_dir) { |
61 | $relativeDirectoryPath = $this->vendorDirectory . $package_dir; |
62 | |
63 | $absolutePath = $this->workingDir . $relativeDirectoryPath; |
64 | |
65 | // Fix for Windows paths. |
66 | $absolutePath = str_replace(['\\','/'], DIRECTORY_SEPARATOR, $absolutePath); |
67 | |
68 | if (is_link($absolutePath)) { |
69 | unlink($absolutePath); |
70 | } elseif (false !== strpos('WIN', PHP_OS) |
71 | && stat($absolutePath)['nlink'] !== lstat($absolutePath)['nlink'] |
72 | ) { |
73 | /** |
74 | * `unlink()` will not work on Windows. `rmdir()` will not work if there are files in the directory. |
75 | * "On windows, take care that `is_link()` returns false for Junctions." |
76 | * |
77 | * @see https://www.php.net/manual/en/function.is-link.php#113263 |
78 | * @see https://stackoverflow.com/a/18262809/336146 |
79 | */ |
80 | rmdir($absolutePath); |
81 | } |
82 | |
83 | if ($absolutePath !== realpath($absolutePath)) { |
84 | continue; |
85 | } |
86 | |
87 | $this->filesystem->deleteDirectory($relativeDirectoryPath); |
88 | } |
89 | } elseif ($this->isDeleteVendorFiles) { |
90 | foreach ($sourceFiles as $sourceFile) { |
91 | $relativeFilepath = $this->vendorDirectory . $sourceFile; |
92 | |
93 | $absolutePath = $this->workingDir . $relativeFilepath; |
94 | |
95 | if ($absolutePath !== realpath($absolutePath)) { |
96 | continue; |
97 | } |
98 | |
99 | $this->filesystem->delete($relativeFilepath); |
100 | } |
101 | |
102 | $this->cleanupFilesAutoloader(); |
103 | } |
104 | |
105 | // Get the root folders of the moved files. |
106 | $rootSourceDirectories = []; |
107 | foreach ($sourceFiles as $sourceFile) { |
108 | $arr = explode("/", $sourceFile, 2); |
109 | $dir = $arr[0]; |
110 | $rootSourceDirectories[ $dir ] = $dir; |
111 | } |
112 | $rootSourceDirectories = array_map( |
113 | function (string $path): string { |
114 | return $this->vendorDirectory . $path; |
115 | }, |
116 | array_keys($rootSourceDirectories) |
117 | ); |
118 | |
119 | foreach ($rootSourceDirectories as $rootSourceDirectory) { |
120 | if (!is_dir($rootSourceDirectory) || is_link($rootSourceDirectory)) { |
121 | continue; |
122 | } |
123 | |
124 | $it = new RecursiveIteratorIterator( |
125 | new RecursiveDirectoryIterator( |
126 | $this->workingDir . $rootSourceDirectory, |
127 | FilesystemIterator::SKIP_DOTS |
128 | ), |
129 | RecursiveIteratorIterator::CHILD_FIRST |
130 | ); |
131 | |
132 | foreach ($it as $file) { |
133 | if ($file->isDir() && $this->dirIsEmpty((string) $file)) { |
134 | rmdir((string)$file); |
135 | } |
136 | } |
137 | } |
138 | |
139 | $this->cleanupInstalledJson(); |
140 | } |
141 | |
142 | // TODO: Use Symfony or Flysystem functions. |
143 | protected function dirIsEmpty(string $dir): bool |
144 | { |
145 | $di = new RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS); |
146 | return iterator_count($di) === 0; |
147 | } |
148 | |
149 | /** |
150 | * Composer creates a file `vendor/composer/installed.json` which is uses when running `composer dump-autoload`. |
151 | * When `delete-vendor-packages` or `delete-vendor-files` is true, files and directories which have been deleted |
152 | * must also be removed from `installed.json` or Composer will throw an error. |
153 | * |
154 | * TODO: {@see self::cleanupFilesAutoloader()} might be redundant if we run this function and then run `composer dump-autoload`. |
155 | */ |
156 | public function cleanupInstalledJson(): void |
157 | { |
158 | $installedJsonFile = new JsonFile($this->workingDir . '/vendor/composer/installed.json'); |
159 | if (!$installedJsonFile->exists()) { |
160 | return; |
161 | } |
162 | $installedJsonArray = $installedJsonFile->read(); |
163 | |
164 | foreach ($installedJsonArray['packages'] as $key => $package) { |
165 | if (!isset($package['autoload'])) { |
166 | continue; |
167 | } |
168 | $packageDir = $this->workingDir . $this->vendorDirectory . ltrim($package['install-path'], '.' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
169 | if (!is_dir($packageDir)) { |
170 | // pcre, xdebug-handler. |
171 | continue; |
172 | } |
173 | $autoload_key = $package['autoload']; |
174 | foreach ($autoload_key as $type => $autoload) { |
175 | switch ($type) { |
176 | case 'psr-4': |
177 | foreach ($autoload_key[$type] as $namespace => $dirs) { |
178 | if (is_array($dirs)) { |
179 | $autoload_key[$type][$namespace] = array_filter($dirs, function ($dir) use ($packageDir) { |
180 | $dir = $packageDir . $dir; |
181 | return is_readable($dir); |
182 | }); |
183 | } else { |
184 | $dir = $packageDir . $dirs; |
185 | if (! is_readable($dir)) { |
186 | unset($autoload_key[$type][$namespace]); |
187 | } |
188 | } |
189 | } |
190 | break; |
191 | default: // files, classmap |
192 | $autoload_key[$type] = array_filter($autoload, function ($file) use ($packageDir) { |
193 | $filename = $packageDir . $file; |
194 | return file_exists($packageDir . $file); |
195 | }); |
196 | break; |
197 | } |
198 | } |
199 | $installedJsonArray['packages'][$key]['autoload'] = array_filter($autoload_key); |
200 | } |
201 | $installedJsonFile->write($installedJsonArray); |
202 | } |
203 | |
204 | /** |
205 | * After files are deleted, remove them from the Composer files autoloaders. |
206 | * |
207 | * @see https://github.com/BrianHenryIE/strauss/issues/34#issuecomment-922503813 |
208 | */ |
209 | protected function cleanupFilesAutoloader(): void |
210 | { |
211 | if (! file_exists($this->workingDir . 'vendor/composer/autoload_files.php')) { |
212 | return; |
213 | } |
214 | |
215 | $files = include $this->workingDir . 'vendor/composer/autoload_files.php'; |
216 | |
217 | $missingFiles = array(); |
218 | |
219 | foreach ($files as $file) { |
220 | if (! file_exists($file)) { |
221 | $missingFiles[] = str_replace([ $this->workingDir, 'vendor/composer/../', 'vendor/' ], '', $file); |
222 | // When `composer install --no-dev` is run, it creates an index of files autoload files which |
223 | // references the non-existant files. This causes a fatal error when the autoloader is included. |
224 | // TODO: if delete_vendor_packages is true, do not create this file. |
225 | $this->filesystem->write( |
226 | str_replace($this->workingDir, '', $file), |
227 | '<?php // This file was deleted by {@see https://github.com/BrianHenryIE/strauss}.' |
228 | ); |
229 | } |
230 | } |
231 | |
232 | if (empty($missingFiles)) { |
233 | return; |
234 | } |
235 | |
236 | $targetDirectory = $this->targetDirectory; |
237 | |
238 | foreach (array('autoload_static.php', 'autoload_files.php') as $autoloadFile) { |
239 | $autoloadStaticPhp = $this->filesystem->read('vendor/composer/'.$autoloadFile); |
240 | |
241 | $autoloadStaticPhpAsArray = explode(PHP_EOL, $autoloadStaticPhp); |
242 | |
243 | $newAutoloadStaticPhpAsArray = array_map( |
244 | function (string $line) use ($missingFiles, $targetDirectory): string { |
245 | $containsFile = array_reduce( |
246 | $missingFiles, |
247 | function (bool $carry, string $filepath) use ($line): bool { |
248 | return $carry || false !== strpos($line, $filepath); |
249 | }, |
250 | false |
251 | ); |
252 | |
253 | if (!$containsFile) { |
254 | return $line; |
255 | } |
256 | |
257 | // TODO: Check the file does exist at the new location. It definitely should be. |
258 | // TODO: If the Strauss autoloader is being created, just return an empty string here. |
259 | |
260 | return str_replace([ |
261 | "=> __DIR__ . '/..' . '/", |
262 | "=> \$vendorDir . '/" |
263 | ], [ |
264 | "=> __DIR__ . '/../../$targetDirectory' . '/", |
265 | "=> \$baseDir . '/$targetDirectory" |
266 | ], $line); |
267 | }, |
268 | $autoloadStaticPhpAsArray |
269 | ); |
270 | |
271 | $newAutoloadStaticPhp = implode(PHP_EOL, $newAutoloadStaticPhpAsArray); |
272 | |
273 | $this->filesystem->write('vendor/composer/'.$autoloadFile, $newAutoloadStaticPhp); |
274 | } |
275 | } |
276 | } |