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 */ 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(); } }