Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
ValidatePublicationsCommand
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
11 / 11
33
100.00% covered (success)
100.00%
1 / 1
 safeHandle
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 getPublicationTypesToValidate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 validatePublicationType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 displayResults
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 displayPublicationResults
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 displayPublicationFieldResults
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getPublicationResultsIcon
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getMessageTypesInResult
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 outputSummary
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 outputJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 countRecursive
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Publications\Commands;
6
7use Hyde\Hyde;
8use Hyde\Publications\Actions\PublicationPageValidator;
9use Hyde\Publications\Models\PublicationType;
10use Hyde\Publications\Publications;
11use Illuminate\Support\Collection;
12use InvalidArgumentException;
13use LaravelZero\Framework\Commands\Command;
14
15use function array_key_last;
16use function array_map;
17use function array_values;
18use function basename;
19use function collect;
20use function count;
21use function explode;
22use function filled;
23use function glob;
24use function in_array;
25use function json_encode;
26use function memory_get_peak_usage;
27use function microtime;
28use function round;
29use function sprintf;
30use function str_repeat;
31use function str_replace;
32use function str_starts_with;
33use function strlen;
34use function substr_count;
35
36/**
37 * Hyde command to validate one or all publications.
38 *
39 * @see \Hyde\Publications\Testing\Feature\ValidatePublicationsCommandTest
40 *
41 * @internal This command is not part of the public API and may change without notice.
42 */
43class ValidatePublicationsCommand extends ValidatingCommand
44{
45    /** @var string */
46    protected $signature = 'validate:publications
47        {publicationType? : The name of the publication type to validate.}
48        {--json : Display results as JSON.}';
49
50    /** @var string */
51    protected $description = 'Validate all or the specified publication type(s)';
52
53    protected float $timeStart;
54
55    protected array $results = [];
56
57    protected int $countedErrors = 0;
58    protected int $countedWarnings = 0;
59
60    protected string $passedIcon = "<fg=green>\u{2713}</>";
61    protected string $failedIcon = "<fg=red>\u{2A2F}</>";
62    protected string $warningIcon = "<fg=yellow>\u{0021}</>";
63
64    public function safeHandle(): int
65    {
66        $this->timeStart = microtime(true);
67
68        if (! $this->option('json')) {
69            $this->title('Validating publications!');
70        }
71
72        $publicationTypesToValidate = $this->getPublicationTypesToValidate();
73
74        foreach ($publicationTypesToValidate as $publicationType) {
75            $this->validatePublicationType($publicationType);
76        }
77
78        $this->countedErrors = substr_count(json_encode($this->results), '":"Error: ');
79        $this->countedWarnings = substr_count(json_encode($this->results), '":"Warning: ');
80
81        if ($this->option('json')) {
82            $this->outputJson();
83        } else {
84            $this->displayResults();
85
86            $this->outputSummary();
87        }
88
89        if ($this->countedErrors > 0) {
90            return Command::FAILURE;
91        }
92
93        return Command::SUCCESS;
94    }
95
96    protected function getPublicationTypesToValidate(): Collection
97    {
98        $publicationTypes = Publications::getPublicationTypes();
99        $name = $this->argument('publicationType');
100
101        if (filled($name)) {
102            if (! $publicationTypes->has($name)) {
103                throw new InvalidArgumentException("Publication type [$name] does not exist");
104            }
105
106            return collect([$name => PublicationType::get($name)]);
107        }
108
109        if ($publicationTypes->isEmpty()) {
110            throw new InvalidArgumentException('No publication types to validate!');
111        }
112
113        return $publicationTypes;
114    }
115
116    protected function validatePublicationType(PublicationType $publicationType): void
117    {
118        $this->results[$publicationType->getIdentifier()] = [];
119
120        foreach (glob(Hyde::path("{$publicationType->getDirectory()}/*.md")) as $publicationFile) {
121            $identifier = basename($publicationFile, '.md');
122            $this->results[$publicationType->getIdentifier()][$identifier] = PublicationPageValidator::call($publicationType, $identifier)->getResults();
123        }
124    }
125
126    protected function displayResults(): void
127    {
128        foreach ($this->results as $publicationTypeName => $publications) {
129            $this->infoComment("Validating publication type [$publicationTypeName]");
130            foreach ($publications ?? [] as $publicationName => $errors) {
131                $this->displayPublicationResults($publicationName, $errors);
132            }
133
134            if ($publicationTypeName !== array_key_last($this->results)) {
135                $this->output->newLine();
136            }
137        }
138    }
139
140    protected function displayPublicationResults(string $publicationName, array $results): void
141    {
142        $this->line(sprintf('  %s <fg=cyan>%s.md</>', $this->getPublicationResultsIcon(
143            $this->getMessageTypesInResult($results)), $publicationName
144        ));
145
146        foreach ($results as $message) {
147            $this->displayPublicationFieldResults($message);
148        }
149    }
150
151    protected function displayPublicationFieldResults(string $message): void
152    {
153        $isWarning = str_starts_with($message, 'Warning: ');
154        $isError = str_starts_with($message, 'Error: ');
155
156        $message = str_replace(['Warning: ', 'Error: '], '', $message);
157
158        if ($isWarning || $isError) {
159            if ($isWarning) {
160                $this->line(sprintf('    %s <comment>%s</comment>', $this->warningIcon, $message));
161            } else {
162                $this->line(sprintf('    %s <fg=red>%s</>', $this->failedIcon, $message));
163            }
164        } elseif ($this->output->isVerbose()) {
165            $this->line(sprintf('    %s <fg=green>%s</>', $this->passedIcon, $message));
166        }
167    }
168
169    protected function getPublicationResultsIcon(array $types): string
170    {
171        if (in_array('Error', $types)) {
172            return $this->failedIcon;
173        }
174
175        if (in_array('Warning', $types)) {
176            return $this->warningIcon;
177        }
178
179        return $this->passedIcon;
180    }
181
182    protected function getMessageTypesInResult(array $results): array
183    {
184        return array_map(function (string $result): string {
185            return explode(':', $result)[0];
186        }, array_values($results));
187    }
188
189    protected function outputSummary(): void
190    {
191        $size = strlen('Summary:');
192        $spaces = str_repeat(' ', $size);
193
194        $this->output->newLine();
195        $this->output->writeln("<bg=blue;fg=white>{$spaces}Summary:$spaces</>");
196        $this->output->newLine();
197
198        $countPublicationTypes = count($this->results);
199        $countPublications = self::countRecursive($this->results, 1);
200        $countFields = self::countRecursive($this->results, 2);
201
202        $this->output->writeln(sprintf('<fg=green>Validated %d publication types, %d publications, %d fields</><fg=gray> in %sms using %sMB peak memory</>',
203            $countPublicationTypes, $countPublications, $countFields,
204            round((microtime(true) - $this->timeStart) * 1000),
205            round(memory_get_peak_usage() / 1024 / 1024)
206        ));
207
208        $this->output->writeln('<fg='.($this->countedWarnings ? 'yellow' : 'green').">Found $this->countedWarnings Warnings</>");
209        $this->output->writeln('<fg='.($this->countedErrors ? 'red' : 'green').">Found $this->countedErrors Errors</>");
210    }
211
212    protected function outputJson(): void
213    {
214        $this->output->writeln(json_encode($this->results, JSON_PRETTY_PRINT));
215    }
216
217    protected static function countRecursive(array $array, int $limit): int
218    {
219        $count = 0;
220
221        foreach ($array as $child) {
222            if ($limit > 0) {
223                $count += self::countRecursive($child, $limit - 1);
224            } else {
225                $count += 1;
226            }
227        }
228
229        return $count;
230    }
231}