filename)) { return; } $this->data = file($this->filename, FILE_IGNORE_NEW_LINES); } /** * Auto-save dirty data before releasing the lock * * When indexes are used in tandem, a new RID written to one index may already * be referenced by other indexes that were saved. Losing unsaved data here * would leave dangling references, causing silent index corruption. * * The try/catch is necessary because unlock() is called from __destruct() * (in the parent class), and PHP destructors must not throw — a throw * during exception unwinding causes a fatal error. * * @inheritdoc */ public function unlock(): void { if ($this->isDirty()) { try { $this->save(); } catch (\Exception $e) { Logger::error('MemoryIndex failed to save on unlock: ' . $e->getMessage()); } } parent::unlock(); } /** * @inheritdoc * @throws IndexLockException */ public function changeRow(int $rid, string $value): void { if (!$this->isWritable) throw new IndexLockException(); if ($rid > count($this->data)) { $this->data = array_pad($this->data, $rid, ''); } $this->data[$rid] = $value; $this->dirty = true; } /** @inheritdoc */ public function retrieveRow(int $rid): string { return $this->data[$rid] ?? ''; } /** @inheritdoc */ public function retrieveRows(array $rids): array { $result = []; foreach ($rids as $rid) { if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid]; } return $result; } /** @inheritdoc */ public function getRowIDs(array $values): array { $values = array_map(trim(...), $values); $values = array_fill_keys($values, 1); // easier access as associative array $result = []; $count = count($this->data); for ($ln = 0; $ln < $count; $ln++) { $line = $this->data[$ln]; if (isset($values[$line])) { $result[$line] = $ln; unset($values[$line]); } } if (!$this->isWritable) return $result; // if there are still values, they have not been found and will be appended foreach (array_keys($values) as $value) { $this->data[] = $value; $result[$value] = $ln++; $this->dirty = true; } return $result; } /** @inheritdoc */ public function search(string $re): array { return preg_grep($re, $this->data); } /** * Save the changed index back to its file * * The method will check the internal dirty state and will only write when the index has actually been changed * * @throws IndexWriteException * @throws IndexLockException */ public function save(): void { global $conf; if (!$this->isDirty()) { return; } if (!$this->isWritable) throw new IndexLockException(); $tempname = $this->filename . '.tmp'; $fh = @fopen($tempname, 'w'); if (!$fh) { throw new IndexWriteException("Failed to write $tempname"); } fwrite($fh, implode("\n", $this->data)); if ($this->data !== []) { fwrite($fh, "\n"); } fclose($fh); if ($conf['fperm']) { chmod($tempname, $conf['fperm']); } if (!io_rename($tempname, $this->filename)) { throw new IndexWriteException("Failed to write $this->filename"); } $this->dirty = false; } /** * Check if the index has been modified and needs to be saved * @return bool */ public function isDirty(): bool { return $this->dirty; } /** @inheritdoc */ public function count(): int { return count($this->data); } /** @inheritdoc */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->data); } }