Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
RevisionStoreCacheRecord
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
4 / 4
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getVisibility
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 loadFreshRow
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * A RevisionStoreRecord loaded from the cache.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Revision;
10
11use MediaWiki\CommentStore\CommentStoreComment;
12use MediaWiki\Page\PageIdentity;
13use MediaWiki\Permissions\Authority;
14use MediaWiki\User\UserIdentity;
15
16/**
17 * A cached RevisionStoreRecord.  Ensures that changes performed "behind the back"
18 * of the cache do not cause the revision record to deliver stale data.
19 *
20 * @internal
21 * @since 1.33
22 */
23class RevisionStoreCacheRecord extends RevisionStoreRecord {
24
25    /**
26     * @var null|callable ( int $revId ): [ int $rev_deleted, UserIdentity $user ]
27     */
28    private $mCallback;
29
30    /**
31     * @note Avoid calling this constructor directly. Use the appropriate methods
32     * in RevisionStore instead.
33     *
34     * @param callable $callback Callback for loading data.
35     *        Signature: function ( int $revId ): [ int $rev_deleted, UserIdentity $user ]
36     *        This function will only be called once.
37     * @param PageIdentity $page The page this RevisionRecord is associated with.
38     * @param UserIdentity $user
39     * @param CommentStoreComment $comment
40     * @param \stdClass $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
41     *        a query that yields the required fields.
42     * @param RevisionSlots $slots The slots of this revision.
43     * @param false|string $wikiID Relevant wiki id or self::LOCAL for the current one.
44     */
45    public function __construct(
46        callable $callback,
47        PageIdentity $page,
48        UserIdentity $user,
49        CommentStoreComment $comment,
50        $row,
51        RevisionSlots $slots,
52        $wikiID = self::LOCAL
53    ) {
54        parent::__construct( $page, $user, $comment, $row, $slots, $wikiID );
55        $this->mCallback = $callback;
56    }
57
58    /**
59     * Overridden to ensure that we return a fresh value and not a cached one.
60     *
61     * @return int
62     */
63    public function getVisibility() {
64        if ( $this->mCallback ) {
65            $this->loadFreshRow();
66        }
67        return parent::getVisibility();
68    }
69
70    /** @inheritDoc */
71    public function getUser( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
72        if ( $this->mCallback ) {
73            $this->loadFreshRow();
74        }
75        return parent::getUser( $audience, $performer );
76    }
77
78    /**
79     * Load a fresh row from the database to ensure we return updated information.
80     * Once loading a fresh row is attempted on this RevisionStoreCacheRecord instance,
81     * it will not be attempted again. Subsequent calls to methods on this instance
82     * will not go back to the database for a 'fresh row'.
83     *
84     * If a RevisionAccessException is thrown and is caught in the caller, it is possible
85     * to continue using this RevisionStoreCacheRecord instance to access RevisionRecord
86     * data, but there is no guarantee that the data will be 'fresh'.
87     * See: https://phabricator.wikimedia.org/T400380#11207694
88     *
89     * @throws RevisionAccessException if the row could not be loaded
90     */
91    private function loadFreshRow() {
92        [ $freshRevDeleted, $freshUser ] = ( $this->mCallback )( $this->mId );
93
94        // Set to null to ensure we do not make unnecessary queries for subsequent getter calls,
95        // and to allow the closure to be freed.
96        $this->mCallback = null;
97
98        if ( $freshRevDeleted !== null && $freshUser !== null ) {
99            $this->mDeleted = intval( $freshRevDeleted );
100            $this->mUser = $freshUser;
101        } else {
102            throw new RevisionAccessException(
103                'Unable to load fresh row for rev_id: {rev_id}',
104                [ 'rev_id' => $this->mId ]
105            );
106        }
107    }
108
109}