Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.64% covered (warning)
88.64%
273 / 308
38.89% covered (danger)
38.89%
7 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventStore
88.64% covered (warning)
88.64%
273 / 308
38.89% covered (danger)
38.89%
7 / 18
53.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getEventByID
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
3.01
 getEventByPage
82.14% covered (warning)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
3.05
 getEventAddressRow
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 getEventTrackingToolRow
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 getEventsByOrganizer
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 getEventsByParticipant
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 newEventsFromDBRows
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 getAddressRowsForEvents
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 getTrackingToolsRowsForEvents
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 newEventFromDBRow
97.83% covered (success)
97.83%
45 / 46
0.00% covered (danger)
0.00%
0 / 1
5
 saveRegistration
87.76% covered (warning)
87.76%
43 / 49
0.00% covered (danger)
0.00%
0 / 1
4.03
 updateStoredAddresses
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
4
 deleteRegistration
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 getMeetingTypeFromDBVal
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 meetingTypeToDBVal
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getEventStatusDBVal
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEventStatusFromDBVal
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\Event\Store;
6
7use DateTimeZone;
8use InvalidArgumentException;
9use LogicException;
10use MediaWiki\Extension\CampaignEvents\Address\AddressStore;
11use MediaWiki\Extension\CampaignEvents\Database\CampaignsDatabaseHelper;
12use MediaWiki\Extension\CampaignEvents\Event\EventRegistration;
13use MediaWiki\Extension\CampaignEvents\Event\ExistingEventRegistration;
14use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory;
15use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsPage;
16use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsStore;
17use MediaWiki\Extension\CampaignEvents\TrackingTool\TrackingToolAssociation;
18use MediaWiki\Extension\CampaignEvents\TrackingTool\TrackingToolUpdater;
19use MediaWiki\Extension\CampaignEvents\Utils;
20use RuntimeException;
21use stdClass;
22use Wikimedia\Rdbms\IDatabase;
23use Wikimedia\Rdbms\IDBAccessObject;
24
25/**
26 * @note Some pieces of code involving addresses may seem unnecessarily complex, but this is necessary because
27 * we will add support for multiple addresses (T321811).
28 */
29class EventStore implements IEventStore, IEventLookup {
30    private const EVENT_STATUS_MAP = [
31        EventRegistration::STATUS_OPEN => 1,
32        EventRegistration::STATUS_CLOSED => 2,
33    ];
34
35    private const EVENT_TYPE_MAP = [
36        EventRegistration::TYPE_GENERIC => 'generic',
37    ];
38
39    private const EVENT_MEETING_TYPE_MAP = [
40        EventRegistration::MEETING_TYPE_IN_PERSON => 1,
41        EventRegistration::MEETING_TYPE_ONLINE => 2,
42    ];
43
44    private CampaignsDatabaseHelper $dbHelper;
45    private CampaignsPageFactory $campaignsPageFactory;
46    private AddressStore $addressStore;
47    private TrackingToolUpdater $trackingToolUpdater;
48    private EventQuestionsStore $eventQuestionsStore;
49    private EventWikisStore $eventWikisStore;
50    private EventTopicsStore $eventTopicsStore;
51
52    /**
53     * @var array<int,ExistingEventRegistration> Cache of stored registrations, keyed by ID.
54     */
55    private array $cache = [];
56
57    public function __construct(
58        CampaignsDatabaseHelper $dbHelper,
59        CampaignsPageFactory $campaignsPageFactory,
60        AddressStore $addressStore,
61        TrackingToolUpdater $trackingToolUpdater,
62        EventQuestionsStore $eventQuestionsStore,
63        EventWikisStore $eventWikisStore,
64        EventTopicsStore $eventTopicsStore
65    ) {
66        $this->dbHelper = $dbHelper;
67        $this->campaignsPageFactory = $campaignsPageFactory;
68        $this->addressStore = $addressStore;
69        $this->trackingToolUpdater = $trackingToolUpdater;
70        $this->eventQuestionsStore = $eventQuestionsStore;
71        $this->eventWikisStore = $eventWikisStore;
72        $this->eventTopicsStore = $eventTopicsStore;
73    }
74
75    /**
76     * @inheritDoc
77     */
78    public function getEventByID( int $eventID ): ExistingEventRegistration {
79        if ( isset( $this->cache[$eventID] ) ) {
80            return $this->cache[$eventID];
81        }
82
83        $dbr = $this->dbHelper->getDBConnection( DB_REPLICA );
84        $eventRow = $dbr->newSelectQueryBuilder()
85            ->select( '*' )
86            ->from( 'campaign_events' )
87            ->where( [ 'event_id' => $eventID ] )
88            ->caller( __METHOD__ )
89            ->fetchRow();
90        if ( !$eventRow ) {
91            throw new EventNotFoundException( "Event $eventID not found" );
92        }
93
94        $this->cache[$eventID] = $this->newEventFromDBRow(
95            $eventRow,
96            $this->getEventAddressRow( $dbr, $eventID ),
97            $this->getEventTrackingToolRow( $dbr, $eventID ),
98            $this->eventWikisStore->getEventWikis( $eventID ),
99            $this->eventTopicsStore->getEventTopics( $eventID ),
100            $this->eventQuestionsStore->getEventQuestions( $eventID )
101        );
102        return $this->cache[$eventID];
103    }
104
105    /**
106     * @inheritDoc
107     */
108    public function getEventByPage(
109        ICampaignsPage $page,
110        int $readFlags = IDBAccessObject::READ_NORMAL
111    ): ExistingEventRegistration {
112        if ( ( $readFlags & IDBAccessObject::READ_LATEST ) === IDBAccessObject::READ_LATEST ) {
113            $db = $this->dbHelper->getDBConnection( DB_PRIMARY );
114        } else {
115            $db = $this->dbHelper->getDBConnection( DB_REPLICA );
116        }
117
118        $eventRow = $db->newSelectQueryBuilder()
119            ->select( '*' )
120            ->from( 'campaign_events' )
121            ->where( [
122                'event_page_namespace' => $page->getNamespace(),
123                'event_page_title' => $page->getDBkey(),
124                'event_page_wiki' => Utils::getWikiIDString( $page->getWikiId() ),
125            ] )
126            ->caller( __METHOD__ )
127            ->recency( $readFlags )
128            ->fetchRow();
129        if ( !$eventRow ) {
130            throw new EventNotFoundException(
131                "No event found for the given page (ns={$page->getNamespace()}" .
132                "dbkey={$page->getDBkey()}, wiki={$page->getWikiId()})"
133            );
134        }
135
136        $eventID = (int)$eventRow->event_id;
137        return $this->newEventFromDBRow(
138            $eventRow,
139            $this->getEventAddressRow( $db, $eventID ),
140            $this->getEventTrackingToolRow( $db, $eventID ),
141            $this->eventWikisStore->getEventWikis( $eventID ),
142            $this->eventTopicsStore->getEventTopics( $eventID ),
143            $this->eventQuestionsStore->getEventQuestions( $eventID )
144        );
145    }
146
147    /**
148     * @param IDatabase $db
149     * @param int $eventID
150     * @return stdClass|null
151     */
152    private function getEventAddressRow( IDatabase $db, int $eventID ): ?stdClass {
153        $addressRows = $db->newSelectQueryBuilder()
154            ->select( '*' )
155            ->from( 'ce_address' )
156            ->join( 'ce_event_address', null, [ 'ceea_address=cea_id', 'ceea_event' => $eventID ] )
157            ->caller( __METHOD__ )
158            ->fetchResultSet();
159
160        // TODO Add support for multiple addresses per event
161        if ( count( $addressRows ) > 1 ) {
162            throw new RuntimeException( 'Events should have only one address.' );
163        }
164
165        $addressRow = null;
166        foreach ( $addressRows as $row ) {
167            $addressRow = $row;
168            break;
169        }
170        return $addressRow;
171    }
172
173    /**
174     * @param IDatabase $db
175     * @param int $eventID
176     * @return stdClass|null
177     */
178    private function getEventTrackingToolRow( IDatabase $db, int $eventID ): ?stdClass {
179        $trackingToolsRows = $db->newSelectQueryBuilder()
180            ->select( '*' )
181            ->from( 'ce_tracking_tools' )
182            ->where( [ 'cett_event' => $eventID ] )
183            ->caller( __METHOD__ )
184            ->fetchResultSet();
185
186        // TODO Add support for multiple tracking tools per event
187        if ( count( $trackingToolsRows ) > 1 ) {
188            throw new RuntimeException( 'Events should have only one tracking tool.' );
189        }
190
191        $trackingToolRow = null;
192        foreach ( $trackingToolsRows as $row ) {
193            $trackingToolRow = $row;
194            break;
195        }
196        return $trackingToolRow;
197    }
198
199    /**
200     * @inheritDoc
201     */
202    public function getEventsByOrganizer( int $organizerID, ?int $limit = null ): array {
203        $dbr = $this->dbHelper->getDBConnection( DB_REPLICA );
204
205        $queryBuilder = $dbr->newSelectQueryBuilder()
206            ->select( '*' )
207            ->from( 'campaign_events' )
208            ->join( 'ce_organizers', null, [
209                'event_id=ceo_event_id',
210                'ceo_deleted_at' => null
211            ] )
212            ->where( [ 'ceo_user_id' => $organizerID ] )
213            ->orderBy( 'event_id' )
214            ->caller( __METHOD__ );
215        if ( $limit !== null ) {
216            $queryBuilder->limit( $limit );
217        }
218        $eventRows = $queryBuilder->fetchResultSet();
219
220        return $this->newEventsFromDBRows( $dbr, $eventRows );
221    }
222
223    /**
224     * @inheritDoc
225     */
226    public function getEventsByParticipant( int $participantID, ?int $limit = null ): array {
227        $dbr = $this->dbHelper->getDBConnection( DB_REPLICA );
228
229        $queryBuilder = $dbr->newSelectQueryBuilder()
230            ->select( '*' )
231            ->from( 'campaign_events' )
232            ->join( 'ce_participants', null, [
233                'event_id=cep_event_id',
234                // TODO Perhaps consider more granular permission check here.
235                'cep_private' => false,
236            ] )
237            ->where( [
238                'cep_user_id' => $participantID,
239                'cep_unregistered_at' => null,
240            ] )
241            ->orderBy( 'event_id' )
242            ->caller( __METHOD__ );
243        if ( $limit !== null ) {
244            $queryBuilder->limit( $limit );
245        }
246        $eventRows = $queryBuilder->fetchResultSet();
247
248        return $this->newEventsFromDBRows( $dbr, $eventRows );
249    }
250
251    /**
252     * @param IDatabase $db
253     * @param iterable<stdClass> $eventRows
254     * @return ExistingEventRegistration[]
255     */
256    private function newEventsFromDBRows( IDatabase $db, iterable $eventRows ): array {
257        $eventIDs = [];
258        foreach ( $eventRows as $eventRow ) {
259            $eventIDs[] = (int)$eventRow->event_id;
260        }
261
262        $addressRowsByEvent = $this->getAddressRowsForEvents( $db, $eventIDs );
263        $trackingToolRowsByEvent = $this->getTrackingToolsRowsForEvents( $db, $eventIDs );
264        $wikisByEvent = $this->eventWikisStore->getEventWikisMulti( $eventIDs );
265        $topicsByEvent = $this->eventTopicsStore->getEventTopicsMulti( $eventIDs );
266        $questionsByEvent = $this->eventQuestionsStore->getEventQuestionsMulti( $eventIDs );
267
268        $events = [];
269        foreach ( $eventRows as $row ) {
270            $curEventID = (int)$row->event_id;
271            $events[] = $this->newEventFromDBRow(
272                $row,
273                $addressRowsByEvent[$curEventID] ?? null,
274                $trackingToolRowsByEvent[$curEventID] ?? null,
275                $wikisByEvent[$curEventID],
276                $topicsByEvent[$curEventID],
277                $questionsByEvent[$curEventID]
278            );
279        }
280        return $events;
281    }
282
283    /**
284     * @param IDatabase $db
285     * @param int[] $eventIDs
286     * @return array<int,stdClass> Maps event IDs to the corresponding address row
287     */
288    private function getAddressRowsForEvents( IDatabase $db, array $eventIDs ): array {
289        $addressRows = $db->newSelectQueryBuilder()
290            ->select( '*' )
291            ->from( 'ce_address' )
292            ->join( 'ce_event_address', null, [ 'ceea_address=cea_id', 'ceea_event' => $eventIDs ] )
293            ->caller( __METHOD__ )
294            ->fetchResultSet();
295
296        $addressRowsByEvent = [];
297        foreach ( $addressRows as $addressRow ) {
298            $curEventID = (int)$addressRow->ceea_event;
299            if ( isset( $addressRowsByEvent[$curEventID] ) ) {
300                // TODO Add support for multiple addresses per event
301                throw new RuntimeException( "Event $curEventID should have only one address." );
302            }
303            $addressRowsByEvent[$curEventID] = $addressRow;
304        }
305        return $addressRowsByEvent;
306    }
307
308    /**
309     * @param IDatabase $db
310     * @param int[] $eventIDs
311     * @return array<int,stdClass> Maps event IDs to the corresponding tracking tool row
312     */
313    private function getTrackingToolsRowsForEvents(
314        IDatabase $db,
315        array $eventIDs
316    ): array {
317        $trackingToolsRows = $db->newSelectQueryBuilder()
318            ->select( '*' )
319            ->from( 'ce_tracking_tools' )
320            ->where( [ 'cett_event' => $eventIDs ] )
321            ->caller( __METHOD__ )
322            ->fetchResultSet();
323
324        $trackingToolsRowsByEvent = [];
325        foreach ( $trackingToolsRows as $trackingToolRow ) {
326            $curEventID = (int)$trackingToolRow->cett_event;
327            if ( isset( $trackingToolsRowsByEvent[$curEventID] ) ) {
328                // TODO Add support for multiple tracking tools per event
329                throw new RuntimeException( "Event $curEventID should have only one tracking tool." );
330            }
331            $trackingToolsRowsByEvent[$curEventID] = $trackingToolRow;
332        }
333        return $trackingToolsRowsByEvent;
334    }
335
336    /**
337     * @param stdClass $row
338     * @param stdClass|null $addressRow
339     * @param stdClass|null $trackingToolRow
340     * @param string[]|true $wikis List of wiki IDs or {@see EventRegistration::ALL_WIKIS}
341     * @param string[] $topics
342     * @param int[] $questionIDs
343     * @return ExistingEventRegistration
344     */
345    private function newEventFromDBRow(
346        stdClass $row,
347        ?stdClass $addressRow,
348        ?stdClass $trackingToolRow,
349        $wikis,
350        array $topics,
351        array $questionIDs
352    ): ExistingEventRegistration {
353        $eventPage = $this->campaignsPageFactory->newPageFromDB(
354            (int)$row->event_page_namespace,
355            $row->event_page_title,
356            $row->event_page_prefixedtext,
357            $row->event_page_wiki
358        );
359        $meetingType = self::getMeetingTypeFromDBVal( $row->event_meeting_type );
360
361        $address = null;
362        $country = null;
363        if ( $addressRow ) {
364            // TODO this is ugly and should be removed as soon as we remove the country on the front end
365            $address = explode( " \n ", $addressRow->cea_full_address );
366            array_pop( $address );
367            $address = implode( " \n ", $address );
368            $country = $addressRow->cea_country;
369        }
370
371        if ( $trackingToolRow ) {
372            $trackingTools = [
373                new TrackingToolAssociation(
374                    (int)$trackingToolRow->cett_tool_id,
375                    $trackingToolRow->cett_tool_event_id,
376                    TrackingToolUpdater::dbSyncStatusToConst( (int)$trackingToolRow->cett_sync_status ),
377                    wfTimestampOrNull( TS_UNIX, $trackingToolRow->cett_last_sync )
378                )
379            ];
380        } else {
381            $trackingTools = [];
382        }
383
384        return new ExistingEventRegistration(
385            (int)$row->event_id,
386            $row->event_name,
387            $eventPage,
388            $row->event_chat_url !== '' ? $row->event_chat_url : null,
389            $wikis,
390            $topics,
391            $trackingTools,
392            array_search( (int)$row->event_status, self::EVENT_STATUS_MAP, true ),
393            new DateTimeZone( $row->event_timezone ),
394            wfTimestamp( TS_MW, $row->event_start_local ),
395            wfTimestamp( TS_MW, $row->event_end_local ),
396            array_search( $row->event_type, self::EVENT_TYPE_MAP, true ),
397            $meetingType,
398            $row->event_meeting_url !== '' ? $row->event_meeting_url : null,
399            $country,
400            $address,
401            $questionIDs,
402            wfTimestamp( TS_UNIX, $row->event_created_at ),
403            wfTimestamp( TS_UNIX, $row->event_last_edit ),
404            wfTimestampOrNull( TS_UNIX, $row->event_deleted_at )
405        );
406    }
407
408    /**
409     * @inheritDoc
410     */
411    public function saveRegistration( EventRegistration $event ): int {
412        $dbw = $this->dbHelper->getDBConnection( DB_PRIMARY );
413        $curDBTimestamp = $dbw->timestamp();
414
415        $curCreationTS = $event->getCreationTimestamp();
416        $curDeletionTS = $event->getDeletionTimestamp();
417        // The local timestamps are already guaranteed to be in TS_MW format and the EventRegistration constructor
418        // enforces that, but convert them again as an extra safeguard to avoid any chance of storing garbage.
419        $localStartDB = wfTimestamp( TS_MW, $event->getStartLocalTimestamp() );
420        $localEndDB = wfTimestamp( TS_MW, $event->getEndLocalTimestamp() );
421        $newRow = [
422            'event_name' => $event->getName(),
423            'event_page_namespace' => $event->getPage()->getNamespace(),
424            'event_page_title' => $event->getPage()->getDBkey(),
425            'event_page_prefixedtext' => $event->getPage()->getPrefixedText(),
426            'event_page_wiki' => Utils::getWikiIDString( $event->getPage()->getWikiId() ),
427            'event_chat_url' => $event->getChatURL() ?? '',
428            'event_status' => self::EVENT_STATUS_MAP[$event->getStatus()],
429            'event_timezone' => $event->getTimezone()->getName(),
430            'event_start_local' => $localStartDB,
431            'event_start_utc' => $dbw->timestamp( $event->getStartUTCTimestamp() ),
432            'event_end_local' => $localEndDB,
433            'event_end_utc' => $dbw->timestamp( $event->getEndUTCTimestamp() ),
434            'event_type' => self::EVENT_TYPE_MAP[$event->getType()],
435            'event_meeting_type' => self::meetingTypeToDBVal( $event->getMeetingType() ),
436            'event_meeting_url' => $event->getMeetingURL() ?? '',
437            'event_created_at' => $curCreationTS ? $dbw->timestamp( $curCreationTS ) : $curDBTimestamp,
438            'event_last_edit' => $curDBTimestamp,
439            'event_deleted_at' => $curDeletionTS ? $dbw->timestamp( $curDeletionTS ) : null,
440        ];
441
442        $eventID = $event->getID();
443        $dbw->startAtomic( __METHOD__ );
444        if ( $eventID === null ) {
445            $dbw->newInsertQueryBuilder()
446                ->insertInto( 'campaign_events' )
447                ->row( $newRow )
448                ->caller( __METHOD__ )
449                ->execute();
450            $eventID = $dbw->insertId();
451        } else {
452            $dbw->newUpdateQueryBuilder()
453                ->update( 'campaign_events' )
454                ->set( $newRow )
455                ->where( [ 'event_id' => $eventID ] )
456                ->caller( __METHOD__ )
457                ->execute();
458        }
459
460        $this->updateStoredAddresses( $dbw, $event->getMeetingAddress(), $event->getMeetingCountry(), $eventID );
461        $this->trackingToolUpdater->replaceEventTools( $eventID, $event->getTrackingTools(), $dbw );
462        $this->eventQuestionsStore->replaceEventQuestions( $eventID, $event->getParticipantQuestions() );
463        $this->eventWikisStore->addOrUpdateEventWikis( $eventID, $event->getWikis() );
464        $this->eventTopicsStore->addOrUpdateEventTopics( $eventID, $event->getTopics() );
465
466        $dbw->endAtomic( __METHOD__ );
467
468        unset( $this->cache[$eventID] );
469
470        return $eventID;
471    }
472
473    /**
474     * @param IDatabase $dbw
475     * @param string|null $meetingAddress
476     * @param string|null $meetingCountry
477     * @param int $eventID
478     * @return void
479     */
480    private function updateStoredAddresses(
481        IDatabase $dbw,
482        ?string $meetingAddress,
483        ?string $meetingCountry,
484        int $eventID
485    ): void {
486        $where = [ 'ceea_event' => $eventID ];
487        if ( $meetingAddress || $meetingCountry ) {
488            $meetingAddress .= " \n " . $meetingCountry;
489            $where[] = $dbw->expr( 'cea_full_address', '!=', $meetingAddress );
490        }
491
492        $dbw->deleteJoin(
493            'ce_event_address',
494            'ce_address',
495            'ceea_address',
496            'cea_id',
497            $where,
498            __METHOD__
499        );
500
501        if ( $meetingAddress ) {
502            $addressID = $this->addressStore->acquireAddressID( $meetingAddress, $meetingCountry );
503            $dbw->newInsertQueryBuilder()
504                ->insertInto( 'ce_event_address' )
505                ->ignore()
506                ->row( [
507                    'ceea_event' => $eventID,
508                    'ceea_address' => $addressID
509                ] )
510                ->caller( __METHOD__ )
511                ->execute();
512        }
513    }
514
515    /**
516     * @inheritDoc
517     */
518    public function deleteRegistration( ExistingEventRegistration $registration ): bool {
519        $dbw = $this->dbHelper->getDBConnection( DB_PRIMARY );
520        $dbw->newUpdateQueryBuilder()
521            ->update( 'campaign_events' )
522            ->set( [ 'event_deleted_at' => $dbw->timestamp() ] )
523            ->where( [
524                'event_id' => $registration->getID(),
525                'event_deleted_at' => null
526            ] )
527            ->caller( __METHOD__ )
528            ->execute();
529        unset( $this->cache[$registration->getID()] );
530        return $dbw->affectedRows() > 0;
531    }
532
533    /**
534     * Converts a meeting type as stored in the DB into a combination of the EventRegistration::MEETING_TYPE_* constants
535     * @param string $dbMeetingType
536     * @return int
537     */
538    public static function getMeetingTypeFromDBVal( string $dbMeetingType ): int {
539        $ret = 0;
540        $dbMeetingTypeNum = (int)$dbMeetingType;
541        foreach ( self::EVENT_MEETING_TYPE_MAP as $eventVal => $dbVal ) {
542            if ( $dbMeetingTypeNum & $dbVal ) {
543                $ret |= $eventVal;
544            }
545        }
546        return $ret;
547    }
548
549    /**
550     * Converts an EventRegistration::MEETING_TYPE_* constant to the corresponding value used in the database.
551     * @param int $meetingType
552     * @return int
553     */
554    public static function meetingTypeToDBVal( int $meetingType ): int {
555        $dbMeetingType = 0;
556        foreach ( self::EVENT_MEETING_TYPE_MAP as $eventVal => $dbVal ) {
557            if ( $meetingType & $eventVal ) {
558                $dbMeetingType |= $dbVal;
559            }
560        }
561        return $dbMeetingType;
562    }
563
564    /**
565     * Converts an EventRegistration::STATUS_* constant into the respective DB value.
566     * @param string $eventStatus
567     * @return int
568     */
569    public static function getEventStatusDBVal( string $eventStatus ): int {
570        if ( isset( self::EVENT_STATUS_MAP[$eventStatus] ) ) {
571            return self::EVENT_STATUS_MAP[$eventStatus];
572        }
573        throw new LogicException( "Unknown status $eventStatus" );
574    }
575
576    /**
577     * Converts an event status as stored in the database to an EventRegistration::STATUS_* constant
578     * @param string $eventStatus
579     * @return string
580     */
581    public static function getEventStatusFromDBVal( string $eventStatus ): string {
582        $val = array_search( (int)$eventStatus, self::EVENT_STATUS_MAP, true );
583        if ( $val === false ) {
584            throw new InvalidArgumentException( "Unknown event status: $eventStatus" );
585        }
586        return $val;
587    }
588}