Implement academy analytics, billing, and web stories updates
This commit is contained in:
163
app/Console/Commands/ValidateWorldWebStoriesCommand.php
Normal file
163
app/Console/Commands/ValidateWorldWebStoriesCommand.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WorldWebStory;
|
||||
use App\Services\WebStories\WorldWebStoryValidationService;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
final class ValidateWorldWebStoriesCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:webstories:validate
|
||||
{story? : Story ID or slug}
|
||||
{--published : Limit batch mode to published stories}
|
||||
{--visible : Limit batch mode to publicly visible stories}
|
||||
{--limit=100 : Maximum stories to validate in batch mode}
|
||||
{--amp : Also run amphtml-validator against the public story URL}
|
||||
{--fail-warnings : Treat validation warnings as failures}';
|
||||
|
||||
protected $description = 'Validate World Web Stories for publish safety and optional AMP validity';
|
||||
|
||||
public function handle(WorldWebStoryValidationService $validation): int
|
||||
{
|
||||
$storyKey = $this->argument('story');
|
||||
|
||||
if ($storyKey !== null && trim((string) $storyKey) !== '') {
|
||||
$story = $this->resolveStory((string) $storyKey);
|
||||
|
||||
if (! $story instanceof WorldWebStory) {
|
||||
$this->error(sprintf('Web story [%s] was not found.', (string) $storyKey));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return $this->validateOne($validation, $story);
|
||||
}
|
||||
|
||||
return $this->validateBatch($validation, max(1, (int) $this->option('limit')));
|
||||
}
|
||||
|
||||
private function validateOne(WorldWebStoryValidationService $validation, WorldWebStory $story): int
|
||||
{
|
||||
$result = $validation->validate($story);
|
||||
$ampErrors = $this->ampErrors($story);
|
||||
|
||||
$this->line(sprintf('Story [%d] %s', (int) $story->id, (string) $story->slug));
|
||||
|
||||
foreach ((array) $result['warnings'] as $warning) {
|
||||
$this->warn(' - ' . $warning);
|
||||
}
|
||||
|
||||
foreach ((array) $result['errors'] as $error) {
|
||||
$this->error(' - ' . $error);
|
||||
}
|
||||
|
||||
foreach ($ampErrors as $ampError) {
|
||||
$this->error(' - AMP: ' . $ampError);
|
||||
}
|
||||
|
||||
if ($result['valid'] && $ampErrors === []) {
|
||||
$this->info('Validation passed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
private function validateBatch(WorldWebStoryValidationService $validation, int $limit): int
|
||||
{
|
||||
$processed = 0;
|
||||
$failed = 0;
|
||||
|
||||
$this->storyQuery()
|
||||
->limit($limit)
|
||||
->get()
|
||||
->each(function (WorldWebStory $story) use ($validation, &$processed, &$failed): void {
|
||||
$processed++;
|
||||
$result = $validation->validate($story);
|
||||
$ampErrors = $this->ampErrors($story);
|
||||
$warningsFail = (bool) $this->option('fail-warnings') && count((array) $result['warnings']) > 0;
|
||||
$hasFailure = ! $result['valid'] || $warningsFail || $ampErrors !== [];
|
||||
|
||||
if ($hasFailure) {
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$this->line(sprintf('[%d] %s -> %s', (int) $story->id, (string) $story->slug, $hasFailure ? 'invalid' : 'valid'));
|
||||
|
||||
foreach ((array) $result['warnings'] as $warning) {
|
||||
$this->warn(' - ' . $warning);
|
||||
}
|
||||
|
||||
foreach ((array) $result['errors'] as $error) {
|
||||
$this->error(' - ' . $error);
|
||||
}
|
||||
|
||||
foreach ($ampErrors as $ampError) {
|
||||
$this->error(' - AMP: ' . $ampError);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info(sprintf('Done. processed=%d failed=%d', $processed, $failed));
|
||||
|
||||
return $failed === 0 ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function storyQuery()
|
||||
{
|
||||
return WorldWebStory::query()
|
||||
->when((bool) $this->option('published'), fn ($query) => $query->published())
|
||||
->when((bool) $this->option('visible'), fn ($query) => $query->visible())
|
||||
->orderByDesc('published_at')
|
||||
->orderByDesc('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function ampErrors(WorldWebStory $story): array
|
||||
{
|
||||
if (! (bool) $this->option('amp')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! $story->exists || ! $story->publicUrl()) {
|
||||
return ['Story has no public URL to validate.'];
|
||||
}
|
||||
|
||||
$probe = new Process(['npx', 'amphtml-validator', '--version'], base_path(), null, null, 60);
|
||||
$probe->run();
|
||||
|
||||
if (! $probe->isSuccessful()) {
|
||||
return ['amphtml-validator is not available via npx.'];
|
||||
}
|
||||
|
||||
$process = new Process(['npx', 'amphtml-validator', $story->publicUrl()], base_path(), null, null, 120);
|
||||
$process->run();
|
||||
|
||||
if ($process->isSuccessful()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$output = trim($process->getErrorOutput() ?: $process->getOutput());
|
||||
|
||||
if ($output === '') {
|
||||
return ['AMP validator failed without output.'];
|
||||
}
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $output);
|
||||
|
||||
return $lines === false || $lines === [] ? ['AMP validator failed.'] : $lines;
|
||||
}
|
||||
|
||||
private function resolveStory(string $value): ?WorldWebStory
|
||||
{
|
||||
return WorldWebStory::query()
|
||||
->when(is_numeric($value), fn ($query) => $query->where('id', (int) $value), fn ($query) => $query->where('slug', $value))
|
||||
->first();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user