Passed
Push — master ( 0e30e3...7b249e )
by MusikAnimal
07:20
created

Page::getPageviews()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0932

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 12
ccs 5
cts 7
cp 0.7143
crap 2.0932
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the Page class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
use DateTime;
11
12
/**
13
 * A Page is a single wiki page in one project.
14
 */
15
class Page extends Model
16
{
17
    /** @var string The page name as provided at instantiation. */
18
    protected $unnormalizedPageName;
19
20
    /** @var string[] Metadata about this page. */
21
    protected $pageInfo;
22
23
    /** @var string[] Revision history of this page. */
24
    protected $revisions;
25
26
    /** @var int Number of revisions for this page. */
27
    protected $numRevisions;
28
29
    /** @var string[] List of Wikidata sitelinks for this page. */
30
    protected $wikidataItems;
31
32
    /** @var int Number of Wikidata sitelinks for this page. */
33
    protected $numWikidataItems;
34
35
    /**
36
     * Page constructor.
37
     * @param Project $project
38
     * @param string $pageName
39
     */
40 42
    public function __construct(Project $project, string $pageName)
41
    {
42 42
        $this->project = $project;
43 42
        $this->unnormalizedPageName = $pageName;
44 42
    }
45
46
    /**
47
     * Unique identifier for this Page, to be used in cache keys.
48
     * Use of md5 ensures the cache key does not contain reserved characters.
49
     * @see Repository::getCacheKey()
50
     * @return string
51
     * @codeCoverageIgnore
52
     */
53
    public function getCacheKey(): string
54
    {
55
        return md5((string)$this->getId());
56
    }
57
58
    /**
59
     * Get basic information about this page from the repository.
60
     * @return array|null
61
     */
62 9
    protected function getPageInfo(): ?array
63
    {
64 9
        if (empty($this->pageInfo)) {
65 9
            $this->pageInfo = $this->getRepository()
66 9
                ->getPageInfo($this->project, $this->unnormalizedPageName);
0 ignored issues
show
Bug introduced by
The method getPageInfo() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

66
                ->/** @scrutinizer ignore-call */ getPageInfo($this->project, $this->unnormalizedPageName);
Loading history...
67
        }
68 9
        return $this->pageInfo;
69
    }
70
71
    /**
72
     * Get the page's title.
73
     * @param bool $useUnnormalized Use the unnormalized page title to avoid an API call. This should be used only if
74
     *   you fetched the page title via other means (SQL query), and is not from user input alone.
75
     * @return string
76
     */
77 3
    public function getTitle(bool $useUnnormalized = false): string
78
    {
79 3
        if ($useUnnormalized) {
80 1
            return $this->unnormalizedPageName;
81
        }
82 3
        $info = $this->getPageInfo();
83 3
        return $info['title'] ?? $this->unnormalizedPageName;
84
    }
85
86
    /**
87
     * Get the page's title without the namespace.
88
     * @return string
89
     */
90 1
    public function getTitleWithoutNamespace(): string
91
    {
92 1
        $info = $this->getPageInfo();
93 1
        $title = $info['title'] ?? $this->unnormalizedPageName;
94 1
        $nsName = $this->getNamespaceName();
95 1
        return str_replace($nsName . ':', '', $title);
96
    }
97
98
    /**
99
     * Get this page's database ID.
100
     * @return int|null Null if nonexistent.
101
     */
102 1
    public function getId(): ?int
103
    {
104 1
        $info = $this->getPageInfo();
105 1
        return isset($info['pageid']) ? (int)$info['pageid'] : null;
106
    }
107
108
    /**
109
     * Get this page's length in bytes.
110
     * @return int|null Null if nonexistent.
111
     */
112 1
    public function getLength(): ?int
113
    {
114 1
        $info = $this->getPageInfo();
115 1
        return isset($info['length']) ? (int)$info['length'] : null;
116
    }
117
118
    /**
119
     * Get HTML for the stylized display of the title.
120
     * The text will be the same as Page::getTitle().
121
     * @return string
122
     */
123 1
    public function getDisplayTitle(): string
124
    {
125 1
        $info = $this->getPageInfo();
126 1
        if (isset($info['displaytitle'])) {
127 1
            return $info['displaytitle'];
128
        }
129 1
        return $this->getTitle();
130
    }
131
132
    /**
133
     * Get the full URL of this page.
134
     * @return string|null Null if nonexistent.
135
     */
136 1
    public function getUrl(): ?string
137
    {
138 1
        $info = $this->getPageInfo();
139 1
        return $info['fullurl'] ?? null;
140
    }
141
142
    /**
143
     * Get the numerical ID of the namespace of this page.
144
     * @return int|null Null if page doesn't exist.
145
     */
146 2
    public function getNamespace(): ?int
147
    {
148 2
        $info = $this->getPageInfo();
149 2
        return isset($info['ns']) ? (int)$info['ns'] : null;
150
    }
151
152
    /**
153
     * Get the name of the namespace of this page.
154
     * @return string|null Null if could not be determined.
155
     */
156 1
    public function getNamespaceName(): ?string
157
    {
158 1
        $info = $this->getPageInfo();
159 1
        return isset($info['ns'])
160 1
            ? ($this->getProject()->getNamespaces()[$info['ns']] ?? null)
161 1
            : null;
162
    }
163
164
    /**
165
     * Get the number of page watchers.
166
     * @return int|null Null if unknown.
167
     */
168 1
    public function getWatchers(): ?int
169
    {
170 1
        $info = $this->getPageInfo();
171 1
        return isset($info['watchers']) ? (int)$info['watchers'] : null;
172
    }
173
174
    /**
175
     * Get the HTML content of the body of the page.
176
     * @param DateTime|int $target If a DateTime object, the
177
     *   revision at that time will be returned. If an integer, it is
178
     *   assumed to be the actual revision ID.
179
     * @return string
180
     */
181 1
    public function getHTMLContent($target = null): string
182
    {
183 1
        if (is_a($target, 'DateTime')) {
184
            $target = $this->getRepository()->getRevisionIdAtDate($this, $target);
0 ignored issues
show
Bug introduced by
The method getRevisionIdAtDate() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

184
            $target = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionIdAtDate($this, $target);
Loading history...
185
        }
186 1
        return $this->getRepository()->getHTMLContent($this, $target);
0 ignored issues
show
Bug introduced by
The method getHTMLContent() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
        return $this->getRepository()->/** @scrutinizer ignore-call */ getHTMLContent($this, $target);
Loading history...
187
    }
188
189
    /**
190
     * Whether or not this page exists.
191
     * @return bool
192
     */
193 1
    public function exists(): bool
194
    {
195 1
        $info = $this->getPageInfo();
196 1
        return null !== $info && !isset($info['missing']) && !isset($info['invalid']) && !isset($info['interwiki']);
197
    }
198
199
    /**
200
     * Get the Project to which this page belongs.
201
     * @return Project
202
     */
203 16
    public function getProject(): Project
204
    {
205 16
        return $this->project;
206
    }
207
208
    /**
209
     * Get the language code for this page.
210
     * If not set, the language code for the project is returned.
211
     * @return string
212
     */
213 2
    public function getLang(): string
214
    {
215 2
        $info = $this->getPageInfo();
216 2
        if (isset($info['pagelanguage'])) {
217 2
            return $info['pagelanguage'];
218
        } else {
219
            return $this->getProject()->getLang();
220
        }
221
    }
222
223
    /**
224
     * Get the Wikidata ID of this page.
225
     * @return string|null Null if none exists.
226
     */
227 4
    public function getWikidataId(): ?string
228
    {
229 4
        $info = $this->getPageInfo();
230 4
        if (isset($info['pageprops']['wikibase_item'])) {
231 3
            return $info['pageprops']['wikibase_item'];
232
        } else {
233 1
            return null;
234
        }
235
    }
236
237
    /**
238
     * Get the number of revisions the page has.
239
     * @param User $user Optionally limit to those of this user.
240
     * @param false|int $start
241
     * @param false|int $end
242
     * @return int
243
     */
244 4
    public function getNumRevisions(?User $user = null, $start = false, $end = false): int
245
    {
246
        // If a user is given, we will not cache the result via instance variable.
247 4
        if (null !== $user) {
248
            return (int)$this->getRepository()->getNumRevisions($this, $user, $start, $end);
0 ignored issues
show
Bug introduced by
The method getNumRevisions() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

248
            return (int)$this->getRepository()->/** @scrutinizer ignore-call */ getNumRevisions($this, $user, $start, $end);
Loading history...
249
        }
250
251
        // Return cached value, if present.
252 4
        if (null !== $this->numRevisions) {
253
            return $this->numRevisions;
254
        }
255
256
        // Otherwise, return the count of all revisions if already present.
257 4
        if (null !== $this->revisions) {
258
            $this->numRevisions = count($this->revisions);
259
        } else {
260
            // Otherwise do a COUNT in the event fetching all revisions is not desired.
261 4
            $this->numRevisions = (int)$this->getRepository()->getNumRevisions($this, null, $start, $end);
262
        }
263
264 4
        return $this->numRevisions;
265
    }
266
267
    /**
268
     * Get all edits made to this page.
269
     * @param User|null $user Specify to get only revisions by the given user.
270
     * @param false|int $start
271
     * @param false|int $end
272
     * @return array
273
     */
274
    public function getRevisions(?User $user = null, $start = false, $end = false): array
275
    {
276
        if ($this->revisions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->revisions of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
277
            return $this->revisions;
278
        }
279
280
        $this->revisions = $this->getRepository()->getRevisions($this, $user, $start, $end);
0 ignored issues
show
Bug introduced by
The method getRevisions() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\EditSummaryRepository or AppBundle\Repository\GlobalContribsRepository or AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

280
        $this->revisions = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisions($this, $user, $start, $end);
Loading history...
281
282
        return $this->revisions;
283
    }
284
285
    /**
286
     * Get the full page wikitext.
287
     * @return string|null Null if nothing was found.
288
     */
289 1
    public function getWikitext(): ?string
290
    {
291 1
        $content = $this->getRepository()->getPagesWikitext(
0 ignored issues
show
Bug introduced by
The method getPagesWikitext() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

291
        $content = $this->getRepository()->/** @scrutinizer ignore-call */ getPagesWikitext(
Loading history...
292 1
            $this->getProject(),
293 1
            [ $this->getTitle() ]
294
        );
295
296 1
        return $content[$this->getTitle()] ?? null;
297
    }
298
299
    /**
300
     * Get the statement for a single revision, so that you can iterate row by row.
301
     * @see PageRepository::getRevisionsStmt()
302
     * @param User|null $user Specify to get only revisions by the given user.
303
     * @param int $limit Max number of revisions to process.
304
     * @param int $numRevisions Number of revisions, if known. This is used solely to determine the
305
     *   OFFSET if we are given a $limit. If $limit is set and $numRevisions is not set, a
306
     *   separate query is ran to get the nuber of revisions.
307
     * @param false|int $start
308
     * @param false|int $end
309
     * @return \Doctrine\DBAL\Driver\PDOStatement
310
     */
311
    public function getRevisionsStmt(
312
        ?User $user = null,
313
        ?int $limit = null,
314
        ?int $numRevisions = null,
315
        $start = false,
316
        $end = false
317
    ): \Doctrine\DBAL\Driver\PDOStatement {
318
        // If we have a limit, we need to know the total number of revisions so that PageRepo
319
        // will properly set the OFFSET. See PageRepository::getRevisionsStmt() for more info.
320
        if (isset($limit) && null === $numRevisions) {
321
            $numRevisions = $this->getNumRevisions($user, $start, $end);
322
        }
323
        return $this->getRepository()->getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
0 ignored issues
show
Bug introduced by
The method getRevisionsStmt() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

323
        return $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
Loading history...
324
    }
325
326
    /**
327
     * Get the revision ID that immediately precedes the given date.
328
     * @param DateTime $date
329
     * @return int|null Null if none found.
330
     */
331
    public function getRevisionIdAtDate(DateTime $date): ?int
332
    {
333
        return $this->getRepository()->getRevisionIdAtDate($this, $date);
334
    }
335
336
    /**
337
     * Get CheckWiki errors for this page
338
     * @return string[] See getErrors() for format
339
     */
340 1
    public function getCheckWikiErrors(): array
341
    {
342 1
        return $this->getRepository()->getCheckWikiErrors($this);
0 ignored issues
show
Bug introduced by
The method getCheckWikiErrors() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

342
        return $this->getRepository()->/** @scrutinizer ignore-call */ getCheckWikiErrors($this);
Loading history...
343
    }
344
345
    /**
346
     * Get Wikidata errors for this page
347
     * @return string[] See getErrors() for format
348
     */
349 2
    public function getWikidataErrors(): array
350
    {
351 2
        $errors = [];
352
353 2
        if (empty($this->getWikidataId())) {
354
            return [];
355
        }
356
357 2
        $wikidataInfo = $this->getRepository()->getWikidataInfo($this);
0 ignored issues
show
Bug introduced by
The method getWikidataInfo() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

357
        $wikidataInfo = $this->getRepository()->/** @scrutinizer ignore-call */ getWikidataInfo($this);
Loading history...
358
359 2
        $terms = array_map(function ($entry) {
360 2
            return $entry['term'];
361 2
        }, $wikidataInfo);
362
363 2
        $lang = $this->getLang();
364
365 2
        if (!in_array('label', $terms)) {
366
            $errors[] = [
367
                'prio' => 2,
368
                'name' => 'Wikidata',
369
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
370
                'explanation' => "See: <a target='_blank' " .
371
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
372
            ];
373
        }
374
375 2
        if (!in_array('description', $terms)) {
376 2
            $errors[] = [
377 2
                'prio' => 3,
378 2
                'name' => 'Wikidata',
379 2
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
380
                'explanation' => "See: <a target='_blank' " .
381
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
382
            ];
383
        }
384
385 2
        return $errors;
386
    }
387
388
    /**
389
     * Get Wikidata and CheckWiki errors, if present
390
     * @return string[] List of errors in the format:
391
     *    [[
392
     *         'prio' => int,
393
     *         'name' => string,
394
     *         'notice' => string (HTML),
395
     *         'explanation' => string (HTML)
396
     *     ], ... ]
397
     */
398 1
    public function getErrors(): array
399
    {
400
        // Includes label and description
401 1
        $wikidataErrors = $this->getWikidataErrors();
402
403 1
        $checkWikiErrors = $this->getCheckWikiErrors();
404
405 1
        return array_merge($wikidataErrors, $checkWikiErrors);
406
    }
407
408
    /**
409
     * Get all wikidata items for the page, not just languages of sister projects
410
     * @return string[]
411
     */
412 1
    public function getWikidataItems(): array
413
    {
414 1
        if (!is_array($this->wikidataItems)) {
0 ignored issues
show
introduced by
The condition is_array($this->wikidataItems) is always true.
Loading history...
415 1
            $this->wikidataItems = $this->getRepository()->getWikidataItems($this);
416
        }
417 1
        return $this->wikidataItems;
418
    }
419
420
    /**
421
     * Count wikidata items for the page, not just languages of sister projects
422
     * @return int Number of records.
423
     */
424 2
    public function countWikidataItems(): int
425
    {
426 2
        if (is_array($this->wikidataItems)) {
0 ignored issues
show
introduced by
The condition is_array($this->wikidataItems) is always true.
Loading history...
427
            $this->numWikidataItems = count($this->wikidataItems);
428 2
        } elseif (null === $this->numWikidataItems) {
429 2
            $this->numWikidataItems = (int)$this->getRepository()->countWikidataItems($this);
430
        }
431 2
        return $this->numWikidataItems;
432
    }
433
434
    /**
435
     * Get number of in and outgoing links and redirects to this page.
436
     * @return string[] Counts with keys 'links_ext_count', 'links_out_count', 'links_in_count' and 'redirects_count'.
437
     */
438 2
    public function countLinksAndRedirects(): array
439
    {
440 2
        return $this->getRepository()->countLinksAndRedirects($this);
0 ignored issues
show
Bug introduced by
The method countLinksAndRedirects() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

440
        return $this->getRepository()->/** @scrutinizer ignore-call */ countLinksAndRedirects($this);
Loading history...
441
    }
442
443
    /**
444
     * Get the sum of pageviews for the given page and timeframe.
445
     * @param string|DateTime $start In the format YYYYMMDD
446
     * @param string|DateTime $end In the format YYYYMMDD
447
     * @return int
448
     */
449 1
    public function getPageviews($start, $end): int
450
    {
451
        try {
452 1
            $pageviews = $this->getRepository()->getPageviews($this, $start, $end);
0 ignored issues
show
Bug introduced by
The method getPageviews() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\PageRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

452
            $pageviews = $this->getRepository()->/** @scrutinizer ignore-call */ getPageviews($this, $start, $end);
Loading history...
453
        } catch (\GuzzleHttp\Exception\ClientException $e) {
454
            // 404 means zero pageviews
455
            return 0;
456
        }
457
458 1
        return array_sum(array_map(function ($item) {
459 1
            return (int)$item['views'];
460 1
        }, $pageviews['items']));
461
    }
462
463
    /**
464
     * Get the sum of pageviews over the last N days
465
     * @param int $days Default 30
466
     * @return int Number of pageviews
467
     */
468 1
    public function getLastPageviews(int $days = 30): int
469
    {
470 1
        $start = date('Ymd', strtotime("-$days days"));
471 1
        $end = date('Ymd');
472 1
        return $this->getPageviews($start, $end);
473
    }
474
475
    /**
476
     * Is the page the project's Main Page?
477
     * @return bool
478
     */
479 1
    public function isMainPage(): bool
480
    {
481 1
        return $this->getProject()->getMainPage() === $this->getTitle();
482
    }
483
}
484