Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.10% |
54 / 58 |
|
72.73% |
8 / 11 |
CRAP | |
0.00% |
0 / 1 |
MentorStore | |
93.10% |
54 / 58 |
|
72.73% |
8 / 11 |
16.08 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
makeLoadMentorCacheKey | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
invalidateMentorCache | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
validateMentorRole | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadMentorUser | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
loadMentorUserUncached | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getMenteesByMentor | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
hasAnyMentees | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
setMentorForUser | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
setMentorForUserInternal | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
dropMenteeRelationship | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isMentee | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
makeIsMenteeActiveCacheKey | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
invalidateIsMenteeActive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isMenteeActive | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
isMenteeActiveUncached | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
markMenteeAsActive | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
markMenteeAsInactive | n/a |
0 / 0 |
n/a |
0 / 0 |
0 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Mentorship\Store; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\User\UserIdentity; |
7 | use Psr\Log\LoggerAwareInterface; |
8 | use Psr\Log\LoggerAwareTrait; |
9 | use Psr\Log\NullLogger; |
10 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
11 | use Wikimedia\ObjectCache\WANObjectCache; |
12 | use Wikimedia\Rdbms\DBAccessObjectUtils; |
13 | use Wikimedia\Rdbms\IDBAccessObject; |
14 | |
15 | abstract class MentorStore implements ExpirationAwareness, LoggerAwareInterface { |
16 | use LoggerAwareTrait; |
17 | |
18 | /** @var string */ |
19 | public const ROLE_PRIMARY = 'primary'; |
20 | |
21 | /** @var string */ |
22 | public const ROLE_BACKUP = 'backup'; |
23 | |
24 | /** @var string[] */ |
25 | public const ROLES = [ |
26 | self::ROLE_PRIMARY, |
27 | self::ROLE_BACKUP |
28 | ]; |
29 | |
30 | /** @var WANObjectCache */ |
31 | protected WANObjectCache $wanCache; |
32 | /** @var array Cache key =>¨value; custom in-process cache */ |
33 | protected array $inProcessCache = []; |
34 | /** @var bool */ |
35 | protected bool $wasPosted; |
36 | |
37 | /** |
38 | * @param WANObjectCache $wanCache |
39 | * @param bool $wasPosted |
40 | */ |
41 | public function __construct( |
42 | WANObjectCache $wanCache, |
43 | bool $wasPosted |
44 | ) { |
45 | $this->wanCache = $wanCache; |
46 | $this->wasPosted = $wasPosted; |
47 | |
48 | $this->setLogger( new NullLogger() ); |
49 | } |
50 | |
51 | /** |
52 | * Helper to generate cache key for a mentee |
53 | * @param UserIdentity $user Mentee's username |
54 | * @param string $mentorRole |
55 | * @return string Cache key |
56 | */ |
57 | protected function makeLoadMentorCacheKey( |
58 | UserIdentity $user, |
59 | string $mentorRole |
60 | ): string { |
61 | return $this->wanCache->makeKey( |
62 | 'GrowthExperiments', |
63 | 'MentorStore', __CLASS__, |
64 | 'Mentee', $user->getId(), |
65 | 'Mentor', $mentorRole |
66 | ); |
67 | } |
68 | |
69 | /** |
70 | * Invalidates mentor cache for loadMentorUser |
71 | * @param UserIdentity $user Who will have their cache invalidated |
72 | * @param string $mentorRole |
73 | */ |
74 | protected function invalidateMentorCache( UserIdentity $user, string $mentorRole ): void { |
75 | $key = $this->makeLoadMentorCacheKey( $user, $mentorRole ); |
76 | $this->wanCache->delete( |
77 | $key |
78 | ); |
79 | unset( $this->inProcessCache[$key] ); |
80 | } |
81 | |
82 | /** |
83 | * Code helper for validating mentor type |
84 | * |
85 | * @param string $mentorRole |
86 | * @return bool True when valid, false otherwise |
87 | */ |
88 | private function validateMentorRole( string $mentorRole ): bool { |
89 | return in_array( $mentorRole, self::ROLES ); |
90 | } |
91 | |
92 | /** |
93 | * Get the mentor assigned to this user, if it exists. |
94 | * @param UserIdentity $mentee |
95 | * @param string $mentorRole One of MentorStore::ROLE_* constants |
96 | * @param int $flags bit field, see IDBAccessObject::READ_XXX |
97 | * @return UserIdentity|null |
98 | */ |
99 | public function loadMentorUser( |
100 | UserIdentity $mentee, |
101 | string $mentorRole, |
102 | $flags = IDBAccessObject::READ_NORMAL |
103 | ): ?UserIdentity { |
104 | if ( !$this->validateMentorRole( $mentorRole ) ) { |
105 | throw new InvalidArgumentException( "Invalid \$mentorRole passed: $mentorRole" ); |
106 | } |
107 | |
108 | if ( DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST ) ) { |
109 | $this->invalidateMentorCache( $mentee, $mentorRole ); |
110 | } |
111 | |
112 | $cacheKey = $this->makeLoadMentorCacheKey( $mentee, $mentorRole ); |
113 | if ( isset( $this->inProcessCache[$cacheKey] ) ) { |
114 | return $this->inProcessCache[$cacheKey]; |
115 | } |
116 | |
117 | $res = $this->wanCache->getWithSetCallback( |
118 | $cacheKey, |
119 | self::TTL_DAY, |
120 | function () use ( $mentee, $mentorRole, $flags ) { |
121 | return $this->loadMentorUserUncached( $mentee, $mentorRole, $flags ); |
122 | } |
123 | ); |
124 | $this->inProcessCache[$cacheKey] = $res; |
125 | return $res; |
126 | } |
127 | |
128 | /** |
129 | * Load mentor user with no cache |
130 | * |
131 | * @param UserIdentity $mentee |
132 | * @param string $mentorRole One of MentorStore::ROLE_* constants |
133 | * @param int $flags bit field, see IDBAccessObject::READ_XXX |
134 | * @return UserIdentity|null |
135 | */ |
136 | abstract protected function loadMentorUserUncached( |
137 | UserIdentity $mentee, |
138 | string $mentorRole, |
139 | $flags |
140 | ): ?UserIdentity; |
141 | |
142 | /** |
143 | * Return mentees who are mentored by given mentor |
144 | * |
145 | * @param UserIdentity $mentor |
146 | * @param string $mentorRole |
147 | * @param bool $includeHiddenUsers |
148 | * @param bool $includeInactiveUsers |
149 | * @param int $flags |
150 | * @return UserIdentity[] |
151 | */ |
152 | abstract public function getMenteesByMentor( |
153 | UserIdentity $mentor, |
154 | string $mentorRole, |
155 | bool $includeHiddenUsers = false, |
156 | bool $includeInactiveUsers = true, |
157 | int $flags = 0 |
158 | ): array; |
159 | |
160 | /** |
161 | * Checks whether a mentor has any mentees assigned |
162 | * |
163 | * @param UserIdentity $mentor |
164 | * @param string $mentorRole |
165 | * @param bool $includeHiddenUsers Deprecated (and ignored) since 1.43 |
166 | * @param int $flags |
167 | * @return bool |
168 | */ |
169 | abstract public function hasAnyMentees( |
170 | UserIdentity $mentor, |
171 | string $mentorRole, |
172 | bool $includeHiddenUsers = true, |
173 | int $flags = 0 |
174 | ): bool; |
175 | |
176 | /** |
177 | * Assign a mentor to this user, overriding any previous assignments. |
178 | * |
179 | * This method can be safely called on GET requests. |
180 | * |
181 | * The actual logic for changing mentor is in setMentorForUserInternal, this method |
182 | * only validates mentor type and calls the internal one. |
183 | * |
184 | * @param UserIdentity $mentee |
185 | * @param UserIdentity|null $mentor Null to drop the relationship |
186 | * @param string $mentorRole One of MentorStore::ROLE_* constants |
187 | */ |
188 | public function setMentorForUser( |
189 | UserIdentity $mentee, |
190 | ?UserIdentity $mentor, |
191 | string $mentorRole |
192 | ): void { |
193 | if ( !$this->validateMentorRole( $mentorRole ) ) { |
194 | throw new InvalidArgumentException( "Invalid \$mentorRole passed: $mentorRole" ); |
195 | } |
196 | |
197 | $this->setMentorForUserInternal( $mentee, $mentor, $mentorRole ); |
198 | |
199 | $this->invalidateMentorCache( $mentee, $mentorRole ); |
200 | $this->invalidateIsMenteeActive( $mentee ); |
201 | |
202 | // Set the mentor in the in-process cache |
203 | $this->inProcessCache[$this->makeLoadMentorCacheKey( $mentee, $mentorRole )] = $mentor; |
204 | } |
205 | |
206 | /** |
207 | * Actual logic for setting a mentor |
208 | * @param UserIdentity $mentee |
209 | * @param UserIdentity|null $mentor Set to null to drop the relationship |
210 | * @param string $mentorRole |
211 | */ |
212 | abstract protected function setMentorForUserInternal( |
213 | UserIdentity $mentee, |
214 | ?UserIdentity $mentor, |
215 | string $mentorRole |
216 | ): void; |
217 | |
218 | /** |
219 | * Drop mentor/mentee relationship for a given user |
220 | * |
221 | * @param UserIdentity $mentee |
222 | */ |
223 | public function dropMenteeRelationship( UserIdentity $mentee ): void { |
224 | foreach ( self::ROLES as $role ) { |
225 | $this->setMentorForUser( $mentee, null, $role ); |
226 | } |
227 | } |
228 | |
229 | /** |
230 | * Is an user considered a mentee? |
231 | * |
232 | * Equivalent to "Do they have a primary mentor assigned?" |
233 | * |
234 | * @param UserIdentity $user |
235 | * @param int $flags |
236 | * @return bool |
237 | */ |
238 | public function isMentee( |
239 | UserIdentity $user, |
240 | int $flags = IDBAccessObject::READ_NORMAL |
241 | ): bool { |
242 | return $this->loadMentorUser( |
243 | $user, |
244 | self::ROLE_PRIMARY, |
245 | $flags |
246 | ) !== null; |
247 | } |
248 | |
249 | /** |
250 | * Make cache key for isMenteeActive() |
251 | * |
252 | * @param UserIdentity $user |
253 | * @return string |
254 | */ |
255 | private function makeIsMenteeActiveCacheKey( UserIdentity $user ): string { |
256 | return $this->wanCache->makeKey( |
257 | 'GrowthExperiments', |
258 | 'MentorStore', __CLASS__, |
259 | 'Mentee', $user->getId(), |
260 | 'IsActive' |
261 | ); |
262 | } |
263 | |
264 | /** |
265 | * Invalidates cache for isMenteeActive() |
266 | * |
267 | * @param UserIdentity $user |
268 | */ |
269 | protected function invalidateIsMenteeActive( UserIdentity $user ): void { |
270 | $this->wanCache->delete( $this->makeIsMenteeActiveCacheKey( $user ) ); |
271 | } |
272 | |
273 | /** |
274 | * Is the mentee active? |
275 | * |
276 | * This will be used by MentorFilterHooks to only include |
277 | * recently active mentees, to avoid errors like T293182. |
278 | * |
279 | * A mentee should be marked as active if they edited less than |
280 | * $wgRCMaxAge seconds ago. |
281 | * |
282 | * @param UserIdentity $mentee |
283 | * @return bool|null |
284 | */ |
285 | public function isMenteeActive( UserIdentity $mentee ): ?bool { |
286 | return $this->wanCache->getWithSetCallback( |
287 | $this->makeIsMenteeActiveCacheKey( $mentee ), |
288 | self::TTL_DAY, |
289 | function () use ( $mentee ) { |
290 | return $this->isMenteeActiveUncached( $mentee ); |
291 | } |
292 | ); |
293 | } |
294 | |
295 | /** |
296 | * Is the mentee active? |
297 | * |
298 | * Bypasses caching. |
299 | * |
300 | * @see MentorStore::isMenteeActive() |
301 | * @param UserIdentity $mentee |
302 | * @return bool|null |
303 | */ |
304 | abstract protected function isMenteeActiveUncached( UserIdentity $mentee ): ?bool; |
305 | |
306 | /** |
307 | * Mark the mentee as active |
308 | * |
309 | * This will be used by MentorFilterHooks to only include |
310 | * recently active mentees, to avoid errors like T293182. |
311 | * |
312 | * A mentee should be marked as active if they edited less than |
313 | * $wgRCMaxAge seconds ago. |
314 | * |
315 | * Method should only make a write query if the mentee is not |
316 | * already marked as active. |
317 | * |
318 | * @param UserIdentity $mentee |
319 | */ |
320 | abstract public function markMenteeAsActive( UserIdentity $mentee ): void; |
321 | |
322 | /** |
323 | * Mark a mentee as inactive |
324 | * |
325 | * This will be used by MentorFilterHooks to only include |
326 | * recently active mentees, to avoid errors like T293182. |
327 | * |
328 | * A mentee should be marked as inactive if they edited more than |
329 | * $wgRCMaxAge seconds ago. |
330 | * |
331 | * Method should only make a write query if the mentee is not |
332 | * already marked as inactive. |
333 | * |
334 | * @param UserIdentity $mentee |
335 | */ |
336 | abstract public function markMenteeAsInactive( UserIdentity $mentee ): void; |
337 | } |