Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.10% covered (success)
93.10%
54 / 58
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
MentorStore
93.10% covered (success)
93.10%
54 / 58
72.73% covered (warning)
72.73%
8 / 11
16.08
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 makeLoadMentorCacheKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 invalidateMentorCache
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 validateMentorRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadMentorUser
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
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% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 setMentorForUserInternal
n/a
0 / 0
n/a
0 / 0
0
 dropMenteeRelationship
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isMentee
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 makeIsMenteeActiveCacheKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 invalidateIsMenteeActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMenteeActive
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
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
3namespace GrowthExperiments\Mentorship\Store;
4
5use InvalidArgumentException;
6use MediaWiki\User\UserIdentity;
7use Psr\Log\LoggerAwareInterface;
8use Psr\Log\LoggerAwareTrait;
9use Psr\Log\NullLogger;
10use Wikimedia\LightweightObjectStore\ExpirationAwareness;
11use Wikimedia\ObjectCache\WANObjectCache;
12use Wikimedia\Rdbms\DBAccessObjectUtils;
13use Wikimedia\Rdbms\IDBAccessObject;
14
15abstract 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}