Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 283
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
SignageAdapter
0.00% covered (danger)
0.00%
0 / 283
0.00% covered (danger)
0.00%
0 / 20
5402
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
56
 syncDates
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 activate
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 deactivate
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 delete
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 getStatus
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 validateConfig
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
462
 getDefaultConfig
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 resolveSlideIds
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 mapSourceToTagType
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 resolveSource
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTagService
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getNextPosition
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addToLoop
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 addToSchedule
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 updateScheduleDates
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 deleteFromSchedule
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getLoopIdsByEvent
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\EventManagement\Adapters;
4
5use PDO;
6use PDOException;
7use DateTime;
8use BuyerKiosk\EventManagement\Models\Event;
9use BuyerKiosk\EventManagement\Models\EventIntegration;
10use BuyerKiosk\DigitalSign\Constants;
11use BuyerKiosk\DigitalSign\Services\SlideTagService;
12
13/**
14 * SignageAdapter - Manages digital signage slides for events
15 *
16 * This adapter integrates events with the digital signage system (dsLoop table).
17 * It supports adding slides to the loop by:
18 * - Direct slide IDs
19 * - Tag-based slide lookup
20 *
21 * When slides are added through this adapter, they receive:
22 * - eventId marking for traceability
23 * - start/expire dates matching the event period
24 * - Proper scheduling in the digitalSignSchedule table
25 *
26 * Target tables:
27 * - dsLoop (store DB): Slide playlist entries
28 * - digitalSignSchedule (global DB): Scheduled activations/expirations
29 *
30 * @package BuyerKiosk\EventManagement\Adapters
31 */
32class SignageAdapter extends AbstractAdapter
33{
34    /**
35     * Slide source mapping from config value to constant
36     */
37    private const SOURCE_MAP = [
38        'store' => Constants::SOURCE_STORE,
39        'corporate' => Constants::SOURCE_CORPORATE,
40        'hipbone' => Constants::SOURCE_HIPBONE,
41        'corp' => Constants::SOURCE_CORPORATE,
42        'hb' => Constants::SOURCE_HIPBONE,
43    ];
44
45    /**
46     * @var PDO Global database connection for digitalSignSchedule
47     */
48    private PDO $globalDb;
49
50    /**
51     * @var string Store typeNum for schedule entries
52     */
53    private string $typeNum;
54
55    /**
56     * @var SlideTagService|null Tag service instance (lazy loaded)
57     */
58    private ?SlideTagService $tagService = null;
59
60    /**
61     * Constructor
62     *
63     * @param PDO $db Store database connection
64     * @param PDO $globalDb Global database connection
65     * @param string $typeNum Store typeNum identifier
66     * @param string|null $timezone Store timezone
67     */
68    public function __construct(
69        PDO $db,
70        PDO $globalDb,
71        string $typeNum,
72        ?string $timezone = null
73    ) {
74        parent::__construct($db, $timezone);
75        $this->globalDb = $globalDb;
76        $this->typeNum = $typeNum;
77    }
78
79    /**
80     * Get the integration type this adapter handles
81     *
82     * @return string
83     */
84    public function getType(): string
85    {
86        return EventIntegration::TYPE_SIGNAGE;
87    }
88
89    /**
90     * Create signage integration - add slides to dsLoop
91     *
92     * Supports two modes:
93     * 1. Direct slide IDs: config['slideIds'] array
94     * 2. Tag-based: config['tags'] array (finds slides matching any tag)
95     *
96     * All slides added receive:
97     * - eventId set for traceability
98     * - start/expire dates from event
99     * - Entries in digitalSignSchedule for timed activation
100     *
101     * @param Event $event The event to link
102     * @param array $config Configuration with slideIds, tags, source, etc.
103     * @return int The first dsLoop entry ID created
104     * @throws IntegrationException On failure
105     */
106    public function create(Event $event, array $config): int
107    {
108        $errors = $this->validateConfig($config);
109        if (!empty($errors)) {
110            throw IntegrationException::invalidConfig($this->getType(), $errors);
111        }
112
113        $slideIds = $this->resolveSlideIds($config);
114        if (empty($slideIds)) {
115            throw IntegrationException::createFailed(
116                $this->getType(),
117                'No slides found matching the configuration',
118                $config
119            );
120        }
121
122        $source = $this->resolveSource($config);
123        $duration = $this->getConfigValue($config, 'duration', Constants::DEFAULT_DURATION);
124        $displayOrder = $this->getConfigValue($config, 'displayOrder', null);
125
126        // Calculate dates from event
127        $startDate = $this->formatDateOnly($event->startDate);
128        $expireDate = $this->formatDateOnly($event->endDate);
129
130        try {
131            $this->beginTransaction();
132
133            $firstLoopId = null;
134            $position = $displayOrder ?? $this->getNextPosition();
135
136            foreach ($slideIds as $slideId) {
137                $loopId = $this->addToLoop(
138                    $slideId,
139                    $source,
140                    $position,
141                    $duration,
142                    $startDate,
143                    $expireDate,
144                    $event->id
145                );
146
147                if ($loopId === null) {
148                    $this->rollback();
149                    throw IntegrationException::createFailed(
150                        $this->getType(),
151                        "Failed to add slide {$slideId} to loop",
152                        ['slideId' => $slideId]
153                    );
154                }
155
156                if ($firstLoopId === null) {
157                    $firstLoopId = $loopId;
158                }
159
160                // Add to global schedule for timed activation
161                $this->addToSchedule($loopId, $startDate, $expireDate);
162
163                $position++;
164            }
165
166            $this->commit();
167
168            $this->logError("Created signage integration", [
169                'eventId' => $event->id,
170                'slideCount' => count($slideIds),
171                'firstLoopId' => $firstLoopId,
172            ]);
173
174            return $firstLoopId;
175
176        } catch (PDOException $e) {
177            $this->rollback();
178            throw IntegrationException::createFailed(
179                $this->getType(),
180                'Database error: ' . $e->getMessage(),
181                $config,
182                $e
183            );
184        }
185    }
186
187    /**
188     * Sync dates when event dates change
189     *
190     * Updates all dsLoop entries and digitalSignSchedule entries
191     * linked to this event with new start/expire dates.
192     *
193     * @param Event $event The event with updated dates
194     * @param EventIntegration $integration The integration record
195     * @throws IntegrationException On failure
196     */
197    public function syncDates(Event $event, EventIntegration $integration): void
198    {
199        $startDate = $this->formatDateOnly($event->startDate);
200        $expireDate = $this->formatDateOnly($event->endDate);
201
202        try {
203            $this->beginTransaction();
204
205            // Update all dsLoop entries for this event
206            $sql = "UPDATE dsLoop
207                    SET startDate = :startDate,
208                        expireDate = :expireDate,
209                        scheduled = CASE
210                            WHEN :startDate <= CURDATE() THEN 0
211                            ELSE 1
212                        END
213                    WHERE eventId = :eventId";
214
215            $stmt = $this->db->prepare($sql);
216            $stmt->execute([
217                ':startDate' => $startDate,
218                ':expireDate' => $expireDate,
219                ':eventId' => $event->id,
220            ]);
221
222            // Update digitalSignSchedule entries
223            $this->updateScheduleDates($event->id, $startDate, $expireDate);
224
225            $this->commit();
226
227            $this->logError("Synced signage dates", [
228                'eventId' => $event->id,
229                'startDate' => $startDate,
230                'expireDate' => $expireDate,
231                'updatedCount' => $stmt->rowCount(),
232            ]);
233
234        } catch (PDOException $e) {
235            $this->rollback();
236            throw IntegrationException::syncDatesFailed(
237                $this->getType(),
238                $integration->foreignId,
239                'Database error: ' . $e->getMessage(),
240                $e
241            );
242        }
243    }
244
245    /**
246     * Activate the signage integration
247     *
248     * Sets all slides for this event to active (scheduled = 0).
249     *
250     * @param Event $event The event being activated
251     * @param EventIntegration $integration The integration to activate
252     * @throws IntegrationException On failure
253     */
254    public function activate(Event $event, EventIntegration $integration): void
255    {
256        try {
257            $sql = "UPDATE dsLoop
258                    SET scheduled = :scheduled
259                    WHERE eventId = :eventId";
260
261            $stmt = $this->db->prepare($sql);
262            $stmt->execute([
263                ':scheduled' => Constants::SCHEDULE_ACTIVE,
264                ':eventId' => $event->id,
265            ]);
266
267            $this->logError("Activated signage integration", [
268                'eventId' => $event->id,
269                'activatedCount' => $stmt->rowCount(),
270            ]);
271
272        } catch (PDOException $e) {
273            throw IntegrationException::activateFailed(
274                $this->getType(),
275                $integration->foreignId,
276                'Database error: ' . $e->getMessage(),
277                $e
278            );
279        }
280    }
281
282    /**
283     * Deactivate the signage integration
284     *
285     * Sets all slides for this event to pending/inactive (scheduled = 1).
286     *
287     * @param Event $event The event being deactivated
288     * @param EventIntegration $integration The integration to deactivate
289     * @throws IntegrationException On failure
290     */
291    public function deactivate(Event $event, EventIntegration $integration): void
292    {
293        try {
294            $sql = "UPDATE dsLoop
295                    SET scheduled = :scheduled
296                    WHERE eventId = :eventId";
297
298            $stmt = $this->db->prepare($sql);
299            $stmt->execute([
300                ':scheduled' => Constants::SCHEDULE_PENDING,
301                ':eventId' => $event->id,
302            ]);
303
304            $this->logError("Deactivated signage integration", [
305                'eventId' => $event->id,
306                'deactivatedCount' => $stmt->rowCount(),
307            ]);
308
309        } catch (PDOException $e) {
310            throw IntegrationException::deactivateFailed(
311                $this->getType(),
312                $integration->foreignId,
313                'Database error: ' . $e->getMessage(),
314                $e
315            );
316        }
317    }
318
319    /**
320     * Delete the signage integration
321     *
322     * Removes all dsLoop entries and digitalSignSchedule entries
323     * linked to this event.
324     *
325     * @param EventIntegration $integration The integration to delete
326     * @throws IntegrationException On failure
327     */
328    public function delete(EventIntegration $integration): void
329    {
330        try {
331            $this->beginTransaction();
332
333            // Get all loop IDs for this event before deletion (for schedule cleanup)
334            $loopIds = $this->getLoopIdsByEvent($integration->eventId);
335
336            // Delete from dsLoop
337            $sql = "DELETE FROM dsLoop WHERE eventId = :eventId";
338            $stmt = $this->db->prepare($sql);
339            $stmt->execute([':eventId' => $integration->eventId]);
340
341            $deletedCount = $stmt->rowCount();
342
343            // Delete from digitalSignSchedule
344            foreach ($loopIds as $loopId) {
345                $this->deleteFromSchedule($loopId);
346            }
347
348            $this->commit();
349
350            $this->logError("Deleted signage integration", [
351                'eventId' => $integration->eventId,
352                'deletedCount' => $deletedCount,
353            ]);
354
355        } catch (PDOException $e) {
356            $this->rollback();
357            throw IntegrationException::deleteFailed(
358                $this->getType(),
359                $integration->foreignId,
360                'Database error: ' . $e->getMessage(),
361                $e
362            );
363        }
364    }
365
366    /**
367     * Get status information for the signage integration
368     *
369     * Returns counts and display metrics for slides linked to the event.
370     *
371     * @param EventIntegration $integration The integration to check
372     * @return array Status with slide counts and metrics
373     */
374    public function getStatus(EventIntegration $integration): array
375    {
376        try {
377            // Get slide counts by status
378            $sql = "SELECT
379                        COUNT(*) as total,
380                        SUM(CASE WHEN scheduled = 0 THEN 1 ELSE 0 END) as active,
381                        SUM(CASE WHEN scheduled = 1 THEN 1 ELSE 0 END) as pending
382                    FROM dsLoop
383                    WHERE eventId = :eventId";
384
385            $stmt = $this->db->prepare($sql);
386            $stmt->execute([':eventId' => $integration->eventId]);
387            $counts = $stmt->fetch(PDO::FETCH_ASSOC);
388
389            // Determine overall status
390            $total = (int) $counts['total'];
391            $active = (int) $counts['active'];
392
393            if ($total === 0) {
394                $status = EventIntegration::STATUS_COMPLETED;
395            } elseif ($active === $total) {
396                $status = EventIntegration::STATUS_ACTIVE;
397            } elseif ($active > 0) {
398                $status = EventIntegration::STATUS_ACTIVE;
399            } else {
400                $status = EventIntegration::STATUS_PENDING;
401            }
402
403            return $this->buildStatusResponse($status, [
404                'totalSlides' => $total,
405                'activeSlides' => $active,
406                'pendingSlides' => (int) $counts['pending'],
407            ]);
408
409        } catch (PDOException $e) {
410            throw IntegrationException::getStatusFailed(
411                $this->getType(),
412                $integration->foreignId,
413                'Database error: ' . $e->getMessage(),
414                $e
415            );
416        }
417    }
418
419    /**
420     * Validate configuration for signage integration
421     *
422     * @param array $config Configuration to validate
423     * @return array Array of validation errors (empty if valid)
424     */
425    public function validateConfig(array $config): array
426    {
427        $errors = [];
428
429        // Must have either slideIds or tags
430        $hasSlideIds = !empty($config['slideIds']) && is_array($config['slideIds']);
431        $hasTags = !empty($config['tags']) && is_array($config['tags']);
432
433        if (!$hasSlideIds && !$hasTags) {
434            $errors[] = 'Configuration must include either slideIds or tags array';
435        }
436
437        // Validate slideIds if provided
438        if ($hasSlideIds) {
439            foreach ($config['slideIds'] as $id) {
440                if (!is_numeric($id) || (int) $id <= 0) {
441                    $errors[] = 'All slideIds must be positive integers';
442                    break;
443                }
444            }
445        }
446
447        // Validate tags if provided
448        if ($hasTags) {
449            foreach ($config['tags'] as $tag) {
450                if (!is_string($tag) || trim($tag) === '') {
451                    $errors[] = 'All tags must be non-empty strings';
452                    break;
453                }
454            }
455        }
456
457        // Validate source if provided
458        if (isset($config['source'])) {
459            if (!isset(self::SOURCE_MAP[$config['source']])) {
460                $errors[] = "Invalid source: must be 'store', 'corporate', or 'hipbone'";
461            }
462        }
463
464        // Validate duration if provided
465        if (isset($config['duration'])) {
466            if (!is_numeric($config['duration']) || $config['duration'] <= 0) {
467                $errors[] = 'Duration must be a positive number';
468            }
469        }
470
471        // Validate displayOrder if provided
472        if (isset($config['displayOrder'])) {
473            if (!is_numeric($config['displayOrder']) || $config['displayOrder'] < 0) {
474                $errors[] = 'Display order must be a non-negative number';
475            }
476        }
477
478        return $errors;
479    }
480
481    /**
482     * Get default configuration for signage integration
483     *
484     * @return array Default configuration values
485     */
486    public function getDefaultConfig(): array
487    {
488        return [
489            'slideIds' => [],
490            'tags' => [],
491            'source' => 'store',
492            'displayOrder' => null,
493            'duration' => Constants::DEFAULT_DURATION,
494        ];
495    }
496
497    // =========================================================================
498    // PRIVATE HELPER METHODS
499    // =========================================================================
500
501    /**
502     * Resolve slide IDs from configuration
503     *
504     * Handles both direct IDs and tag-based lookup.
505     *
506     * @param array $config Configuration array
507     * @return array Array of slide IDs
508     */
509    private function resolveSlideIds(array $config): array
510    {
511        $slideIds = [];
512
513        // Add direct slide IDs
514        if (!empty($config['slideIds']) && is_array($config['slideIds'])) {
515            foreach ($config['slideIds'] as $id) {
516                $slideIds[] = (int) $id;
517            }
518        }
519
520        // Add slides found by tags
521        if (!empty($config['tags']) && is_array($config['tags'])) {
522            $source = $config['source'] ?? 'store';
523            $tagSlideType = $this->mapSourceToTagType($source);
524
525            $tagService = $this->getTagService();
526            $tagSlideIds = $tagService->getSlidesByTags($config['tags'], $tagSlideType);
527
528            foreach ($tagSlideIds as $id) {
529                if (!in_array($id, $slideIds, true)) {
530                    $slideIds[] = $id;
531                }
532            }
533        }
534
535        return $slideIds;
536    }
537
538    /**
539     * Map source config value to SlideTagService type
540     *
541     * @param string $source Source value from config
542     * @return string Tag service slide type
543     */
544    private function mapSourceToTagType(string $source): string
545    {
546        return match ($source) {
547            'corporate', 'corp' => 'corp',
548            'hipbone', 'hb' => 'hb',
549            default => 'store',
550        };
551    }
552
553    /**
554     * Resolve source constant from config
555     *
556     * @param array $config Configuration array
557     * @return int Source constant value
558     */
559    private function resolveSource(array $config): int
560    {
561        $source = $config['source'] ?? 'store';
562        return self::SOURCE_MAP[$source] ?? Constants::SOURCE_STORE;
563    }
564
565    /**
566     * Get the SlideTagService instance
567     *
568     * @return SlideTagService
569     */
570    private function getTagService(): SlideTagService
571    {
572        if ($this->tagService === null) {
573            $this->tagService = new SlideTagService($this->db);
574        }
575        return $this->tagService;
576    }
577
578    /**
579     * Get the next position value in the loop
580     *
581     * @return int Next position (max + 1)
582     */
583    private function getNextPosition(): int
584    {
585        $sql = "SELECT COALESCE(MAX(position), -1) + 1 as nextPos FROM dsLoop";
586        $stmt = $this->db->query($sql);
587        $result = $stmt->fetch(PDO::FETCH_ASSOC);
588        return (int) $result['nextPos'];
589    }
590
591    /**
592     * Add a slide to the dsLoop table
593     *
594     * @param int $slideId Slide ID
595     * @param int $source Slide source constant
596     * @param int $position Position in loop
597     * @param int $duration Display duration
598     * @param string|null $startDate Start date
599     * @param string|null $expireDate Expire date
600     * @param int $eventId Event ID for traceability
601     * @return int|null Loop ID on success, null on failure
602     */
603    private function addToLoop(
604        int $slideId,
605        int $source,
606        int $position,
607        int $duration,
608        ?string $startDate,
609        ?string $expireDate,
610        int $eventId
611    ): ?int {
612        $sql = "INSERT INTO dsLoop
613                (slideID, slideUploader, position, duration, animation,
614                 startDate, expireDate, scheduled, eventId)
615                VALUES
616                (:slideId, :source, :position, :duration, :animation,
617                 :startDate, :expireDate, :scheduled, :eventId)";
618
619        $scheduled = ($startDate !== null && $startDate > date('Y-m-d'))
620            ? Constants::SCHEDULE_PENDING
621            : Constants::SCHEDULE_ACTIVE;
622
623        $stmt = $this->db->prepare($sql);
624        $result = $stmt->execute([
625            ':slideId' => $slideId,
626            ':source' => $source,
627            ':position' => $position,
628            ':duration' => $duration,
629            ':animation' => Constants::DEFAULT_ANIMATION,
630            ':startDate' => $startDate,
631            ':expireDate' => $expireDate,
632            ':scheduled' => $scheduled,
633            ':eventId' => $eventId,
634        ]);
635
636        if ($result === false) {
637            return null;
638        }
639
640        return (int) $this->db->lastInsertId();
641    }
642
643    /**
644     * Add a loop entry to the global schedule
645     *
646     * @param int $loopId Loop entry ID
647     * @param string|null $startDate Start date
648     * @param string|null $expireDate Expire date
649     * @return bool Success
650     */
651    private function addToSchedule(int $loopId, ?string $startDate, ?string $expireDate): bool
652    {
653        // Only add to schedule if there are dates to manage
654        if ($startDate === null && $expireDate === null) {
655            return true;
656        }
657
658        $sql = "INSERT INTO digitalSignSchedule
659                (loopID, typeNum, startDate, expireDate, finished)
660                VALUES
661                (:loopId, :typeNum, :startDate, :expireDate, 0)";
662
663        $stmt = $this->globalDb->prepare($sql);
664        return $stmt->execute([
665            ':loopId' => $loopId,
666            ':typeNum' => $this->typeNum,
667            ':startDate' => $startDate,
668            ':expireDate' => $expireDate,
669        ]);
670    }
671
672    /**
673     * Update schedule dates for all entries linked to an event
674     *
675     * @param int $eventId Event ID
676     * @param string|null $startDate New start date
677     * @param string|null $expireDate New expire date
678     * @return bool Success
679     */
680    private function updateScheduleDates(int $eventId, ?string $startDate, ?string $expireDate): bool
681    {
682        // Get loop IDs for this event
683        $loopIds = $this->getLoopIdsByEvent($eventId);
684
685        if (empty($loopIds)) {
686            return true;
687        }
688
689        $placeholders = implode(',', array_fill(0, count($loopIds), '?'));
690        $sql = "UPDATE digitalSignSchedule
691                SET startDate = ?, expireDate = ?, finished = 0
692                WHERE loopID IN ({$placeholders}) AND typeNum = ?";
693
694        $params = [$startDate, $expireDate];
695        $params = array_merge($params, $loopIds);
696        $params[] = $this->typeNum;
697
698        $stmt = $this->globalDb->prepare($sql);
699        return $stmt->execute($params);
700    }
701
702    /**
703     * Delete a loop entry from the global schedule
704     *
705     * @param int $loopId Loop entry ID
706     * @return bool Success
707     */
708    private function deleteFromSchedule(int $loopId): bool
709    {
710        $sql = "DELETE FROM digitalSignSchedule
711                WHERE loopID = :loopId AND typeNum = :typeNum";
712
713        $stmt = $this->globalDb->prepare($sql);
714        return $stmt->execute([
715            ':loopId' => $loopId,
716            ':typeNum' => $this->typeNum,
717        ]);
718    }
719
720    /**
721     * Get all loop IDs linked to an event
722     *
723     * @param int $eventId Event ID
724     * @return array Array of loop IDs
725     */
726    private function getLoopIdsByEvent(int $eventId): array
727    {
728        $sql = "SELECT id FROM dsLoop WHERE eventId = :eventId";
729        $stmt = $this->db->prepare($sql);
730        $stmt->execute([':eventId' => $eventId]);
731
732        $ids = [];
733        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
734            $ids[] = (int) $row['id'];
735        }
736
737        return $ids;
738    }
739}