utilikit-1.0.0/src/Commands/UtilikitCommands.php
src/Commands/UtilikitCommands.php
<?php
declare(strict_types=1);
namespace Drupal\utilikit\Commands;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\State\StateInterface;
use Drupal\utilikit\Service\UtilikitServiceProvider;
use Drupal\utilikit\Service\UtilikitConstants;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Drush commands for UtiliKit module.
*
* Provides command-line interface for managing UtiliKit CSS generation,
* content scanning, mode switching, and maintenance operations.
*/
final class UtilikitCommands extends DrushCommands {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* The UtiliKit service provider.
*
* @var \Drupal\utilikit\Service\UtilikitServiceProvider
*/
protected UtilikitServiceProvider $serviceProvider;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected StateInterface $state;
/**
* Constructs a UtilikitCommands object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory service.
* @param \Drupal\utilikit\Service\UtilikitServiceProvider $serviceProvider
* The UtiliKit service provider.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(
ConfigFactoryInterface $configFactory,
UtilikitServiceProvider $serviceProvider,
StateInterface $state,
) {
parent::__construct();
$this->configFactory = $configFactory;
$this->serviceProvider = $serviceProvider;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self(
$container->get('config.factory'),
$container->get('utilikit.service_provider'),
$container->get('state')
);
}
/**
* Formats bytes into human-readable size.
*
* @param int $bytes
* Number of bytes.
*
* @return string
* Formatted size string.
*/
private function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
/**
* Generate or regenerate UtiliKit static CSS file.
*
* @command utilikit:generate
* @aliases uk-gen,utilikit-generate
* @usage utilikit:generate
* Generate CSS from all known utility classes
* @usage drush uk-gen
* Short alias for generating CSS
*/
#[CLI\Command(name: 'utilikit:generate', aliases: ['uk-gen', 'utilikit-generate'])]
#[CLI\Usage(name: 'utilikit:generate', description: 'Generate CSS from all known utility classes')]
#[CLI\Usage(name: 'drush uk-gen', description: 'Short alias for generating CSS')]
public function generate(): void {
$config = $this->configFactory->get('utilikit.settings');
$mode = $config->get('rendering_mode') ?? 'inline';
$this->io()->title('UtiliKit CSS Generation');
// Check mode first.
if ($mode !== 'static') {
$this->io()->warning([
'UtiliKit is currently in ' . strtoupper($mode) . ' mode.',
'CSS file generation is only available in STATIC mode.',
'',
'In ' . $mode . ' mode, CSS is generated ' . ($mode === 'inline' ? 'dynamically by JavaScript' : 'in the page head') . ' on each page load.',
'No static CSS file is needed or created.',
]);
$this->io()->note([
'To switch to static mode and enable CSS file generation:',
' drush utilikit:mode static',
]);
return;
}
$stateManager = $this->serviceProvider->getStateManager();
$knownClasses = $stateManager->getKnownClasses();
if (empty($knownClasses)) {
$this->io()->error([
'No utility classes found in the system.',
'',
'UtiliKit needs to scan your content to discover utility classes before generating CSS.',
]);
$this->io()->note([
'Run a content scan to find utility classes:',
' drush utilikit:scan',
'',
'Or check your scanning configuration:',
' drush utilikit:status',
]);
return;
}
$this->io()->section('Generating Static CSS File');
$this->io()->text(sprintf('Processing %d utility classes...', count($knownClasses)));
$result = $this->serviceProvider->regenerateStaticCss();
if ($result) {
$css = $stateManager->getGeneratedCss();
$fileManager = $this->serviceProvider->getFileManager();
$cssUrl = $fileManager->getStaticCssUrl();
$timestamp = $stateManager->getCssTimestamp();
$this->io()->success('CSS file generated successfully');
$this->io()->definitionList(
['Utility Classes' => count($knownClasses)],
['CSS File Size' => $this->formatBytes(strlen($css))],
['File Location' => $cssUrl ?? 'Error: Unable to determine file URL'],
['Generated At' => date('Y-m-d H:i:s', $timestamp)],
);
}
else {
$this->io()->error([
'CSS generation failed.',
'',
'The CSS generator was unable to create a valid CSS file from the known classes.',
'This usually indicates a configuration or permission issue.',
]);
$this->io()->note([
'Check the following:',
' 1. Ensure the CSS directory is writable',
' 2. Verify utility classes exist: drush utilikit:classes',
' 3. Check logs for detailed error messages',
]);
}
}
/**
* Scan content for UtiliKit utility classes.
*
* @param array $options
* Command options.
*
* @command utilikit:scan
* @aliases uk-scan,utilikit-scan
* @option entity-types Comma-separated list of entity types to scan (e.g., node,block_content)
* @option batch-size Number of entities to process per batch (default: 50)
* @option timeout Maximum execution time in seconds (default: 300)
* @usage utilikit:scan
* Scan all configured entity types
* @usage utilikit:scan --entity-types=node,paragraph
* Scan only nodes and paragraphs
* @usage drush uk-scan --batch-size=100
* Scan with larger batch size
*/
#[CLI\Command(name: 'utilikit:scan', aliases: ['uk-scan', 'utilikit-scan'])]
#[CLI\Option(name: 'entity-types', description: 'Comma-separated list of entity types to scan')]
#[CLI\Option(name: 'batch-size', description: 'Number of entities to process per batch')]
#[CLI\Option(name: 'timeout', description: 'Maximum execution time in seconds')]
#[CLI\Usage(name: 'utilikit:scan', description: 'Scan all configured entity types')]
#[CLI\Usage(name: 'utilikit:scan --entity-types=node,paragraph', description: 'Scan only nodes and paragraphs')]
public function scan(
array $options = [
'entity-types' => NULL,
'batch-size' => 50,
'timeout' => 300,
],
): void {
$config = $this->configFactory->get('utilikit.settings');
$mode = $config->get('rendering_mode') ?? 'inline';
$this->io()->title('UtiliKit Content Scanner');
// Show current mode.
if ($mode === 'inline') {
$this->io()->note([
'Current mode: INLINE',
'Scanning will identify utility classes, but they will be processed dynamically by JavaScript.',
'No static CSS file will be generated.',
]);
}
else {
$this->io()->note([
'Current mode: STATIC',
'After scanning, a static CSS file will be automatically generated.',
]);
}
// Override config temporarily if entity-types specified.
$originalTypes = $config->get('scanning_entity_types');
if ($options['entity-types']) {
$entityTypes = array_map('trim', explode(',', $options['entity-types']));
$editableConfig = $this->configFactory->getEditable('utilikit.settings');
$editableConfig->set('scanning_entity_types', $entityTypes)->save();
$this->io()->text(sprintf('Scanning entity types: %s', implode(', ', $entityTypes)));
}
else {
$entityTypes = $originalTypes ?? ['node', 'block_content', 'paragraph'];
$this->io()->text(sprintf('Scanning configured entity types: %s', implode(', ', $entityTypes)));
}
$this->io()->section('Scanning Content');
$scanner = $this->serviceProvider->getContentScanner();
$result = $scanner->scanAllContent(
(int) $options['batch-size'],
(int) $options['timeout']
);
// CRITICAL: Save classes to state immediately after scanning.
$stateManager = $this->serviceProvider->getStateManager();
if (!empty($result['classes'])) {
$stateManager->addKnownClasses($result['classes']);
}
// Restore original config if it was changed.
if ($options['entity-types']) {
$editableConfig = $this->configFactory->getEditable('utilikit.settings');
$editableConfig->set('scanning_entity_types', $originalTypes)->save();
}
// Check for timeout.
if (!$result['completed']) {
$this->io()->warning([
'Content scan timed out after ' . $result['execution_time'] . ' seconds.',
'',
'Some content may not have been scanned.',
'Partial results have been saved.',
]);
}
// Display scan results.
$this->io()->success('Content scan completed');
$this->io()->definitionList(
['Entities Scanned' => number_format($result['scanned_count'])],
['Utility Classes Found' => count($result['classes'])],
['Execution Time' => $result['execution_time'] . ' seconds'],
['Status' => $result['completed'] ? 'Complete' : 'Timed out (partial)'],
);
// Show found classes if any.
if (!empty($result['classes'])) {
$this->io()->section('Sample Classes Found');
$sampleClasses = array_slice($result['classes'], 0, 10);
$this->io()->listing($sampleClasses);
if (count($result['classes']) > 10) {
$this->io()->text(sprintf('... and %d more classes', count($result['classes']) - 10));
}
// Auto-generate CSS in static mode.
if ($mode === 'static') {
$this->io()->section('Generating Static CSS File');
$this->io()->text('Automatically generating CSS file from scanned classes...');
$this->generate();
}
else {
$this->io()->note([
'Classes have been saved and will be processed dynamically in inline mode.',
'To generate a static CSS file instead:',
' 1. Switch to static mode: drush utilikit:mode static',
' 2. Generate CSS: drush utilikit:generate',
]);
}
}
else {
$this->io()->warning([
'No UtiliKit utility classes found in scanned content.',
'This could mean:',
' • No content is using UtiliKit utility classes yet',
' • The scanned entity types don\'t contain UtiliKit classes',
' • UtiliKit classes are in entity types not included in the scan',
]);
$this->io()->note([
'Next steps:',
' 1. Add UtiliKit classes to your content (e.g., uk-pd--20, uk-bg--primary)',
' 2. Verify scanning configuration: drush utilikit:status',
' 3. Try scanning different entity types: drush utilikit:scan --entity-types=node,block_content',
]);
}
}
/**
* Clear all UtiliKit CSS and reset known classes.
*
* @command utilikit:clear
* @aliases uk-clear,utilikit-clear
* @usage utilikit:clear
* Clear all CSS and reset UtiliKit state
* @usage drush uk-clear
* Short alias for clearing
*/
#[CLI\Command(name: 'utilikit:clear', aliases: ['uk-clear', 'utilikit-clear'])]
#[CLI\Usage(name: 'utilikit:clear', description: 'Clear all CSS and reset UtiliKit state')]
public function clear(): void {
$this->io()->title('Clearing UtiliKit Data');
$stateManager = $this->serviceProvider->getStateManager();
$classCount = count($stateManager->getKnownClasses());
// Clear state using State API.
$this->state->deleteMultiple([
UtilikitConstants::STATE_GENERATED_CSS,
UtilikitConstants::STATE_KNOWN_CLASSES,
UtilikitConstants::STATE_CSS_TIMESTAMP,
UtilikitConstants::STATE_LAST_CLEANUP,
]);
// Clear caches.
$this->serviceProvider->getCacheManager()->clearAllCaches();
// Clean up files.
$fileManager = $this->serviceProvider->getFileManager();
$fileManager->cleanupStaticFiles();
$this->io()->success([
sprintf('Cleared %d utility classes', $classCount),
'Cleared all caches',
'Removed static CSS files',
]);
}
/**
* Switch UtiliKit rendering mode.
*
* @param string $mode
* The rendering mode (inline or static).
*
* @command utilikit:mode
* @aliases uk-mode,utilikit-mode
* @argument mode The rendering mode: inline or static
* @usage utilikit:mode static
* Switch to static mode (pre-generated CSS)
* @usage utilikit:mode inline
* Switch to inline mode (dynamic CSS)
* @usage drush uk-mode static
* Short alias for mode switching
*/
#[CLI\Command(name: 'utilikit:mode', aliases: ['uk-mode', 'utilikit-mode'])]
#[CLI\Argument(name: 'mode', description: 'The rendering mode: inline or static')]
#[CLI\Usage(name: 'utilikit:mode static', description: 'Switch to static mode')]
#[CLI\Usage(name: 'utilikit:mode inline', description: 'Switch to inline mode')]
public function switchMode(string $mode): void {
// Validate mode.
if (!in_array($mode, ['inline', 'static', 'head'], TRUE)) {
$this->io()->error('Mode must be "inline", "static", or "head"');
return;
}
$config = $this->configFactory->getEditable('utilikit.settings');
$oldMode = $config->get('rendering_mode') ?? 'inline';
if ($oldMode === $mode) {
$this->io()->note(sprintf('Already in %s mode', $mode));
return;
}
$this->io()->title(sprintf('Switching from %s to %s mode', $oldMode, $mode));
// Save new mode.
$config->set('rendering_mode', $mode)->save();
// Clean up old mode artifacts.
if ($mode === 'inline') {
// Switching to inline - clean up static files.
$fileManager = $this->serviceProvider->getFileManager();
$fileManager->cleanupStaticFiles();
$this->io()->text('Cleaned up static CSS files');
}
elseif ($mode === 'head') {
// Switching to head - clean up static files.
$fileManager = $this->serviceProvider->getFileManager();
$fileManager->cleanupStaticFiles();
$this->io()->text('Cleaned up static CSS files');
$stateManager = $this->serviceProvider->getStateManager();
$knownClasses = $stateManager->getKnownClasses();
if (empty($knownClasses)) {
$this->io()->text('Scanning content for utility classes...');
$scanResult = $this->serviceProvider->getContentScanner()->scanAllContent();
if (!empty($scanResult['classes'])) {
$stateManager->setKnownClasses($scanResult['classes']);
$this->io()->text(sprintf('Found %d utility classes', count($scanResult['classes'])));
}
}
}
else {
// Switching to static - generate CSS if classes exist.
$stateManager = $this->serviceProvider->getStateManager();
$knownClasses = $stateManager->getKnownClasses();
if (!empty($knownClasses)) {
$this->io()->text(sprintf('Generating CSS for %d classes...', count($knownClasses)));
$this->serviceProvider->regenerateStaticCss();
}
else {
$this->io()->warning('No utility classes found. Run "drush utilikit:scan" to scan content.');
}
}
// Clear caches.
$this->serviceProvider->getCacheManager()->clearAllCaches();
$this->io()->success(sprintf('Switched to %s mode', $mode));
}
/**
* Show UtiliKit status and configuration.
*
* @command utilikit:status
* @aliases uk-status,utilikit-status
* @usage utilikit:status
* Display UtiliKit configuration and statistics
* @usage drush uk-status
* Short alias for status
*/
#[CLI\Command(name: 'utilikit:status', aliases: ['uk-status', 'utilikit-status'])]
#[CLI\Usage(name: 'utilikit:status', description: 'Display UtiliKit configuration and statistics')]
public function status(): void {
$config = $this->configFactory->get('utilikit.settings');
$stateManager = $this->serviceProvider->getStateManager();
$mode = $config->get('rendering_mode') ?? 'inline';
$knownClasses = $stateManager->getKnownClasses();
$scanningTypes = $config->get('scanning_entity_types') ?? [];
$css = $stateManager->getGeneratedCss();
$rows = [
['Rendering Mode', $mode],
['Utility Classes', count($knownClasses)],
['Scanning Entity Types', implode(', ', $scanningTypes)],
['Scope', $config->get('scope_global') ? 'Global' : 'Content Types'],
['Dev Mode', $config->get('dev_mode') ? 'Enabled' : 'Disabled'],
];
if ($mode === 'static') {
$fileManager = $this->serviceProvider->getFileManager();
$cssUrl = $fileManager->getStaticCssUrl();
$rows[] = ['CSS File', $cssUrl ?? 'Not generated'];
$rows[] = ['CSS Size', $css ? $this->formatBytes(strlen($css)) : 'N/A'];
$rows[] = [
'Last Updated',
$stateManager->getCssTimestamp() ? date('Y-m-d H:i:s', $stateManager->getCssTimestamp()) : 'Never',
];
}
$this->io()->title('UtiliKit Status');
$this->io()->table(['Setting', 'Value'], $rows);
}
/**
* List all tracked utility classes.
*
* @param array $options
* Command options.
*
* @command utilikit:classes
* @aliases uk-classes,utilikit-classes
* @option limit Maximum number of classes to display (default: 50, 0 = all)
* @option filter Filter classes by prefix (e.g., uk-pd for padding)
* @usage utilikit:classes
* List first 50 utility classes
* @usage utilikit:classes --limit=0
* List all utility classes
* @usage utilikit:classes --filter=uk-pd
* List only padding classes
* @usage drush uk-classes --limit=20
* Short alias showing 20 classes
*/
#[CLI\Command(name: 'utilikit:classes', aliases: ['uk-classes', 'utilikit-classes'])]
#[CLI\Option(name: 'limit', description: 'Maximum number of classes to display')]
#[CLI\Option(name: 'filter', description: 'Filter classes by prefix')]
#[CLI\Usage(name: 'utilikit:classes', description: 'List first 50 utility classes')]
#[CLI\Usage(name: 'utilikit:classes --limit=0', description: 'List all utility classes')]
public function listClasses(
array $options = [
'limit' => 50,
'filter' => NULL,
],
): void {
$stateManager = $this->serviceProvider->getStateManager();
$classes = $stateManager->getKnownClasses();
if (empty($classes)) {
$this->io()->warning('No utility classes found. Run "drush utilikit:scan" first.');
return;
}
// Filter if requested.
if ($options['filter']) {
$classes = array_filter($classes, function ($class) use ($options) {
return str_starts_with($class, $options['filter']);
});
}
sort($classes);
$total = count($classes);
$limit = (int) $options['limit'];
if ($limit > 0 && $total > $limit) {
$classes = array_slice($classes, 0, $limit);
$this->io()->title(sprintf('Utility Classes (showing %d of %d)', $limit, $total));
}
else {
$this->io()->title(sprintf('Utility Classes (%d total)', $total));
}
$this->io()->listing($classes);
if ($limit > 0 && $total > $limit) {
$this->io()->note(sprintf('Showing first %d classes. Use --limit=0 to see all.', $limit));
}
}
/**
* Validate UtiliKit CSS generation.
*
* @command utilikit:validate
* @aliases uk-validate,utilikit-validate
* @usage utilikit:validate
* Check for CSS generation errors
* @usage drush uk-validate
* Short alias for validation
*/
#[CLI\Command(name: 'utilikit:validate', aliases: ['uk-validate', 'utilikit-validate'])]
#[CLI\Usage(name: 'utilikit:validate', description: 'Check for CSS generation errors')]
public function validate(): void {
$this->io()->title('Validating UtiliKit Configuration');
$config = $this->configFactory->get('utilikit.settings');
$stateManager = $this->serviceProvider->getStateManager();
$mode = $config->get('rendering_mode') ?? 'inline';
$issues = [];
$warnings = [];
// Check for known classes.
$knownClasses = $stateManager->getKnownClasses();
if (empty($knownClasses)) {
$warnings[] = 'No utility classes found. Run "drush utilikit:scan" to scan content.';
}
else {
$this->io()->text(sprintf('✓ Found %d utility classes', count($knownClasses)));
}
// Check static mode requirements.
if ($mode === 'static') {
$fileManager = $this->serviceProvider->getFileManager();
$cssUrl = $fileManager->getStaticCssUrl();
if (!$cssUrl) {
$issues[] = 'Static CSS file not found. Run "drush utilikit:generate" to create it.';
}
else {
$this->io()->text(sprintf('✓ Static CSS file exists: %s', $cssUrl));
}
$css = $stateManager->getGeneratedCss();
if (empty($css)) {
$warnings[] = 'Generated CSS is empty. Ensure utility classes exist in content.';
}
else {
$this->io()->text(sprintf('✓ Generated CSS size: %s', $this->formatBytes(strlen($css))));
}
}
// Check scanning configuration.
$scanningTypes = $config->get('scanning_entity_types') ?? [];
if (empty($scanningTypes)) {
$warnings[] = 'No entity types configured for scanning.';
}
else {
$this->io()->text(sprintf('✓ Scanning entity types: %s', implode(', ', $scanningTypes)));
}
// Output results.
if (!empty($issues)) {
$this->io()->error('Validation failed:');
foreach ($issues as $issue) {
$this->io()->text(' ✗ ' . $issue);
}
}
if (!empty($warnings)) {
$this->io()->warning('Warnings:');
foreach ($warnings as $warning) {
$this->io()->text(' ⚠ ' . $warning);
}
}
if (empty($issues) && empty($warnings)) {
$this->io()->success('UtiliKit configuration is valid');
}
elseif (empty($issues)) {
$this->io()->note('Validation passed with warnings');
}
}
/**
* Clear UtiliKit-specific caches.
*
* @command utilikit:cache-clear
* @aliases uk-cc,utilikit-cache-clear
* @usage utilikit:cache-clear
* Clear UtiliKit caches
* @usage drush uk-cc
* Short alias for cache clear
*/
#[CLI\Command(name: 'utilikit:cache-clear', aliases: ['uk-cc', 'utilikit-cache-clear'])]
#[CLI\Usage(name: 'utilikit:cache-clear', description: 'Clear UtiliKit caches')]
public function cacheClear(): void {
$this->io()->title('Clearing UtiliKit Caches');
$this->serviceProvider->getCacheManager()->clearAllCaches();
$this->io()->success('UtiliKit caches cleared');
}
}
