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