1
0
cms11/app/Console/Commands/Bundle/Validate.php

321 lines
9.0 KiB
PHP
Raw Normal View History

2024-04-26 12:50:38 +02:00
<?php
namespace App\Console\Commands\Bundle;
use App\Classes\Bundle;
use App\Console\Commands\Bundle\Traits\ReadsBundles;
use App\Console\Commands\Bundle\Traits\SelectsDisks;
2024-05-09 21:52:08 +02:00
use App\Services\HtmlValidator;
2024-04-26 12:50:38 +02:00
use Illuminate\Console\Command;
2024-05-09 21:52:08 +02:00
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Vite;
2024-05-06 22:11:20 +02:00
use Illuminate\Support\Str;
2024-04-26 12:50:38 +02:00
use JsonSchema\Validator;
2024-05-06 22:11:20 +02:00
use function Laravel\Prompts\progress;
2024-04-26 12:50:38 +02:00
class Validate extends Command
{
use ReadsBundles;
use SelectsDisks;
2024-04-26 12:50:38 +02:00
/**
* The console command description.
*
* @var string
*/
protected $description = 'Validate bundles metadata';
2024-05-09 21:52:08 +02:00
protected array $invalidJson = [];
protected array $invalidHtml = [];
protected array $invalidCss = [];
2024-05-06 22:11:20 +02:00
public function __construct()
{
$this->signature = 'bundle:validate
2024-05-09 21:52:08 +02:00
{ --no-css : Do not validate CSS }
{ --no-html : Do not validate HTML }
{ --no-json : Do not validate JSON }
{ --r|recursive : Also validate sub-bundles }
2024-05-06 22:11:20 +02:00
{ --source-disk= : Use specified content disk - Defaults to <info>' . env('CONTENT_DISK') . '</info> }
2024-05-09 21:52:08 +02:00
{ path? : Path to a specific bundle to validate - Default to <info>/</info> }
2024-05-06 22:11:20 +02:00
';
parent::__construct();
}
2024-04-26 12:50:38 +02:00
/**
* Execute the console command.
*/
public function handle()
2024-05-06 22:11:20 +02:00
{
$this->selectDisk()
2024-05-09 21:52:08 +02:00
->validateCss()
->selectBundles()
->validateJson()
->validateHtml()
2024-05-06 22:11:20 +02:00
->showReport();
2024-05-09 21:52:08 +02:00
if (
!empty($this->invalidCss)
|| !empty($this->invalidJson)
|| !empty($this->invalidHtml)
) {
exit(1);
}
2024-05-06 22:11:20 +02:00
}
/**
* Return an associative array containing available bundles metadata files
* as keys and json-decoded validation schemas as values
*/
2024-05-09 21:52:08 +02:00
private function getJsonValidators()
2024-05-06 22:11:20 +02:00
{
return array_map(function ($path) {
return json_decode(file_get_contents($path));
}, config('json_validator'));
}
/**
* Perform json validation on selected bundles
*/
2024-05-09 21:52:08 +02:00
private function validateJson(): self
2024-05-06 22:11:20 +02:00
{
2024-05-09 21:52:08 +02:00
if ($this->option('no-json')) {
return $this;
}
$this->line('Validating <info>JSON</info>...');
$bundles = $this->bundles;
$validators = $this->getJsonValidators();
2024-04-26 12:50:38 +02:00
2024-05-06 22:11:20 +02:00
progress(
label: 'Validating... ',
steps: $bundles,
2024-05-09 21:52:08 +02:00
callback: fn (Bundle $bundle, $progress) => $this->handleBundleJsonValidation($bundle, $progress, $validators)
2024-05-06 22:11:20 +02:00
);
2024-04-26 12:50:38 +02:00
2024-05-06 22:11:20 +02:00
return $this;
}
2024-04-26 12:50:38 +02:00
2024-05-06 22:11:20 +02:00
/**
* Handle specific bundle
*/
2024-05-09 21:52:08 +02:00
private function handleBundleJsonValidation(Bundle $bundle, $progress, $validators)
2024-05-06 22:11:20 +02:00
{
foreach ($validators as $metadataFilename => $schema) {
$filepath = $bundle->metadata($metadataFilename)->getFilename();
if (!$this->sourceDisk->exists($filepath)) {
continue;
2024-04-26 12:50:38 +02:00
}
2024-05-06 22:11:20 +02:00
$source = $this->sourceDisk->get($filepath) ?? '{}';
$data = json_decode($source);
$validator = new Validator();
$validator->validate($data, $schema);
if (!$validator->isValid()) {
2024-05-09 21:52:08 +02:00
$this->invalidJson[$bundle->getPath()] = [
2024-05-06 22:11:20 +02:00
'metadata' => $metadataFilename,
'errors' => $validator->getErrors(),
];
2024-04-26 12:50:38 +02:00
}
}
$progress->label(sprintf('Validated %s', $bundle->getPath()));
2024-05-06 22:11:20 +02:00
}
2024-04-26 12:50:38 +02:00
2024-05-09 21:52:08 +02:00
private function validateCss(): self
{
if ($this->option('no-css')) {
return $this;
}
$this->line('Validating <info>CSS</info>...');
$file = public_path(Vite::asset('resources/css/app.css'));
$cacheKey = sprintf('css_validation_%s', Str::slug($file));
$result = Cache::remember($cacheKey, now()->addMonth(), function () use ($file) {
$content = file_get_contents($file);
return HtmlValidator::validateCss($content);
});
$errors = collect($result['messages'])->where('type', '=', 'error');
if ($errors->count() > 0) {
$this->invalidCss = $result;
Cache::forever('css_is_valid', false);
} else {
Cache::forever('css_is_valid', true);
}
return $this;
}
/**
* Perform json validation on selected bundles
*/
private function validateHtml(): self
{
if ($this->option('no-html')) {
return $this;
}
$this->line('Validating <info>HTML</info>...');
$bundles = $this->bundles;
progress(
label: 'Validating... ',
steps: $bundles,
callback: fn (Bundle $bundle, $progress) => $this->handleBundleHtmlValidation($bundle, $progress)
);
return $this;
}
private function handleBundleHtmlValidation(Bundle $bundle, $progress)
{
$rendered = $bundle->render();
2024-05-09 22:24:59 +02:00
$changed = false;
2024-05-09 21:52:08 +02:00
foreach ($rendered as $path => $content) {
2024-05-09 22:24:59 +02:00
$result = HtmlValidator::validateHtml($content);
$errors = collect($result['messages'])->where('type', '=', 'error');
$valid = $errors->count() === 0;
2024-05-09 21:52:08 +02:00
$cacheKey = sprintf('html_is_valid_%s', Str::slug($path));
2024-05-09 22:24:59 +02:00
if (Cache::get($cacheKey) !== $valid) {
$changed = true;
2024-05-09 23:02:41 +02:00
Cache::forever($cacheKey, $valid);
2024-05-09 22:24:59 +02:00
}
2024-05-09 21:52:08 +02:00
2024-05-09 22:24:59 +02:00
if (!$valid) {
$this->invalidHtml[$path] = $result;
2024-05-09 21:52:08 +02:00
}
2024-05-09 22:24:59 +02:00
}
if ($changed) {
2024-05-09 23:11:48 +02:00
$bundle->touch();
2024-05-09 21:52:08 +02:00
}
$progress->label(sprintf('Validated %s', $bundle->getPath()));
2024-05-09 21:52:08 +02:00
}
2024-05-06 22:11:20 +02:00
/**
* Show a report in case of need
*/
private function showReport()
{
2024-05-09 21:52:08 +02:00
$this->showCssReport();
$this->showJsonReport();
$this->showHtmlReport();
}
/**
* Show a report in case of need
*/
private function showJsonReport()
{
if ($this->option('no-json')) {
return $this;
}
if (empty($this->invalidJson)) {
$this->info('Checked JSON files are valid');
2024-04-26 12:50:38 +02:00
} else {
2024-05-09 21:52:08 +02:00
$count = count($this->invalidJson);
2024-05-06 22:11:20 +02:00
2024-05-09 21:52:08 +02:00
$this->error(sprintf('%d invalid JSON %s reported', $count, Str::plural('file', $count)));
2024-05-06 22:11:20 +02:00
$this->newLine(2);
2024-05-09 21:52:08 +02:00
foreach ($this->invalidJson as $bundlePath => $data) {
2024-05-06 22:11:20 +02:00
$this->line($bundlePath);
$this->line(sprintf(' > In <comment>%s</comment>:', $data['metadata']));
foreach ($data['errors'] as $error) {
$this->line(sprintf(' - %s', $error['message']));
}
}
2024-04-26 12:50:38 +02:00
}
}
2024-05-09 21:52:08 +02:00
private function showCssReport()
{
if ($this->option('no-css')) {
return $this;
}
if (empty($this->invalidCss)) {
$this->info('Checked CSS files are valid');
} else {
$count = count($this->invalidCss);
$this->error(sprintf('%d CSS %s reported', $count, Str::plural('error', $count)));
$this->newLine();
$data = $this->invalidCss;
foreach ($data['messages'] as $message) {
$type = $message['type'];
$subType = $message['subType'] ?? $type;
$decoration = match ($subType) {
'error' => 'error',
'info' => 'comment',
'warning' => 'comment'
};
$this->newLine();
$this->line(sprintf(' > <%1$s>%2$s</%1$s>: %3$s', $decoration, $subType, $message['message']));
$this->line(' > > ' . $message['extract']);
}
}
}
private function showHtmlReport()
{
if ($this->option('no-html')) {
return $this;
}
if (empty($this->invalidHtml)) {
$this->info('Checked HTML files are valid');
} else {
$count = count($this->invalidHtml);
$this->error(sprintf('%d invalid HTML %s reported', $count, Str::plural('file', $count)));
$this->newLine(2);
foreach ($this->invalidHtml as $path => $data) {
$this->newLine(2);
$this->comment($path);
foreach ($data['messages'] as $message) {
$type = $message['type'];
$subType = $message['subType'] ?? $type;
$decoration = match ($subType) {
'error' => 'error',
'info' => 'comment',
'warning' => 'comment'
};
$this->newLine();
$this->line(sprintf(' > <%1$s>%2$s</%1$s>: %3$s', $decoration, $subType, $message['message']));
$this->line(' > > ' . $message['extract']);
}
}
}
}
2024-04-26 12:50:38 +02:00
}