setTable('plate_scanners'); } public function objectChanged(): void { // No need to invalidate the cache, since the plate_scanners object is not cached } public function getPlateScannerById(int $id): plate_scanners_o { global $db; // Get the record from the database $sql = "SELECT * FROM $this->table WHERE id = $id"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = $id; $this->getObjectProperties(); } return $this; } public function getObjectProperties(): void { $this->department_id = new object_property($this->table, $this->id, 'department_id', 'int'); $this->lane_id = new object_property($this->table, $this->id, 'lane_id', 'int'); $this->name = new object_property($this->table, $this->id, 'name', 'string'); $this->notes = new object_property($this->table, $this->id, 'notes', 'string'); $this->api_key = new object_property($this->table, $this->id, 'api_key', 'string'); } public function add(int $department_id, string $name, string $notes, ?int $lane_id = null): void { global $db, $response; try { // Generate an API key $api_key = bin2hex(random_bytes(32)); $lane_id = $this->normalizeLaneId($department_id, $lane_id); // Avoid SQL injection $name = $db->escape_string($name); $notes = $db->escape_string($notes); $laneValue = $lane_id === null ? 'NULL' : (string)$lane_id; // Create a new record in the database $sql = "INSERT INTO $this->table (department_id, lane_id, name, notes, api_key) VALUES ($department_id, $laneValue, '$name', '$notes', '$api_key')"; $db->query($sql); // Get the id of the new record $this->id = $db->insert_id(); // Set the values of the object properties $this->getObjectProperties(); } catch (\Exception $e) { $response->error($e->getMessage()); } } public function edit( int $id, int $department_id, string $name, string $notes, ?int $lane_id = null, bool $laneIdProvided = false ): void { global $response; try { $this->select($id); // Use object_property setters so cached field values are invalidated before we serialize the scanner. $this->department_id->set($department_id); $this->name->set($name); $this->notes->set($notes); if ($laneIdProvided) { $lane_id = $this->normalizeLaneId($department_id, $lane_id); $this->lane_id->set($lane_id); } } catch (\Exception $e) { $response->error($e->getMessage()); } } public function getPlateScannerByApiKey(mixed $token): plate_scanners_o { global $db; // Avoid SQL injection $token = $db->escape_string($token); // Get the record from the database $sql = "SELECT * FROM $this->table WHERE api_key = '$token'"; $result = $db->query($sql); if ($result->num_rows > 0) { $this->id = $result->fetch_assoc()['id']; $this->getObjectProperties(); } return $this; } /** * @return array{id:int,department_id:int,lane_id:int|null,name:string,notes:string,api_key:string} * @throws Exception */ public function asArray(): array { $this->requireSelected(); return [ 'id' => (int)$this->id, 'department_id' => (int)$this->department_id->value(), 'lane_id' => $this->lane_id->value() === null ? null : (int)$this->lane_id->value(), 'name' => (string)$this->name->value(), 'notes' => (string)$this->notes->value(), 'api_key' => (string)$this->api_key->value(), ]; } /** * @return array * @throws Exception */ public function getDepartmentScanners(int $departmentId): array { $scanners = []; $rows = self::getFieldsWhere([ 'department_id' => $departmentId, ], ['id']); foreach ($rows as $row) { $scanner = (new plate_scanners_o())->select((int)$row['id']); if ($scanner->exists()) { $scanners[] = $scanner; } } return $scanners; } /** * @throws Exception */ public function rotateApiKey(int $id): array { $scanner = $this->select($id); if (!$scanner->exists()) { throw new Exception('Number plate scanner not found'); } $newApiKey = bin2hex(random_bytes(32)); $scanner->api_key->set($newApiKey); return $scanner->asArray(); } private function normalizeLaneId(int $departmentId, ?int $laneId): ?int { if ($laneId === null || $laneId <= 0) { return null; } $lane = (new department_lanes_o())->select($laneId); if (!$lane->exists()) { throw new Exception('Department lane not found'); } if ((int)$lane->department->value() !== $departmentId) { throw new Exception('The lane does not belong to the number plate scanner department'); } return (int)$lane->id; } private static function ensureSchema(): void { if (self::$schemaInitialized) { return; } global $db; if (!self::tableHasColumn('plate_scanners', 'lane_id')) { $db->query("ALTER TABLE `plate_scanners` ADD COLUMN `lane_id` INT NULL AFTER `department_id`"); } self::$schemaInitialized = true; } /** Explicit mutation-path compatibility bootstrap for XL Vask hall scoping. */ public static function ensureHallIdSchemaForMutation(): void { global $db; if (!self::tableHasColumn('plate_scanners', 'HallId')) { $db->query("ALTER TABLE `plate_scanners` ADD COLUMN `HallId` VARCHAR(191) NULL AFTER `lane_id`"); } } private static function tableHasColumn(string $table, string $column): bool { global $db; $table = $db->escape_string($table); $column = $db->escape_string($column); $database = $db->escape_string($db->getDatabase()); $result = $db->query( "SELECT COUNT(*) AS c FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$database' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column'" ); if (!$result) { return false; } $row = $result->fetch_assoc(); return ((int)($row['c'] ?? 0)) > 0; } }