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