Skip to content

Commit 607b81f

Browse files
Copilotdermatz
andcommitted
#feature-request - Implement CleanCommand for static file cleanup
Co-authored-by: dermatz <6103201+dermatz@users.noreply.github.com>
1 parent 3fd90e6 commit 607b81f

2 files changed

Lines changed: 358 additions & 0 deletions

File tree

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenForgeProject\MageForge\Console\Command\Static;
6+
7+
use Magento\Framework\App\Filesystem\DirectoryList;
8+
use Magento\Framework\Console\Cli;
9+
use Magento\Framework\Filesystem;
10+
use OpenForgeProject\MageForge\Console\Command\AbstractCommand;
11+
use OpenForgeProject\MageForge\Model\ThemeList;
12+
use Symfony\Component\Console\Helper\Table;
13+
use Symfony\Component\Console\Input\InputArgument;
14+
use Symfony\Component\Console\Input\InputInterface;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
17+
/**
18+
* Command for cleaning Magento static files for specific themes
19+
*/
20+
class CleanCommand extends AbstractCommand
21+
{
22+
/**
23+
* @param ThemeList $themeList
24+
* @param Filesystem $filesystem
25+
*/
26+
public function __construct(
27+
private readonly ThemeList $themeList,
28+
private readonly Filesystem $filesystem
29+
) {
30+
parent::__construct();
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
protected function configure(): void
37+
{
38+
$this->setName($this->getCommandName('static', 'clean'))
39+
->setDescription('Cleans var/view_preprocessed and pub/static folders for specific theme(s)')
40+
->addArgument(
41+
'themeCodes',
42+
InputArgument::IS_ARRAY,
43+
'Theme codes to clean (format: Vendor/theme). If empty, will prompt for selection.'
44+
);
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
protected function executeCommand(InputInterface $input, OutputInterface $output): int
51+
{
52+
$themeCodes = $input->getArgument('themeCodes');
53+
54+
if (empty($themeCodes)) {
55+
$this->displayAvailableThemes();
56+
$this->io->warning('No theme specified. Please provide theme code(s) to clean.');
57+
$this->io->info('Usage: bin/magento mageforge:static:clean <theme-code> [<theme-code>...]');
58+
$this->io->info('Example: bin/magento mageforge:static:clean Magento/luma');
59+
return Cli::RETURN_SUCCESS;
60+
}
61+
62+
return $this->processCleanThemes($themeCodes);
63+
}
64+
65+
/**
66+
* Display available themes
67+
*
68+
* @return void
69+
*/
70+
private function displayAvailableThemes(): void
71+
{
72+
$themes = $this->themeList->getAllThemes();
73+
74+
if (empty($themes)) {
75+
$this->io->info('No themes found.');
76+
return;
77+
}
78+
79+
$this->io->section('Available Themes:');
80+
$table = new Table($this->io);
81+
$table->setHeaders(['Theme Code', 'Title']);
82+
83+
foreach ($themes as $theme) {
84+
$table->addRow([$theme->getCode(), $theme->getThemeTitle()]);
85+
}
86+
87+
$table->render();
88+
}
89+
90+
/**
91+
* Process cleaning themes
92+
*
93+
* @param array $themeCodes
94+
* @return int
95+
*/
96+
private function processCleanThemes(array $themeCodes): int
97+
{
98+
$startTime = microtime(true);
99+
$successList = [];
100+
$failureList = [];
101+
$totalThemes = count($themeCodes);
102+
103+
$this->io->title(sprintf('Cleaning static files for %d theme(s)', $totalThemes));
104+
105+
foreach ($themeCodes as $themeCode) {
106+
$this->io->section(sprintf('Cleaning theme: %s', $themeCode));
107+
108+
if (!$this->validateTheme($themeCode)) {
109+
$failureList[] = $themeCode;
110+
$this->io->error("Theme $themeCode is not installed.");
111+
continue;
112+
}
113+
114+
$cleaned = $this->cleanThemeFiles($themeCode);
115+
116+
if ($cleaned) {
117+
$successList[] = $themeCode;
118+
$this->io->success(sprintf('Successfully cleaned files for theme: %s', $themeCode));
119+
} else {
120+
$failureList[] = $themeCode;
121+
}
122+
}
123+
124+
$this->displayCleanSummary($successList, $failureList, microtime(true) - $startTime);
125+
126+
return Cli::RETURN_SUCCESS;
127+
}
128+
129+
/**
130+
* Validate if theme exists
131+
*
132+
* @param string $themeCode
133+
* @return bool
134+
*/
135+
private function validateTheme(string $themeCode): bool
136+
{
137+
$themes = $this->themeList->getAllThemes();
138+
139+
foreach ($themes as $theme) {
140+
if ($theme->getCode() === $themeCode) {
141+
return true;
142+
}
143+
}
144+
145+
return false;
146+
}
147+
148+
/**
149+
* Clean theme files from var/view_preprocessed and pub/static
150+
*
151+
* @param string $themeCode
152+
* @return bool
153+
*/
154+
private function cleanThemeFiles(string $themeCode): bool
155+
{
156+
$success = true;
157+
158+
// Clean var/view_preprocessed
159+
$viewPreprocessedCleaned = $this->cleanViewPreprocessed($themeCode);
160+
if (!$viewPreprocessedCleaned) {
161+
$success = false;
162+
}
163+
164+
// Clean pub/static
165+
$staticCleaned = $this->cleanPubStatic($themeCode);
166+
if (!$staticCleaned) {
167+
$success = false;
168+
}
169+
170+
return $success;
171+
}
172+
173+
/**
174+
* Clean var/view_preprocessed for theme
175+
*
176+
* @param string $themeCode
177+
* @return bool
178+
*/
179+
private function cleanViewPreprocessed(string $themeCode): bool
180+
{
181+
try {
182+
$varDir = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR);
183+
$viewPreprocessedPath = $varDir->getAbsolutePath('view_preprocessed');
184+
185+
if (!is_dir($viewPreprocessedPath)) {
186+
$this->io->info('var/view_preprocessed directory does not exist.');
187+
return true;
188+
}
189+
190+
// Convert Vendor/theme to frontend/Vendor/theme pattern
191+
$themePattern = $this->getThemePattern($themeCode);
192+
$cleaned = 0;
193+
194+
// Scan for matching theme directories
195+
$iterator = new \RecursiveIteratorIterator(
196+
new \RecursiveDirectoryIterator($viewPreprocessedPath, \RecursiveDirectoryIterator::SKIP_DOTS),
197+
\RecursiveIteratorIterator::SELF_FIRST
198+
);
199+
200+
$dirsToDelete = [];
201+
foreach ($iterator as $file) {
202+
$path = $file->getPathname();
203+
if ($file->isDir() && strpos($path, $themePattern) !== false) {
204+
$dirsToDelete[] = $path;
205+
}
206+
}
207+
208+
// Delete directories from deepest to shallowest
209+
rsort($dirsToDelete);
210+
foreach ($dirsToDelete as $dir) {
211+
if ($this->removeDirectory($dir)) {
212+
$cleaned++;
213+
}
214+
}
215+
216+
if ($cleaned > 0) {
217+
$this->io->writeln(sprintf(' ✓ Cleaned %d directories from var/view_preprocessed', $cleaned));
218+
} else {
219+
$this->io->writeln(' ℹ No files found in var/view_preprocessed');
220+
}
221+
222+
return true;
223+
} catch (\Exception $e) {
224+
$this->io->error('Failed to clean var/view_preprocessed: ' . $e->getMessage());
225+
return false;
226+
}
227+
}
228+
229+
/**
230+
* Clean pub/static for theme
231+
*
232+
* @param string $themeCode
233+
* @return bool
234+
*/
235+
private function cleanPubStatic(string $themeCode): bool
236+
{
237+
try {
238+
$staticDir = $this->filesystem->getDirectoryRead(DirectoryList::STATIC_VIEW);
239+
$staticPath = $staticDir->getAbsolutePath();
240+
241+
if (!is_dir($staticPath)) {
242+
$this->io->info('pub/static directory does not exist.');
243+
return true;
244+
}
245+
246+
// Convert Vendor/theme to frontend/Vendor/theme pattern
247+
$themePattern = $this->getThemePattern($themeCode);
248+
$cleaned = 0;
249+
250+
// Scan for matching theme directories
251+
$iterator = new \RecursiveIteratorIterator(
252+
new \RecursiveDirectoryIterator($staticPath, \RecursiveDirectoryIterator::SKIP_DOTS),
253+
\RecursiveIteratorIterator::SELF_FIRST
254+
);
255+
256+
$dirsToDelete = [];
257+
foreach ($iterator as $file) {
258+
$path = $file->getPathname();
259+
if ($file->isDir() && strpos($path, $themePattern) !== false) {
260+
$dirsToDelete[] = $path;
261+
}
262+
}
263+
264+
// Delete directories from deepest to shallowest
265+
rsort($dirsToDelete);
266+
foreach ($dirsToDelete as $dir) {
267+
if ($this->removeDirectory($dir)) {
268+
$cleaned++;
269+
}
270+
}
271+
272+
if ($cleaned > 0) {
273+
$this->io->writeln(sprintf(' ✓ Cleaned %d directories from pub/static', $cleaned));
274+
} else {
275+
$this->io->writeln(' ℹ No files found in pub/static');
276+
}
277+
278+
return true;
279+
} catch (\Exception $e) {
280+
$this->io->error('Failed to clean pub/static: ' . $e->getMessage());
281+
return false;
282+
}
283+
}
284+
285+
/**
286+
* Get theme pattern for matching directories
287+
*
288+
* @param string $themeCode
289+
* @return string
290+
*/
291+
private function getThemePattern(string $themeCode): string
292+
{
293+
// Theme code format: Vendor/theme
294+
// Pattern in directories: frontend/Vendor/theme
295+
return 'frontend/' . $themeCode;
296+
}
297+
298+
/**
299+
* Remove directory recursively
300+
*
301+
* @param string $dir
302+
* @return bool
303+
*/
304+
private function removeDirectory(string $dir): bool
305+
{
306+
try {
307+
if (!is_dir($dir)) {
308+
return false;
309+
}
310+
311+
$files = array_diff(scandir($dir), ['.', '..']);
312+
foreach ($files as $file) {
313+
$path = $dir . DIRECTORY_SEPARATOR . $file;
314+
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
315+
}
316+
317+
return rmdir($dir);
318+
} catch (\Exception $e) {
319+
$this->io->error('Failed to remove directory: ' . $e->getMessage());
320+
return false;
321+
}
322+
}
323+
324+
/**
325+
* Display clean summary
326+
*
327+
* @param array $successList
328+
* @param array $failureList
329+
* @param float $duration
330+
* @return void
331+
*/
332+
private function displayCleanSummary(array $successList, array $failureList, float $duration): void
333+
{
334+
$this->io->newLine();
335+
$this->io->section(sprintf('Clean process completed in %.2f seconds', $duration));
336+
337+
if (!empty($successList)) {
338+
$this->io->success('Successfully cleaned themes:');
339+
foreach ($successList as $themeCode) {
340+
$this->io->writeln(sprintf(' ✅ %s', $themeCode));
341+
}
342+
}
343+
344+
if (!empty($failureList)) {
345+
$this->io->warning('Failed to clean themes:');
346+
foreach ($failureList as $themeCode) {
347+
$this->io->writeln(sprintf(' ❌ %s', $themeCode));
348+
}
349+
}
350+
351+
if (empty($successList) && empty($failureList)) {
352+
$this->io->info('No themes were processed.');
353+
}
354+
}
355+
}

src/etc/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
<item name="mageforge_theme_watch"
2525
xsi:type="object"
2626
>OpenForgeProject\MageForge\Console\Command\Theme\WatchCommand</item>
27+
<item name="mageforge_static_clean"
28+
xsi:type="object"
29+
>OpenForgeProject\MageForge\Console\Command\Static\CleanCommand</item>
2730
</argument>
2831
</arguments>
2932
</type>

0 commit comments

Comments
 (0)