Self-review (Task 16) follow-up fixes: - PdoEventRepository::recent(): add event_id DESC as a secondary sort key after occurred_at DESC so ties (equal device-reported timestamps) resolve deterministically, matching InMemoryEventRepository's insertion-order tie-break. - PdoZoneRepository::updateOwned(): after the ownership-scoped UPDATE, disambiguate a 0-rowCount() result (MySQL reports "changed" rows, not "matched" rows) with a targeted existence check, so a genuine no-op resubmit (identical level/polygon) still succeeds while a zone deleted concurrently between the ownership check and the UPDATE now throws instead of save() silently reporting success for a zone that no longer exists. Full suite still green: 59 tests, 114 assertions, 4 skipped, 0 failures, 0 warnings/deprecations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne4rU4FToTLHcBUMUvUnT1
105 lines
3.9 KiB
PHP
105 lines
3.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace App\Repository\Pdo;
|
|
|
|
use App\Repository\EventRepository;
|
|
use PDO;
|
|
|
|
final class PdoEventRepository implements EventRepository
|
|
{
|
|
private const SELECT_COLUMNS =
|
|
'event_id, device_id, camera_id, event_type, severity, track_id, zone_id,
|
|
occurred_at, duration_s, metrics_json, acknowledged_by, acknowledged_at';
|
|
|
|
public function __construct(private PDO $pdo) {}
|
|
|
|
public function insert(array $event): int
|
|
{
|
|
$st = $this->pdo->prepare(
|
|
'INSERT INTO events
|
|
(device_id, camera_id, event_type, severity, track_id, zone_id,
|
|
occurred_at, duration_s, metrics_json, received_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
);
|
|
$st->execute([
|
|
(string)$event['device_id'],
|
|
(string)$event['camera_id'],
|
|
(string)$event['event_type'],
|
|
(string)$event['severity'],
|
|
$event['track_id'] ?? null,
|
|
$event['zone_id'] ?? null,
|
|
(float)$event['occurred_at'],
|
|
(float)($event['duration_s'] ?? 0.0),
|
|
json_encode($event['metrics'] ?? [], JSON_THROW_ON_ERROR),
|
|
time(),
|
|
]);
|
|
return (int)$this->pdo->lastInsertId();
|
|
}
|
|
|
|
public function recent(int $limit, ?string $severity): array
|
|
{
|
|
if ($severity !== null) {
|
|
$st = $this->pdo->prepare(
|
|
'SELECT ' . self::SELECT_COLUMNS . '
|
|
FROM events WHERE severity = ?
|
|
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
|
);
|
|
$st->bindValue(1, $severity, PDO::PARAM_STR);
|
|
$st->bindValue(2, $limit, PDO::PARAM_INT);
|
|
} else {
|
|
$st = $this->pdo->prepare(
|
|
'SELECT ' . self::SELECT_COLUMNS . '
|
|
FROM events
|
|
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
|
);
|
|
$st->bindValue(1, $limit, PDO::PARAM_INT);
|
|
}
|
|
$st->execute();
|
|
return array_map(self::mapRow(...), $st->fetchAll());
|
|
}
|
|
|
|
public function find(int $id): ?array
|
|
{
|
|
$st = $this->pdo->prepare('SELECT ' . self::SELECT_COLUMNS . ' FROM events WHERE event_id = ?');
|
|
$st->execute([$id]);
|
|
$row = $st->fetch();
|
|
return $row === false ? null : self::mapRow($row);
|
|
}
|
|
|
|
public function acknowledge(int $id, string $user): bool
|
|
{
|
|
// Existence is checked separately (rather than trusting UPDATE's
|
|
// rowCount()) because MySQL's PDO driver, by default, reports
|
|
// "changed rows" rather than "matched rows": re-acknowledging an
|
|
// already-acknowledged event by the same user would otherwise report
|
|
// 0 affected rows and this method would wrongly return false.
|
|
$exists = $this->pdo->prepare('SELECT 1 FROM events WHERE event_id = ? LIMIT 1');
|
|
$exists->execute([$id]);
|
|
if ($exists->fetchColumn() === false) {
|
|
return false;
|
|
}
|
|
$st = $this->pdo->prepare(
|
|
'UPDATE events SET acknowledged_by = ?, acknowledged_at = ? WHERE event_id = ?'
|
|
);
|
|
$st->execute([$user, time(), $id]);
|
|
return true;
|
|
}
|
|
|
|
private static function mapRow(array $row): array
|
|
{
|
|
return [
|
|
'event_id' => (int)$row['event_id'],
|
|
'device_id' => $row['device_id'],
|
|
'camera_id' => $row['camera_id'],
|
|
'event_type' => $row['event_type'],
|
|
'severity' => $row['severity'],
|
|
'track_id' => $row['track_id'] !== null ? (int)$row['track_id'] : null,
|
|
'zone_id' => $row['zone_id'],
|
|
'occurred_at' => (float)$row['occurred_at'],
|
|
'duration_s' => (float)$row['duration_s'],
|
|
'metrics' => json_decode((string)($row['metrics_json'] ?? '[]'), true) ?? [],
|
|
'acknowledged_by' => $row['acknowledged_by'],
|
|
'acknowledged_at' => $row['acknowledged_at'] !== null ? (int)$row['acknowledged_at'] : null,
|
|
];
|
|
}
|
|
}
|