Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.87% covered (warning)
77.87%
95 / 122
27.27% covered (danger)
27.27%
3 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
DatabaseMentorStore
77.87% covered (warning)
77.87%
95 / 122
27.27% covered (danger)
27.27%
3 / 11
34.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 loadMentorUserUncached
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
4
 getDBByFlags
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getMenteesByMentor
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
4
 hasAnyMentees
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 setMentorForUserReal
64.00% covered (warning)
64.00%
16 / 25
0.00% covered (danger)
0.00%
0 / 1
2.19
 setMentorForUserInternal
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 setMenteeActiveFlag
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 isMenteeActiveUncached
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 markMenteeAsActive
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 markMenteeAsInactive
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace GrowthExperiments\Mentorship\Store;
4
5use JobQueueGroup;
6use MediaWiki\User\UserFactory;
7use MediaWiki\User\UserIdentity;
8use MediaWiki\User\UserIdentityLookup;
9use MediaWiki\User\UserIdentityValue;
10use Wikimedia\ObjectCache\WANObjectCache;
11use Wikimedia\Rdbms\IDatabase;
12use Wikimedia\Rdbms\IDBAccessObject;
13use Wikimedia\Rdbms\ILoadBalancer;
14
15class DatabaseMentorStore extends MentorStore {
16
17    private UserFactory $userFactory;
18    private UserIdentityLookup $userIdentityLookup;
19    private JobQueueGroup $jobQueueGroup;
20    private ILoadBalancer $loadBalancer;
21
22    /**
23     * @param WANObjectCache $wanCache
24     * @param UserFactory $userFactory
25     * @param UserIdentityLookup $userIdentityLookup
26     * @param JobQueueGroup $jobQueueGroup
27     * @param ILoadBalancer $loadBalancer
28     * @param bool $wasPosted
29     */
30    public function __construct(
31        WANObjectCache $wanCache,
32        UserFactory $userFactory,
33        UserIdentityLookup $userIdentityLookup,
34        JobQueueGroup $jobQueueGroup,
35        ILoadBalancer $loadBalancer,
36        bool $wasPosted
37    ) {
38        parent::__construct( $wanCache, $wasPosted );
39
40        $this->userFactory = $userFactory;
41        $this->userIdentityLookup = $userIdentityLookup;
42        $this->jobQueueGroup = $jobQueueGroup;
43        $this->loadBalancer = $loadBalancer;
44    }
45
46    /**
47     * @inheritDoc
48     */
49    public function loadMentorUserUncached(
50        UserIdentity $mentee,
51        string $mentorRole,
52        $flags
53    ): ?UserIdentity {
54        if ( ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
55            $db = $this->loadBalancer->getConnection( DB_PRIMARY );
56        } else {
57            $db = $this->loadBalancer->getConnection( DB_REPLICA );
58        }
59        $id = $db->newSelectQueryBuilder()
60            ->select( 'gemm_mentor_id' )
61            ->from( 'growthexperiments_mentor_mentee' )
62            ->where( [
63                'gemm_mentee_id' => $mentee->getId(),
64                'gemm_mentor_role' => $mentorRole,
65            ] )
66            ->recency( $flags )
67            ->caller( __METHOD__ )
68            ->fetchField();
69
70        if ( $id === false ) {
71            // No mentor in the database, return null
72            return null;
73        }
74
75        // Construct & return the user
76        $user = $this->userFactory->newFromId( $id );
77        // Return null if user does not exist
78        $user->load();
79        if ( !$user->getId() ) {
80            return null;
81        }
82        return new UserIdentityValue( $user->getId(), $user->getName() );
83    }
84
85    private function getDBByFlags( int $flags ): IDatabase {
86        if ( ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
87            $db = $this->loadBalancer->getConnection( DB_PRIMARY );
88        } else {
89            $db = $this->loadBalancer->getConnection( DB_REPLICA );
90        }
91        return $db;
92    }
93
94    /**
95     * @inheritDoc
96     */
97    public function getMenteesByMentor(
98        UserIdentity $mentor,
99        string $mentorRole,
100        bool $includeHiddenUsers = false,
101        bool $includeInactiveUsers = true,
102        int $flags = 0
103    ): array {
104        $db = $this->getDBByFlags( $flags );
105
106        $queryBuilder = $db->newSelectQueryBuilder()
107            ->select( 'gemm_mentee_id' )
108            ->from( 'growthexperiments_mentor_mentee' )
109            ->where( [ 'gemm_mentor_id' => $mentor->getId(), 'gemm_mentor_role' => $mentorRole ] );
110
111        if ( !$includeInactiveUsers ) {
112            $queryBuilder->andWhere( [ 'gemm_mentee_is_active' => true ] );
113        }
114
115        $ids = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
116
117        if ( $ids === [] ) {
118            return [];
119        }
120
121        $builder = $this->userIdentityLookup
122            ->newSelectQueryBuilder()
123            ->registered()
124            ->named()
125            // Ensure $ids are all numerical, otherwise the query might take forever (T375784)
126            ->whereUserIds( array_map( 'intval', $ids ) );
127
128        if ( !$includeHiddenUsers ) {
129            $builder->hidden( false );
130        }
131
132        return iterator_to_array( $builder
133            ->caller( __METHOD__ )
134            ->fetchUserIdentities() );
135    }
136
137    public function hasAnyMentees(
138        UserIdentity $mentor,
139        string $mentorRole,
140        bool $includeHiddenUsers = true,
141        int $flags = 0
142    ): bool {
143        if ( !$includeHiddenUsers ) {
144            wfDeprecated( __METHOD__ . ' called with $includeHiddenUsers = false', '1.43' );
145        }
146
147        return (bool)$this->getDBByFlags( $flags )
148            ->newSelectQueryBuilder()
149            ->select( 'gemm_mentee_id' )
150            ->from( 'growthexperiments_mentor_mentee' )
151            ->where( [ 'gemm_mentor_id' => $mentor->getId(), 'gemm_mentor_role' => $mentorRole ] )
152            ->limit( 1 )
153            ->recency( $flags )
154            ->caller( __METHOD__ )
155            ->fetchField();
156    }
157
158    /**
159     * Really set a mentor for a given user
160     *
161     * @param UserIdentity $mentee
162     * @param UserIdentity|null $mentor Set to null to drop the relationship
163     * @param string $mentorRole
164     */
165    private function setMentorForUserReal(
166        UserIdentity $mentee,
167        ?UserIdentity $mentor,
168        string $mentorRole
169    ): void {
170        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
171        if ( $mentor === null ) {
172            $dbw->newDeleteQueryBuilder()
173                ->deleteFrom( 'growthexperiments_mentor_mentee' )
174                ->where( [
175                    'gemm_mentee_id' => $mentee->getId(),
176                    'gemm_mentor_role' => $mentorRole,
177                ] )
178                ->caller( __METHOD__ )
179                ->execute();
180            return;
181        }
182        $dbw->newInsertQueryBuilder()
183            ->insertInto( 'growthexperiments_mentor_mentee' )
184            ->row( [
185                'gemm_mentee_id' => $mentee->getId(),
186                'gemm_mentor_id' => $mentor->getId(),
187                'gemm_mentor_role' => $mentorRole,
188            ] )
189            ->onDuplicateKeyUpdate()
190            ->uniqueIndexFields( [ 'gemm_mentee_id', 'gemm_mentor_role' ] )
191            ->set( [
192                'gemm_mentor_id' => $mentor->getId(),
193            ] )
194            ->caller( __METHOD__ )
195            ->execute();
196    }
197
198    /**
199     * @inheritDoc
200     */
201    protected function setMentorForUserInternal(
202        UserIdentity $mentee,
203        ?UserIdentity $mentor,
204        string $mentorRole
205    ): void {
206        if ( $this->wasPosted ) {
207            $this->setMentorForUserReal(
208                $mentee,
209                $mentor,
210                $mentorRole
211            );
212        } else {
213            $this->jobQueueGroup->lazyPush( new SetUserMentorDatabaseJob( [
214                'menteeId' => $mentee->getId(),
215                'mentorId' => $mentor ? $mentor->getId() : null,
216                'roleId' => $mentorRole,
217            ] ) );
218        }
219    }
220
221    /**
222     * Set gemm_mentee_is_active to true/false
223     *
224     * @param UserIdentity $mentee
225     * @param bool $isActive
226     */
227    private function setMenteeActiveFlag(
228        UserIdentity $mentee,
229        bool $isActive
230    ): void {
231        $this->loadBalancer->getConnection( DB_PRIMARY )->newUpdateQueryBuilder()
232            ->update( 'growthexperiments_mentor_mentee' )
233            ->set( [ 'gemm_mentee_is_active' => $isActive ] )
234            ->where( [
235                'gemm_mentee_id' => $mentee->getId(),
236                'gemm_mentor_role' => self::ROLE_PRIMARY,
237            ] )
238            ->caller( __METHOD__ )
239            ->execute();
240        $this->invalidateIsMenteeActive( $mentee );
241    }
242
243    /**
244     * @inheritDoc
245     */
246    protected function isMenteeActiveUncached( UserIdentity $mentee ): ?bool {
247        if ( !$this->isMentee( $mentee ) ) {
248            return null;
249        }
250
251        return (bool)$this->loadBalancer->getConnection( DB_REPLICA )->newSelectQueryBuilder()
252            ->select( 'gemm_mentee_is_active' )
253            ->from( 'growthexperiments_mentor_mentee' )
254            ->where( [
255                'gemm_mentee_id' => $mentee->getId(),
256                'gemm_mentor_role' => self::ROLE_PRIMARY,
257            ] )
258            ->caller( __METHOD__ )
259            ->fetchField();
260    }
261
262    /**
263     * @inheritDoc
264     */
265    public function markMenteeAsActive( UserIdentity $mentee ): void {
266        if ( $this->isMentee( $mentee ) && !$this->isMenteeActive( $mentee ) ) {
267            $this->setMenteeActiveFlag( $mentee, true );
268        }
269    }
270
271    /**
272     * @inheritDoc
273     */
274    public function markMenteeAsInactive( UserIdentity $mentee ): void {
275        if ( $this->isMentee( $mentee ) && $this->isMenteeActive( $mentee ) ) {
276            $this->setMenteeActiveFlag( $mentee, false );
277        }
278    }
279}