JsonInvoiceStateAdapter.php
Pfad: src/Infrastructure/Rechnung/JsonInvoiceStateAdapter.php
Ext: php
Größe: 3639 Bytes
Geändert: 2026-07-16 16:54:17+02
<?php
declare(strict_types=1);
namespace Demo\Infrastructure\Rechnung;
use Demo\Domain\Rechnung\InvoiceState;
use Demo\Domain\Rechnung\InvoiceStatePort;
use JsonException;
use RuntimeException;
/**
* Atomarer JSON-State mit exklusivem Prozess-Lock.
*/
final class JsonInvoiceStateAdapter implements InvoiceStatePort
{
/** @var resource|null Aktuell gehaltenes Lock. */
private mixed $lock = null;
/** Bindet den Adapter an eine State-Datei. */
public function __construct(private readonly string $statePath)
{
}
/** Laedt und validiert den State fail-closed. */
public function load(): InvoiceState
{
if (!file_exists($this->statePath)) {
return InvoiceState::empty();
}
$raw = file_get_contents($this->statePath);
if ($raw === false || trim($raw) === '') {
throw new RuntimeException('Rechnungs-State ist nicht lesbar');
}
try {
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
throw new RuntimeException('Rechnungs-State ist korrupt', 0, $error);
}
if (!is_array($data)) {
throw new RuntimeException('Rechnungs-State ist kein Objekt');
}
return InvoiceState::fromArray($data);
}
/** Speichert ueber eindeutige Temp-Datei und atomaren Rename. */
public function save(InvoiceState $state): void
{
if ($this->lock === null) {
$this->withExclusiveLock(function () use ($state): void { $this->write($state); });
return;
}
$this->write($state);
}
/** Fuehrt einen Ablauf unter einem exklusiven Dateilock aus. */
public function withExclusiveLock(callable $operation): mixed
{
if ($this->lock !== null) {
return $operation();
}
$directory = dirname($this->statePath);
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new RuntimeException('State-Verzeichnis kann nicht erstellt werden');
}
$handle = fopen($this->statePath . '.lock', 'c');
if ($handle === false || !flock($handle, LOCK_EX)) {
throw new RuntimeException('State-Lock kann nicht gesetzt werden');
}
$this->lock = $handle;
try {
return $operation();
} finally {
flock($handle, LOCK_UN);
fclose($handle);
$this->lock = null;
}
}
/** Schreibt den State atomar auf dasselbe Dateisystem. */
private function write(InvoiceState $state): void
{
$json = json_encode($state->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
$temporary = tempnam(dirname($this->statePath), '.invoice-state-');
if ($temporary === false) {
throw new RuntimeException('Temporaerer State kann nicht erstellt werden');
}
$handle = fopen($temporary, 'wb');
if ($handle === false || fwrite($handle, $json) !== strlen($json) || !fflush($handle)) {
if (is_resource($handle)) {
fclose($handle);
}
@unlink($temporary);
throw new RuntimeException('Rechnungs-State kann nicht geschrieben werden');
}
if (function_exists('fsync')) {
fsync($handle);
}
fclose($handle);
if (!rename($temporary, $this->statePath)) {
@unlink($temporary);
throw new RuntimeException('Rechnungs-State kann nicht atomar ersetzt werden');
}
}
}
Frühere Versionen
| Version | Zeitpunkt | Operation |
|---|---|---|
| 26 | 2026-07-16 19:01:40.81492+02 | UPDATE |