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