Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
48 / 51
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DatabaseUserImpactStore
94.12% covered (success)
94.12%
48 / 51
75.00% covered (warning)
75.00%
3 / 4
9.02
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
 getUserImpact
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 batchGetUserImpact
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
5.10
 setUserImpact
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace GrowthExperiments\UserImpact;
4
5use MediaWiki\User\UserIdentity;
6use Wikimedia\AtEase\AtEase;
7use Wikimedia\Rdbms\IDBAccessObject;
8use Wikimedia\Rdbms\ILoadBalancer;
9
10class DatabaseUserImpactStore implements UserImpactStore {
11
12    use ExpensiveUserImpactFallbackTrait;
13
14    /** @internal only exposed for tests */
15    public const TABLE_NAME = 'growthexperiments_user_impact';
16
17    private ILoadBalancer $loadBalancer;
18
19    /**
20     * @param ILoadBalancer $loadBalancer
21     */
22    public function __construct(
23        ILoadBalancer $loadBalancer
24    ) {
25        $this->loadBalancer = $loadBalancer;
26    }
27
28    /** @inheritDoc */
29    public function getUserImpact( UserIdentity $user, int $flags = IDBAccessObject::READ_NORMAL ): ?UserImpact {
30        $userId = $user->getId();
31        return $this->batchGetUserImpact( [ $userId ] )[$userId];
32    }
33
34    /**
35     * @param int[] $userIds
36     * @return (UserImpact|null)[] Map of user ID => UserImpact or null.
37     * @todo make this part of the interface
38     */
39    public function batchGetUserImpact( array $userIds ): array {
40        if ( !$userIds ) {
41            return [];
42        }
43
44        $userImpacts = array_fill_keys( $userIds, null );
45        $queryBuilder = $this->loadBalancer->getConnection( DB_REPLICA )->newSelectQueryBuilder()
46            ->select( [ 'geui_user_id', 'geui_data' ] )
47            ->from( self::TABLE_NAME )
48            ->where( [ 'geui_user_id' => $userIds ] )
49            ->caller( __METHOD__ );
50        foreach ( $queryBuilder->fetchResultSet() as $row ) {
51            AtEase::suppressWarnings();
52            $userImpactArray = gzinflate( $row->geui_data );
53            AtEase::restoreWarnings();
54            if ( $userImpactArray === false ) {
55                $userImpactArray = $row->geui_data;
56            }
57            $userImpactArray = json_decode( $userImpactArray, true );
58
59            if ( ( $userImpactArray['@version'] ?? 0 ) !== UserImpact::VERSION ) {
60                continue;
61            }
62            $userImpacts[$row->geui_user_id] = UserImpact::newFromJsonArray( $userImpactArray );
63        }
64        return $userImpacts;
65    }
66
67    /**
68     * Saves the user impact to the database.
69     * @param UserImpact $userImpact
70     * @return void
71     */
72    public function setUserImpact( UserImpact $userImpact ): void {
73        $dbr = $this->loadBalancer->getConnection( DB_REPLICA );
74        $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
75
76        $data = [
77            'geui_data' => gzdeflate( json_encode( $userImpact, JSON_UNESCAPED_UNICODE ) ),
78            'geui_timestamp' => $dbw->timestamp( $userImpact->getGeneratedAt() ),
79        ];
80
81        $storedUserId = $dbr->newSelectQueryBuilder()
82            ->select( 'geui_user_id' )
83            ->from( self::TABLE_NAME )
84            ->where( [ 'geui_user_id' => $userImpact->getUser()->getId() ] )
85            ->caller( __METHOD__ )
86            ->fetchField();
87        if ( $storedUserId !== false ) {
88            $dbw->newUpdateQueryBuilder()
89                ->update( self::TABLE_NAME )
90                ->set( $data )
91                ->where( [ 'geui_user_id' => $userImpact->getUser()->getId() ] )
92                ->caller( __METHOD__ )
93                ->execute();
94        } else {
95            $dbw->newInsertQueryBuilder()
96                ->insertInto( self::TABLE_NAME )
97                ->row( [
98                    'geui_user_id' => $userImpact->getUser()->getId(),
99                ] + $data )
100                ->onDuplicateKeyUpdate()
101                ->uniqueIndexFields( 'geui_user_id' )
102                ->set( $data )
103                ->caller( __METHOD__ )
104                ->execute();
105        }
106    }
107
108}