Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.44% |
57 / 61 |
|
75.00% |
9 / 12 |
CRAP | |
0.00% |
0 / 1 |
MentorStore | |
93.44% |
57 / 61 |
|
75.00% |
9 / 12 |
17.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 | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
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 DBAccessObjectUtils; |
6 | use IDBAccessObject; |
7 | use InvalidArgumentException; |
8 | use MediaWiki\User\UserIdentity; |
9 | use Psr\Log\LoggerAwareInterface; |
10 | use Psr\Log\LoggerAwareTrait; |
11 | use Psr\Log\NullLogger; |
12 | use WANObjectCache; |
13 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
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 |
166 | * @param int $flags |
167 | * @return bool |
168 | */ |
169 | public function hasAnyMentees( |
170 | UserIdentity $mentor, |
171 | string $mentorRole, |
172 | bool $includeHiddenUsers = false, |
173 | int $flags = 0 |
174 | ): bool { |
175 | return $this->getMenteesByMentor( |
176 | $mentor, $mentorRole, $includeHiddenUsers, true, $flags |
177 | ) !== []; |
178 | } |
179 | |
180 | /** |
181 | * Assign a mentor to this user, overriding any previous assignments. |
182 | * |
183 | * This method can be safely called on GET requests. |
184 | * |
185 | * The actual logic for changing mentor is in setMentorForUserInternal, this method |
186 | * only validates mentor type and calls the internal one. |
187 | * |
188 | * @param UserIdentity $mentee |
189 | * @param UserIdentity|null $mentor Null to drop the relationship |
190 | * @param string $mentorRole One of MentorStore::ROLE_* constants |
191 | */ |
192 | public function setMentorForUser( |
193 | UserIdentity $mentee, |
194 | ?UserIdentity $mentor, |
195 | string $mentorRole |
196 | ): void { |
197 | if ( !$this->validateMentorRole( $mentorRole ) ) { |
198 | throw new InvalidArgumentException( "Invalid \$mentorRole passed: $mentorRole" ); |
199 | } |
200 | |
201 | $this->setMentorForUserInternal( $mentee, $mentor, $mentorRole ); |
202 | |
203 | $this->invalidateMentorCache( $mentee, $mentorRole ); |
204 | $this->invalidateIsMenteeActive( $mentee ); |
205 | |
206 | // Set the mentor in the in-process cache |
207 | $this->inProcessCache[$this->makeLoadMentorCacheKey( $mentee, $mentorRole )] = $mentor; |
208 | } |
209 | |
210 | /** |
211 | * Actual logic for setting a mentor |
212 | * @param UserIdentity $mentee |
213 | * @param UserIdentity|null $mentor Set to null to drop the relationship |
214 | * @param string $mentorRole |
215 | */ |
216 | abstract protected function setMentorForUserInternal( |
217 | UserIdentity $mentee, |
218 | ?UserIdentity $mentor, |
219 | string $mentorRole |
220 | ): void; |
221 | |
222 | /** |
223 | * Drop mentor/mentee relationship for a given user |
224 | * |
225 | * @param UserIdentity $mentee |
226 | */ |
227 | public function dropMenteeRelationship( UserIdentity $mentee ): void { |
228 | foreach ( self::ROLES as $role ) { |
229 | $this->setMentorForUser( $mentee, null, $role ); |
230 | } |
231 | } |
232 | |
233 | /** |
234 | * Is an user considered a mentee? |
235 | * |
236 | * Equivalent to "Do they have a primary mentor assigned?" |
237 | * |
238 | * @param UserIdentity $user |
239 | * @param int $flags |
240 | * @return bool |
241 | */ |
242 | public function isMentee( |
243 | UserIdentity $user, |
244 | int $flags = IDBAccessObject::READ_NORMAL |
245 | ): bool { |
246 | return $this->loadMentorUser( |
247 | $user, |
248 | self::ROLE_PRIMARY, |
249 | $flags |
250 | ) !== null; |
251 | } |
252 | |
253 | /** |
254 | * Make cache key for isMenteeActive() |
255 | * |
256 | * @param UserIdentity $user |
257 | * @return string |
258 | */ |
259 | private function makeIsMenteeActiveCacheKey( UserIdentity $user ): string { |
260 | return $this->wanCache->makeKey( |
261 | 'GrowthExperiments', |
262 | 'MentorStore', __CLASS__, |
263 | 'Mentee', $user->getId(), |
264 | 'IsActive' |
265 | ); |
266 | } |
267 | |
268 | /** |
269 | * Invalidates cache for isMenteeActive() |
270 | * |
271 | * @param UserIdentity $user |
272 | */ |
273 | protected function invalidateIsMenteeActive( UserIdentity $user ): void { |
274 | $this->wanCache->delete( $this->makeIsMenteeActiveCacheKey( $user ) ); |
275 | } |
276 | |
277 | /** |
278 | * Is the mentee active? |
279 | * |
280 | * This will be used by MentorFilterHooks to only include |
281 | * recently active mentees, to avoid errors like T293182. |
282 | * |
283 | * A mentee should be marked as active if they edited less than |
284 | * $wgRCMaxAge seconds ago. |
285 | * |
286 | * @param UserIdentity $mentee |
287 | * @return bool|null |
288 | */ |
289 | public function isMenteeActive( UserIdentity $mentee ): ?bool { |
290 | return $this->wanCache->getWithSetCallback( |
291 | $this->makeIsMenteeActiveCacheKey( $mentee ), |
292 | self::TTL_DAY, |
293 | function () use ( $mentee ) { |
294 | return $this->isMenteeActiveUncached( $mentee ); |
295 | } |
296 | ); |
297 | } |
298 | |
299 | /** |
300 | * Is the mentee active? |
301 | * |
302 | * Bypasses caching. |
303 | * |
304 | * @see MentorStore::isMenteeActive() |
305 | * @param UserIdentity $mentee |
306 | * @return bool|null |
307 | */ |
308 | abstract protected function isMenteeActiveUncached( UserIdentity $mentee ): ?bool; |
309 | |
310 | /** |
311 | * Mark the mentee as active |
312 | * |
313 | * This will be used by MentorFilterHooks to only include |
314 | * recently active mentees, to avoid errors like T293182. |
315 | * |
316 | * A mentee should be marked as active if they edited less than |
317 | * $wgRCMaxAge seconds ago. |
318 | * |
319 | * Method should only make a write query if the mentee is not |
320 | * already marked as active. |
321 | * |
322 | * @param UserIdentity $mentee |
323 | */ |
324 | abstract public function markMenteeAsActive( UserIdentity $mentee ): void; |
325 | |
326 | /** |
327 | * Mark a mentee as inactive |
328 | * |
329 | * This will be used by MentorFilterHooks to only include |
330 | * recently active mentees, to avoid errors like T293182. |
331 | * |
332 | * A mentee should be marked as inactive if they edited more than |
333 | * $wgRCMaxAge seconds ago. |
334 | * |
335 | * Method should only make a write query if the mentee is not |
336 | * already marked as inactive. |
337 | * |
338 | * @param UserIdentity $mentee |
339 | */ |
340 | abstract public function markMenteeAsInactive( UserIdentity $mentee ): void; |
341 | } |