Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.77% covered (success)
96.77%
30 / 31
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActorCache
96.77% covered (success)
96.77%
30 / 31
85.71% covered (warning)
85.71%
6 / 7
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActorId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 remove
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 clear
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCachedValue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\User;
22
23/**
24 * Simple in-memory cache for UserIdentity objects indexed by user ID,
25 * actor ID and user name.
26 *
27 * We cannot just use MapCacheLRU for this because of eviction semantics:
28 * we need to be able to remove UserIdentity from the cache even if
29 * user ID or user name has changed, so we track the most accessed VALUES
30 * in the cache, not keys, and evict them alongside with all their indexes.
31 *
32 * @since 1.37
33 * @internal for use by ActorStore
34 * @package MediaWiki\User
35 */
36class ActorCache {
37
38    /** @var string Key by actor ID */
39    public const KEY_ACTOR_ID = 'actorId';
40
41    /** @var string Key by user ID */
42    public const KEY_USER_ID = 'userId';
43
44    /** @var string Key by user name */
45    public const KEY_USER_NAME = 'name';
46
47    private int $maxSize;
48
49    /**
50     * @var array[][] Contains 3 keys, KEY_ACTOR_ID, KEY_USER_ID, and KEY_USER_NAME,
51     * each of which has a value of an array of arrays with actor ids and UserIdentity objects,
52     * keyed with the corresponding actor id/user id/user name
53     */
54    private $cache = [ self::KEY_ACTOR_ID => [], self::KEY_USER_NAME => [], self::KEY_USER_ID => [] ];
55
56    /**
57     * @param int $maxSize hold up to this many UserIdentity values
58     */
59    public function __construct( int $maxSize ) {
60        $this->maxSize = $maxSize;
61    }
62
63    /**
64     * Get user identity which has $keyType equal to $keyValue
65     * @param string $keyType one of self::KEY_* constants.
66     * @param string|int $keyValue
67     * @return UserIdentity|null
68     */
69    public function getActor( string $keyType, $keyValue ): ?UserIdentity {
70        return $this->getCachedValue( $keyType, $keyValue )['actor'] ?? null;
71    }
72
73    /**
74     * Get actor ID of the user which has $keyType equal to $keyValue.
75     * @param string $keyType one of self::KEY_* constants.
76     * @param string|int $keyValue
77     * @return int|null
78     */
79    public function getActorId( string $keyType, $keyValue ): ?int {
80        return $this->getCachedValue( $keyType, $keyValue )['actorId'] ?? null;
81    }
82
83    /**
84     * Add $actor with $actorId to the cache.
85     * @param int $actorId
86     * @param UserIdentity $actor
87     */
88    public function add( int $actorId, UserIdentity $actor ) {
89        while ( count( $this->cache[self::KEY_ACTOR_ID] ) >= $this->maxSize ) {
90            $evictId = array_key_first( $this->cache[self::KEY_ACTOR_ID] );
91            $this->remove( $this->cache[self::KEY_ACTOR_ID][$evictId]['actor'] );
92        }
93        $value = [ 'actorId' => $actorId, 'actor' => $actor ];
94        $this->cache[self::KEY_ACTOR_ID][$actorId] = $value;
95        $userId = $actor->getId( $actor->getWikiId() );
96        if ( $userId ) {
97            $this->cache[self::KEY_USER_ID][$userId] = $value;
98        }
99        $this->cache[self::KEY_USER_NAME][$actor->getName()] = $value;
100    }
101
102    /**
103     * Remove $actor from cache.
104     * @param UserIdentity $actor
105     */
106    public function remove( UserIdentity $actor ) {
107        $oldByName = $this->cache[self::KEY_USER_NAME][$actor->getName()] ?? null;
108        $oldByUserId = $this->cache[self::KEY_USER_ID][$actor->getId( $actor->getWikiId() )] ?? null;
109        if ( $oldByName ) {
110            unset( $this->cache[self::KEY_USER_ID][$oldByName['actor']->getId( $oldByName['actor']->getWikiId() )] );
111            unset( $this->cache[self::KEY_ACTOR_ID][$oldByName['actorId']] );
112        }
113        if ( $oldByUserId ) {
114            unset( $this->cache[self::KEY_USER_NAME][$oldByUserId['actor']->getName()] );
115            unset( $this->cache[self::KEY_ACTOR_ID][$oldByUserId['actorId']] );
116        }
117        unset( $this->cache[self::KEY_USER_NAME][$actor->getName()] );
118        unset( $this->cache[self::KEY_USER_ID][$actor->getId( $actor->getWikiId() )] );
119    }
120
121    /**
122     * Remove everything from the cache.
123     * @internal
124     */
125    public function clear() {
126        $this->cache = [ self::KEY_ACTOR_ID => [], self::KEY_USER_NAME => [], self::KEY_USER_ID => [] ];
127    }
128
129    /**
130     * @param string $keyType one of self::KEY_* constants.
131     * @param string|int $keyValue
132     * @return array|null [ 'actor' => UserIdentity, 'actorId' => int ]
133     */
134    private function getCachedValue( string $keyType, $keyValue ): ?array {
135        if ( isset( $this->cache[$keyType][$keyValue] ) ) {
136            $cached = $this->cache[$keyType][$keyValue];
137            $actorId = $cached['actorId'];
138            // Record the actor with $actorId was recently used.
139            $item = $this->cache[self::KEY_ACTOR_ID][$actorId];
140            unset( $this->cache[self::KEY_ACTOR_ID][$actorId] );
141            $this->cache[self::KEY_ACTOR_ID][$actorId] = $item;
142            return $cached;
143        }
144        return null;
145    }
146}