Completed
Push — issue/270 ( d44ab8...3ee3d6 )
by Alex
02:08
created

CheckCommand::checkHitAtDateO()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php declare(strict_types=1);
2
3
namespace FeedIo\Command;
4
5
use FeedIo\Adapter\Guzzle\Client;
6
use FeedIo\FeedInterface;
7
use FeedIo\FeedIo;
8
use Psr\Log\NullLogger;
9
use Symfony\Component\Console\Command\Command;
10
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
11
use Symfony\Component\Console\Helper\Table;
12
use Symfony\Component\Console\Input\InputArgument;
13
use Symfony\Component\Console\Input\InputInterface;
14
use Symfony\Component\Console\Output\OutputInterface;
15
use Symfony\Component\Console\Style\SymfonyStyle;
16
17
/**
18
 * Class CheckCommand
19
 * @codeCoverageIgnore
20
 */
21
class CheckCommand extends Command
22
{
23
    private $ok;
24
25
    private $notOk;
26
27
    private $feedIo;
28
29
    public function __construct(string $name = null)
30
    {
31
        parent::__construct($name);
32
33
        $client = new Client(new \GuzzleHttp\Client());
34
        $this->feedIo = new FeedIo($client, new NullLogger());
35
    }
36
37
    protected function getFeedIo(): FeedIo
38
    {
39
        return $this->feedIo;
40
    }
41
42
    protected function configure()
43
    {
44
        $this->setName('check')
45
            ->setDescription('checks if a feed gets correctly updated')
46
            ->addArgument(
47
                'url',
48
                InputArgument::REQUIRED,
49
                'Please provide an URL or a file to read'
50
            )
51
        ;
52
    }
53
54
    protected function execute(InputInterface $input, OutputInterface $output)
55
    {
56
        $this->configureOutput($output);
57
        $io = new SymfonyStyle($input, $output);
58
        $urls = $this->getUrls($input);
59
60
        $results = [];
61
        $return = 0;
62
63
        foreach ($urls as $url) {
64
            if (empty($url)) {
65
                continue;
66
            }
67
            $result = $this->runChecks($io, trim($url));
68
            $results[] = iterator_to_array($this->renderValues($output, $result->toArray()));
69
            if (! $result->isUpdateable()) {
70
                $return++;
71
            }
72
        }
73
74
        $table = new Table($output);
75
        $table
76
            ->setHeaders(['URL', 'Accessible', 'readSince', 'Last modified', '# items', 'unique IDs', 'Date Flow', 'Jan 1970', '1 year old', 'Future'])
77
            ->setRows($results)
78
            ->setColumnWidth(0, 48)
79
        ;
80
        $table->render();
81
82
        if ($return > 0) {
83
            $io->error("Some feeds were marked as not updateable. Two possible explanations: a feed you tried to consumed doesn't match the specification or FeedIo has a bug.");
84
        }
85
        return $return;
86
    }
87
88
    protected function getUrls(InputInterface $input): array
89
    {
90
        $arg = $input->getArgument('url');
91
        if (filter_var($arg, FILTER_VALIDATE_URL)) {
92
            return [$arg];
93
        }
94
        if (! file_exists($arg)) {
95
            throw new \UnexpectedValueException("$arg must contain a valid URL or a file to read");
96
        }
97
        $content = file_get_contents($arg);
98
        return explode("\n", $content);
99
    }
100
101
    protected function renderValues(OutputInterface $output, array $values): \Generator
102
    {
103
        foreach ($values as $value) {
104
            if (is_bool($value)) {
105
                yield $value ? $this->ok: $this->notOk;
106
            } elseif ($value === 0 || $value === 'null') {
107
                yield $output->getFormatter()->format("<ko>$value</ko>");
108
            } else {
109
                yield $output->getFormatter()->format("<ok>$value</ok>");
110
            }
111
        }
112
    }
113
114
    protected function runChecks(SymfonyStyle $io, string $url): Result
115
    {
116
        $result = new Result($url);
117
        try {
118
            $io->section("reading {$url}");
119
            $feed = $this->getFeedIo()->read($url)->getFeed();
120
121
            $count = count($feed);
122
            $this->printResult($io, "the feed has items ($count)", $count > 0);
123
            if ($count == 0) {
124
                $result->setNotUpdateable();
125
                return $result;
126
            }
127
128
            $result->setItemCount($count);
129
            $firstHitResult = $this->checkFirstHit($io, $feed, $result);
130
        } catch (\Throwable $e) {
131
            $io->error($e->getMessage());
132
            $result->setNotAccessible();
133
            return $result;
134
        }
135
136
        $updateable = $this->checkSecondHit($io, $url, $firstHitResult);
137
        if (!$updateable) {
138
            $result->setNotUpdateable();
139
        }
140
        $this->printResult($io, "the feed is updateable", $updateable);
141
142
        $emptyInTheFuture = $this->checkHitInTheFuture($url);
143
        if (!$emptyInTheFuture) {
144
            $result->markAsFailed();
0 ignored issues
show
Bug introduced by
The call to markAsFailed() misses a required argument $test.

This check looks for function calls that miss required arguments.

Loading history...
145
        }
146
        $this->printResult($io, "a call in the future is empty as expected", $emptyInTheFuture);
147
148
        $dateO = $this->checkHitAtDateO($url);
149
        if (!$dateO) {
150
            $result->markAsFailed(Result::TEST_JAN_1970);
151
        }
152
        $this->printResult($io, "a call at Jan 1970 is filled as expected", $dateO);
153
154
        $OneYearOld = $this->checkHitOneYearOld($url);
155
        if (!$OneYearOld) {
156
            $result->markAsFailed(Result::TEST_1YEAR_OLD);
157
        }
158
        $this->printResult($io, "a call with modifiedSince = 1yr old is filled", $OneYearOld);
159
160
        unset($feed);
161
        return $result;
162
    }
163
164
    private function checkFirstHit(SymfonyStyle $io, FeedInterface $feed, Result $result): array
165
    {
166
        $lastModifiedDates = [];
167
        $publicIds = [];
168
        $result->setModifiedSince($feed->getLastModified());
169
        /** @var \FeedIo\Feed\ItemInterface $item */
170
        foreach ($feed as $i => $item) {
171
            $lastModifiedDates[] = $item->getLastModified();
172
            $publicIds[] = $item->getPublicId();
173
        }
174
175
        if (! $this->checkPublicIds($publicIds)) {
176
            $result->markAsFailed(Result::TEST_UNIQUE_IDS);
177
        }
178
179
        sort($lastModifiedDates);
180
        $first = current($lastModifiedDates);
181
        $last = end($lastModifiedDates);
182
183
        $normalDateFlow = true;
184
        if (! ($last > $first)) {
185
            $result->markAsFailed(Result::TEST_NORMAL_DATE_FLOW);
186
            $normalDateFlow = false;
187
        }
188
        $this->printResult($io, "the date flow is normal", $normalDateFlow);
189
190
        return [
191
            'lastModifiedDates' => $lastModifiedDates,
192
            'normalDateFlow' => $normalDateFlow,
193
            'publicIds' => $publicIds,
194
        ];
195
    }
196
197
    private function checkSecondHit(SymfonyStyle $io, string $url, array $firstResult): bool
198
    {
199
        $count = count($firstResult['lastModifiedDates']);
200
        $last = end($firstResult['lastModifiedDates']);
201
        if ($firstResult['normalDateFlow']) {
202
            $pick = intval($count / 2);
203
            $lastModified = $firstResult['lastModifiedDates'][$pick];
204
        } else {
205
            $lastModified = $last->sub(new \DateInterval('P1D'));
206
        }
207
208
        $secondFeed = $this->getFeedIo()->readSince($url, $lastModified)->getFeed();
209
210
        $count = count($secondFeed);
211
        $this->printResult($io, "the feed has items on second call ($count)", $count > 0);
212
        if ($count == 0) {
213
            return false;
214
        }
215
216
        return true;
217
    }
218
219
    private function checkHitInTheFuture(string $url): bool
220
    {
221
        $feed = $this->getFeedIo()->readSince($url, new \DateTime("+1 week"))->getFeed();
222
223
        return count($feed) == 0;
224
    }
225
226
    private function checkHitAtDateO(string $url): bool
227
    {
228
        $feed = $this->getFeedIo()->readSince($url, new \DateTime("@0"))->getFeed();
229
230
        return count($feed) > 0;
231
    }
232
233
    private function checkHitOneYearOld(string $url): bool
234
    {
235
        $feed = $this->getFeedIo()->readSince($url, new \DateTime("-1 year"))->getFeed();
236
237
        return count($feed) > 0;
238
    }
239
240
    private function checkPublicIds(array $publicIds): bool
241
    {
242
        $deduplicated = array_unique($publicIds);
243
        return count($deduplicated) == count($publicIds);
244
    }
245
246
    private function configureOutput(OutputInterface $output): void
247
    {
248
        $output->getFormatter()->setStyle(
249
            'ko',
250
            new OutputFormatterStyle('red', null, ['bold'])
251
        );
252
253
        $output->getFormatter()->setStyle(
254
            'ok',
255
            new OutputFormatterStyle('green', null, ['bold'])
256
        );
257
258
        $this->ok = $output->getFormatter()->format('<ok>OK</ok>');
259
        $this->notOk = $output->getFormatter()->format("<ko>NOT OK</ko>");
260
    }
261
262
    private function printResult(SymfonyStyle $io, string $message, bool $result): void
263
    {
264
        $o = $result ? $this->ok:$this->notOk;
265
        $t = strlen($message) > 24 ? "\t":"\t\t\t\t";
266
        $io->text("{$message}: {$t} [{$o}]");
267
    }
268
}
269