Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.76% |
59 / 68 |
|
20.00% |
1 / 5 |
CRAP | |
0.00% |
0 / 1 |
TrackingToolUpdater | |
86.76% |
59 / 68 |
|
20.00% |
1 / 5 |
19.84 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
syncStatusToDB | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
dbSyncStatusToConst | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
replaceEventTools | |
97.73% |
43 / 44 |
|
0.00% |
0 / 1 |
12 | |||
updateToolSyncStatus | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\TrackingTool; |
6 | |
7 | use InvalidArgumentException; |
8 | use LogicException; |
9 | use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper; |
10 | use Wikimedia\Rdbms\IDatabase; |
11 | |
12 | /** |
13 | * This class updates the information about tracking tools stored in our database. |
14 | */ |
15 | class TrackingToolUpdater { |
16 | public const SERVICE_NAME = 'CampaignEventsTrackingToolUpdater'; |
17 | |
18 | private const SYNC_STATUS_TO_DB_MAP = [ |
19 | TrackingToolAssociation::SYNC_STATUS_UNKNOWN => 1, |
20 | TrackingToolAssociation::SYNC_STATUS_SYNCED => 2, |
21 | TrackingToolAssociation::SYNC_STATUS_FAILED => 3, |
22 | ]; |
23 | |
24 | private CampaignsDatabaseHelper $dbHelper; |
25 | |
26 | /** |
27 | * @param CampaignsDatabaseHelper $dbHelper |
28 | */ |
29 | public function __construct( CampaignsDatabaseHelper $dbHelper ) { |
30 | $this->dbHelper = $dbHelper; |
31 | } |
32 | |
33 | /** |
34 | * Converts a TrackingToolAssociation::SYNC_STATUS_* constant to the respective DB value |
35 | * @param int $status |
36 | * @return int |
37 | */ |
38 | public static function syncStatusToDB( int $status ): int { |
39 | if ( !isset( self::SYNC_STATUS_TO_DB_MAP[$status] ) ) { |
40 | throw new InvalidArgumentException( "Unknown sync status $status" ); |
41 | } |
42 | return self::SYNC_STATUS_TO_DB_MAP[$status]; |
43 | } |
44 | |
45 | /** |
46 | * Converts a DB value for ce_tracking_tools.cett_sync_status to the respective |
47 | * TrackingToolAssociation::SYNC_STATUS_* constant. |
48 | * @param int $dbVal |
49 | * @return int |
50 | */ |
51 | public static function dbSyncStatusToConst( int $dbVal ): int { |
52 | $const = array_search( $dbVal, self::SYNC_STATUS_TO_DB_MAP, true ); |
53 | if ( $const === false ) { |
54 | throw new InvalidArgumentException( "Unknown DB value for sync status: $dbVal" ); |
55 | } |
56 | return $const; |
57 | } |
58 | |
59 | /** |
60 | * Replaces the tools associated to an event with the given array of tool associations. |
61 | * |
62 | * @param int $eventID |
63 | * @param TrackingToolAssociation[] $tools |
64 | * @param IDatabase|null $dbw Optional, in case the caller opened an atomic section and wants to make sure |
65 | * that writes are done on the same DB handle. |
66 | * @return void |
67 | */ |
68 | public function replaceEventTools( int $eventID, array $tools, ?IDatabase $dbw = null ): void { |
69 | $dbw ??= $this->dbHelper->getDBConnection( DB_PRIMARY ); |
70 | |
71 | // Make a map of tools with faster lookup to compare existing values |
72 | $toolsMap = []; |
73 | foreach ( $tools as $toolAssociation ) { |
74 | $key = $toolAssociation->getToolID() . '|' . $toolAssociation->getToolEventID(); |
75 | $toolsMap[$key] = $toolAssociation; |
76 | } |
77 | |
78 | // Make changes by primary key to avoid lock contention |
79 | $currentToolRows = $dbw->newSelectQueryBuilder() |
80 | ->select( '*' ) |
81 | ->from( 'ce_tracking_tools' ) |
82 | ->where( [ 'cett_event' => $eventID ] ) |
83 | ->caller( __METHOD__ ) |
84 | ->fetchResultSet(); |
85 | |
86 | // TODO Add support for multiple tracking tools per event |
87 | if ( count( $currentToolRows ) > 1 && !defined( 'MW_PHPUNIT_TEST' ) ) { |
88 | throw new LogicException( "Events should have only one tracking tool." ); |
89 | } |
90 | |
91 | // Delete rows for tools that are no longer connected or with outdated sync info |
92 | $deleteIDs = []; |
93 | foreach ( $currentToolRows as $curRow ) { |
94 | $lookupKey = $curRow->cett_tool_id . '|' . $curRow->cett_tool_event_id; |
95 | $syncStatus = self::dbSyncStatusToConst( (int)$curRow->cett_sync_status ); |
96 | if ( |
97 | !isset( $toolsMap[$lookupKey] ) || |
98 | $syncStatus !== $toolsMap[$lookupKey]->getSyncStatus() || |
99 | wfTimestampOrNull( TS_UNIX, $curRow->cett_last_sync ) !== $toolsMap[$lookupKey]->getLastSyncTimestamp() |
100 | ) { |
101 | $deleteIDs[] = $curRow->cett_id; |
102 | } |
103 | } |
104 | |
105 | if ( $deleteIDs ) { |
106 | $dbw->newDeleteQueryBuilder() |
107 | ->deleteFrom( 'ce_tracking_tools' ) |
108 | ->where( [ 'cett_id' => $deleteIDs ] ) |
109 | ->caller( __METHOD__ ) |
110 | ->execute(); |
111 | } |
112 | |
113 | $newRows = []; |
114 | foreach ( $tools as $toolAssoc ) { |
115 | $syncTS = $toolAssoc->getLastSyncTimestamp(); |
116 | $newRows[] = [ |
117 | 'cett_event' => $eventID, |
118 | 'cett_tool_id' => $toolAssoc->getToolID(), |
119 | 'cett_tool_event_id' => $toolAssoc->getToolEventID(), |
120 | 'cett_sync_status' => self::syncStatusToDB( $toolAssoc->getSyncStatus() ), |
121 | 'cett_last_sync' => $syncTS !== null ? $dbw->timestamp( $syncTS ) : $syncTS, |
122 | ]; |
123 | } |
124 | |
125 | if ( $newRows ) { |
126 | // Insert the remaining rows. We can ignore conflicting rows in the database, as the checks above guarantee |
127 | // that they're identical to the new rows. |
128 | $dbw->newInsertQueryBuilder() |
129 | ->insertInto( 'ce_tracking_tools' ) |
130 | ->ignore() |
131 | ->rows( $newRows ) |
132 | ->caller( __METHOD__ ) |
133 | ->execute(); |
134 | } |
135 | } |
136 | |
137 | /** |
138 | * Updates the sync status of a tool in the database, updating the last sync timestamp if $status is |
139 | * SYNC_STATUS_SYNCED. |
140 | * |
141 | * @param int $eventID |
142 | * @param int $toolID |
143 | * @param string $toolEventID |
144 | * @param int $status One of the TrackingToolAssociation::SYNC_STATUS_* constants |
145 | */ |
146 | public function updateToolSyncStatus( int $eventID, int $toolID, string $toolEventID, int $status ): void { |
147 | $dbw = $this->dbHelper->getDBConnection( DB_PRIMARY ); |
148 | $setConds = [ |
149 | 'cett_sync_status' => self::syncStatusToDB( $status ), |
150 | ]; |
151 | if ( $status === TrackingToolAssociation::SYNC_STATUS_SYNCED ) { |
152 | $setConds['cett_last_sync'] = $dbw->timestamp(); |
153 | } |
154 | |
155 | $dbw->newUpdateQueryBuilder() |
156 | ->update( 'ce_tracking_tools' ) |
157 | ->set( $setConds ) |
158 | ->where( [ |
159 | 'cett_event' => $eventID, |
160 | 'cett_tool_id' => $toolID, |
161 | 'cett_tool_event_id' => $toolEventID |
162 | ] ) |
163 | ->caller( __METHOD__ ) |
164 | ->execute(); |
165 | } |
166 | } |