Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Interface für Append-Storage-Operationen
*
* Ermöglicht das Anhängen von Daten an bestehende Dateien,
* ideal für Logs, Analytics und Stream-artige Daten.
*/
interface AppendableStorage
{
/**
* Hängt Inhalt an eine Datei an
*/
public function append(string $path, string $content): void;
/**
* Hängt eine neue Zeile an eine Datei an
*/
public function appendLine(string $path, string $line): void;
/**
* Hängt JSON-Daten als neue Zeile an (JSONL-Format)
*/
public function appendJson(string $path, array $data): void;
/**
* Hängt CSV-Zeile an eine CSV-Datei an
*/
public function appendCsv(string $path, array $row): void;
/**
* Hängt Inhalt mit Timestamp an
*/
public function appendWithTimestamp(string $path, string $content): void;
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Interface für atomare Storage-Operationen
*
* Garantiert, dass Schreiboperationen entweder vollständig erfolgen oder gar nicht.
* Verhindert korrupte/teilweise Dateien bei Concurrent Access oder System-Ausfällen.
*/
interface AtomicStorage
{
/**
* Atomare Schreiboperation
*
* Schreibt Inhalt atomisch (write + rename) um Race Conditions zu vermeiden.
*/
public function putAtomic(string $path, string $content): void;
/**
* Atomare Update-Operation mit Callback
*
* Lädt aktuelle Daten, führt Update-Callback aus und schreibt atomisch zurück.
*/
public function updateAtomic(string $path, callable $updateCallback): void;
/**
* Atomare JSON-Update-Operation
*
* Spezielle Implementierung für JSON-Updates um Parsing-Errors zu vermeiden.
*/
public function updateJsonAtomic(string $path, callable $updateCallback): void;
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Interface für Compression-Storage-Operationen
*
* Ermöglicht automatische Komprimierung für große Dateien.
* Ideal für Analytics-Daten, Logs und Archive.
*/
interface CompressibleStorage
{
/**
* Schreibt Daten komprimiert
*/
public function putCompressed(string $path, string $content, string $algorithm = 'gzip'): void;
/**
* Liest komprimierte Daten
*/
public function getCompressed(string $path): string;
/**
* Komprimiert bestehende Datei
*/
public function compress(string $path, string $algorithm = 'gzip'): void;
/**
* Dekomprimiert Datei
*/
public function decompress(string $path): void;
/**
* Prüft ob Datei komprimiert ist
*/
public function isCompressed(string $path): bool;
/**
* Gibt verfügbare Komprimierungs-Algorithmen zurück
*
* @return array<string>
*/
public function getAvailableAlgorithms(): array;
}

View File

@@ -1,29 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Byte;
/**
* Repräsentiert ein Verzeichnis im Dateisystem mit Lazy-Loading-Unterstützung.
*
* @property-read string $path Pfad zum Verzeichnis
* @property-read FilePath|string $path Pfad zum Verzeichnis
* @property-read Storage $storage Storage-Implementierung
* @property-read array $contents Verzeichnisinhalt (lazy geladen)
*/
final readonly class Directory
{
public function __construct(
public string $path,
public FilePath|string $path,
public Storage $storage,
public array $contents = []
) {}
) {
}
/**
* Get path as FilePath object
*/
public function getPath(): FilePath
{
return $this->path instanceof FilePath ? $this->path : FilePath::create($this->path);
}
/**
* Get path as string
*/
public function getPathString(): string
{
return $this->path instanceof FilePath ? $this->path->toString() : $this->path;
}
/**
* Prüft, ob das Verzeichnis existiert
*/
public function exists(): bool
{
return is_dir($this->path);
return is_dir($this->getPathString());
}
/**
@@ -31,7 +51,7 @@ final readonly class Directory
*/
public function create(int $permissions = 0755, bool $recursive = true): void
{
$this->storage->createDirectory($this->path, $permissions, $recursive);
$this->storage->createDirectory($this->getPathString(), $permissions, $recursive);
}
/**
@@ -41,7 +61,7 @@ final readonly class Directory
*/
public function getFiles(): array
{
$paths = $this->storage->listDirectory($this->path);
$paths = $this->storage->listDirectory($this->getPathString());
$files = [];
foreach ($paths as $path) {
@@ -60,7 +80,7 @@ final readonly class Directory
*/
public function getDirectories(): array
{
$paths = $this->storage->listDirectory($this->path);
$paths = $this->storage->listDirectory($this->getPathString());
$directories = [];
foreach ($paths as $path) {
@@ -79,7 +99,7 @@ final readonly class Directory
*/
public function getContents(): array
{
$paths = $this->storage->listDirectory($this->path);
$paths = $this->storage->listDirectory($this->getPathString());
$files = [];
$directories = [];
@@ -99,8 +119,9 @@ final readonly class Directory
*/
public function getFile(string $filename): File
{
$path = $this->path . DIRECTORY_SEPARATOR . $filename;
return FilesystemFactory::createFile($path, $this->storage);
$filePath = $this->getPath()->join($filename);
return FilesystemFactory::createFile($filePath, $this->storage);
}
/**
@@ -108,8 +129,9 @@ final readonly class Directory
*/
public function getDirectory(string $name): Directory
{
$path = $this->path . DIRECTORY_SEPARATOR . $name;
return FilesystemFactory::createDirectory($path, $this->storage);
$dirPath = $this->getPath()->join($name);
return FilesystemFactory::createDirectory($dirPath, $this->storage);
}
/**
@@ -119,4 +141,185 @@ final readonly class Directory
{
return FilesystemFactory::createDirectory($this->path, $this->storage);
}
/**
* Get directory name
*/
public function getName(): string
{
return $this->getPath()->getFilename();
}
/**
* Get parent directory
*/
public function getParent(): Directory
{
$parentPath = $this->getPath()->getDirectory();
return FilesystemFactory::createDirectory($parentPath, $this->storage);
}
/**
* Get total size of all files in directory
*/
public function getTotalSize(): Byte
{
$totalBytes = 0;
$files = $this->getFiles();
foreach ($files as $file) {
$totalBytes += $file->getSize()->toBytes();
}
return Byte::fromBytes($totalBytes);
}
/**
* Check if directory is empty
*/
public function isEmpty(): bool
{
$contents = $this->getContents();
return empty($contents['files']) && empty($contents['directories']);
}
/**
* Count files in directory
*/
public function countFiles(): int
{
return count($this->getFiles());
}
/**
* Count subdirectories
*/
public function countDirectories(): int
{
return count($this->getDirectories());
}
/**
* Lädt Verzeichnisbaum rekursiv mit optimierter Performance
* Nutzt intern Fiber-basierte Parallelisierung
*/
public function getTree(int $maxDepth = 3): array
{
return $this->buildTree($this, 0, $maxDepth);
}
/**
* Berechnet die Gesamtgröße mit verbesserter Performance
* Nutzt Batch-Operationen für paralleles Laden
*/
public function getTotalSizeOptimized(): Byte
{
$files = $this->getFiles();
if (empty($files)) {
return Byte::fromBytes(0);
}
// Nutze Batch-Operation für paralleles Laden aller Dateigrößen
$operations = [];
foreach ($files as $index => $file) {
$operations["size_$index"] = fn () => $file->getSize()->toBytes();
}
$sizes = $this->storage->batch($operations);
$totalBytes = 0;
foreach ($sizes as $size) {
if (! ($size instanceof \Throwable)) {
$totalBytes += $size;
}
}
return Byte::fromBytes($totalBytes);
}
/**
* Lädt alle Inhalte mit verbesserter Performance
* Nutzt intern Batch-Operationen für parallele Verarbeitung
*/
public function getContentsOptimized(): array
{
$paths = $this->storage->listDirectory($this->getPathString());
if (empty($paths)) {
return ['files' => [], 'directories' => []];
}
// Nutze Batch-Operation für parallele Typ-Prüfung
$operations = [];
foreach ($paths as $path) {
$operations["check_$path"] = fn () => [
'path' => $path,
'is_file' => is_file($path),
'is_dir' => is_dir($path),
];
}
$results = $this->storage->batch($operations);
$files = [];
$directories = [];
foreach ($results as $result) {
if ($result instanceof \Throwable) {
continue;
}
if ($result['is_file']) {
$files[] = FilesystemFactory::createFile($result['path'], $this->storage);
} elseif ($result['is_dir']) {
$directories[] = FilesystemFactory::createDirectory($result['path'], $this->storage);
}
}
return ['files' => $files, 'directories' => $directories];
}
/**
* Hilfsmethode für rekursives Laden mit Fiber-Optimierung
*/
private function buildTree(Directory $directory, int $currentDepth, int $maxDepth): array
{
if ($currentDepth >= $maxDepth) {
return ['files' => [], 'directories' => []];
}
$contents = $directory->getContentsOptimized();
$tree = [
'files' => $contents['files'],
'directories' => [],
];
if (! empty($contents['directories'])) {
// Nutze Batch-Operation für paralleles rekursives Laden
$operations = [];
foreach ($contents['directories'] as $index => $subDir) {
$operations["tree_$index"] = fn () => [
'directory' => $subDir,
'subtree' => $this->buildTree($subDir, $currentDepth + 1, $maxDepth),
];
}
$results = $this->storage->batch($operations);
foreach ($results as $result) {
if (! ($result instanceof \Throwable)) {
$tree['directories'][] = [
'directory' => $result['directory'],
'contents' => $result['subtree'],
];
}
}
}
return $tree;
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class DirectoryCreateException extends FrameworkException
@@ -15,9 +16,10 @@ final class DirectoryCreateException extends FrameworkException
) {
parent::__construct(
message: "Ordner '$directory' konnte nicht angelegt werden.",
context: ExceptionContext::forOperation('directory_creation', 'Filesystem')
->withData(['directory' => $directory]),
code: $code,
previous: $previous,
context: ['directory' => $directory]
previous: $previous
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class DirectoryListException extends FrameworkException
@@ -15,9 +16,10 @@ final class DirectoryListException extends FrameworkException
) {
parent::__construct(
message: "Fehler beim Auslesen des Verzeichnisses '$directory'.",
context: ExceptionContext::forOperation('directory_listing', 'Filesystem')
->withData(['directory' => $directory]),
code: $code,
previous: $previous,
context: ['directory' => $directory]
previous: $previous
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FileCopyException extends FrameworkException
@@ -16,12 +17,13 @@ final class FileCopyException extends FrameworkException
) {
parent::__construct(
message: "Fehler beim Kopieren von '$source' nach '$destination'.",
context: ExceptionContext::forOperation('file_copy', 'Filesystem')
->withData([
'source' => $source,
'destination' => $destination,
]),
code: $code,
previous: $previous,
context: [
'source' => $source,
'destination' => $destination
]
previous: $previous
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FileDeleteException extends FrameworkException
@@ -15,9 +16,10 @@ final class FileDeleteException extends FrameworkException
) {
parent::__construct(
message: "Fehler beim Löschen der Datei '$path'.",
context: ExceptionContext::forOperation('file_delete', 'Filesystem')
->withData(['path' => $path]),
code: $code,
previous: $previous,
context: ['path' => $path]
previous: $previous
);
}
}

View File

@@ -1,14 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FileMetadataException extends FrameworkException
{
public function __construct(string $path, ?\Throwable $previous = null)
{
parent::__construct("Konnte Metadaten für Datei '{$path}' nicht lesen", 0, $previous);
parent::__construct(
message: "Konnte Metadaten für Datei '{$path}' nicht lesen",
context: ExceptionContext::forOperation('file_metadata', 'Filesystem')
->withData(['path' => $path]),
code: 0,
previous: $previous
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FileNotFoundException extends FrameworkException
@@ -15,9 +16,10 @@ final class FileNotFoundException extends FrameworkException
) {
parent::__construct(
message: "Datei '$path' nicht gefunden.",
context: ExceptionContext::forOperation('file_read', 'Filesystem')
->withData(['path' => $path]),
code: $code,
previous: $previous,
context: ['path' => $path]
previous: $previous
);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FilePermissionException extends FrameworkException
{
public function __construct(
string $path,
string $operation = 'access',
?string $reason = null
) {
$message = "Permission denied for {$operation} on file: {$path}";
if ($reason) {
$message .= " ({$reason})";
}
$context = ExceptionContext::forOperation('file.permission', 'filesystem')
->withData([
'path' => $path,
'operation' => $operation,
'reason' => $reason,
]);
parent::__construct($message, $context);
}
public static function read(string $path, ?string $reason = null): self
{
return new self($path, 'read', $reason);
}
public static function write(string $path, ?string $reason = null): self
{
return new self($path, 'write', $reason);
}
public static function delete(string $path, ?string $reason = null): self
{
return new self($path, 'delete', $reason);
}
public static function createDirectory(string $path, ?string $reason = null): self
{
return new self($path, 'create directory', $reason);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FileReadException extends FrameworkException
@@ -15,9 +16,10 @@ final class FileReadException extends FrameworkException
) {
parent::__construct(
message: "Lesefehler bei '$path'.",
context: ExceptionContext::forOperation('file_read', 'Filesystem')
->withData(['path' => $path]),
code: $code,
previous: $previous,
context: ['path' => $path]
previous: $previous
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class FileWriteException extends FrameworkException
@@ -15,9 +16,10 @@ final class FileWriteException extends FrameworkException
) {
parent::__construct(
message: "Fehler beim Schreiben in '$path'.",
context: ExceptionContext::forOperation('file_write', 'Filesystem')
->withData(['path' => $path]),
code: $code,
previous: $previous,
context: ['path' => $path]
previous: $previous
);
}
}

View File

@@ -1,71 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Repräsentiert eine Datei im Dateisystem mit Lazy-Loading-Unterstützung.
*
* @property-read string $path Pfad zur Datei
* @property-read Storage $storage Storage-Implementierung
* @property-read string $contents Dateiinhalt (lazy geladen)
* @property-read int $size Dateigröße in Bytes (lazy geladen)
* @property-read int $lastModified Zeitstempel der letzten Änderung (lazy geladen)
* Clean File value object representing a file path with metadata.
* File operations are handled by FileSystemService for better separation of concerns.
*/
final readonly class File
{
public function __construct(
public string $path,
public Storage $storage,
public string $contents = '',
public int $size = 0,
public int $lastModified = 0
) {}
/**
* Prüft, ob die Datei existiert
*/
public function exists(): bool
{
return $this->storage->exists($this->path);
public FilePath $path,
public FileMetadata $metadata
) {
}
/**
* Löscht die Datei
* Create File from SplFileInfo
*/
public function delete(): void
public static function fromSplFileInfo(\SplFileInfo $fileInfo): self
{
$this->storage->delete($this->path);
$path = FilePath::create($fileInfo->getPathname());
$metadata = new FileMetadata(
size: $fileInfo->getSize() ?: 0,
lastModified: $fileInfo->getMTime() ?: 0,
mimeType: 'application/octet-stream', // Default, can be determined by FileSystemService
isReadable: $fileInfo->isReadable(),
isWritable: $fileInfo->isWritable()
);
return new self($path, $metadata);
}
/**
* Kopiert die Datei an einen neuen Ort
* Get path as FilePath object
*/
public function copyTo(string $destination): File
public function getPath(): FilePath
{
$this->storage->copy($this->path, $destination);
return FilesystemFactory::createFile($destination, $this->storage);
return $this->path;
}
/**
* Lädt die Dateieigenschaften neu
* Get path as string
*/
public function refresh(): File
public function getPathString(): string
{
return FilesystemFactory::createFile($this->path, $this->storage);
return $this->path->toString();
}
/**
* Holt erweiterte Metadaten der Datei
* Get file metadata
*/
public function getMetadata(): FileMetadata
{
return new FileMetadata(
size: $this->size,
lastModified: $this->lastModified,
mimeType: $this->storage->getMimeType($this->path),
isReadable: $this->storage->isReadable($this->path),
isWritable: $this->storage->isWritable($this->path)
);
return $this->metadata;
}
/**
* Get modification time as Timestamp
*/
public function getModificationTime(): Timestamp
{
return Timestamp::fromFloat($this->metadata->lastModified);
}
/**
* Get file size as Byte object
*/
public function getSize(): Byte
{
return Byte::fromBytes($this->metadata->size);
}
/**
* Get filename from path
*/
public function getFilename(): string
{
return $this->path->getFilename();
}
/**
* Get file extension
*/
public function getExtension(): string
{
return $this->path->getExtension();
}
/**
* Get directory path
*/
public function getDirectory(): FilePath
{
return $this->path->getDirectory();
}
/**
* Check if file has specific extension
*/
public function hasExtension(string $extension): bool
{
return $this->path->hasExtension($extension);
}
}

View File

@@ -1,18 +1,258 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Hash;
use App\Framework\Core\ValueObjects\HashAlgorithm;
use App\Framework\Core\ValueObjects\Timestamp;
use InvalidArgumentException;
/**
* Kapselt Metadaten einer Datei.
* Erweitert mit Funktionalität für Discovery-Modul und nutzt Value Objects.
*/
final readonly class FileMetadata
{
/**
* @param FilePath|string $path Vollständiger Pfad zur Datei
* @param Byte $size Dateigröße
* @param Timestamp $lastModified Zeitstempel der letzten Änderung
* @param Hash $checksum Prüfsumme des Dateiinhalts (optional)
* @param Timestamp $scanTime Zeitstempel des Scans
* @param string $mimeType MIME-Typ der Datei
* @param bool $isReadable Ob die Datei lesbar ist
* @param bool $isWritable Ob die Datei schreibbar ist
* @param array<string, mixed> $additionalData Zusätzliche Metadaten
*/
public function __construct(
public int $size = 0,
public int $lastModified = 0,
public FilePath|string $path = '',
public Byte|int $size = 0,
public Timestamp|int $lastModified = 0,
public ?Hash $checksum = null,
public ?Timestamp $scanTime = null,
public string $mimeType = '',
public bool $isReadable = false,
public bool $isWritable = false
) {}
public bool $isWritable = false,
public array $additionalData = []
) {
}
/**
* Erstellt Metadaten aus einer Datei unter Verwendung des Storage-Interfaces
*
* @param FilePath|string $filePath Pfad zur Datei
* @param Storage $storage Storage-Implementierung
* @param bool $calculateChecksum Ob eine Prüfsumme berechnet werden soll
* @return self
*/
public static function fromFile(FilePath|string $filePath, Storage $storage, bool $calculateChecksum = true): self
{
$path = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
$pathString = $path->toString();
if (! $storage->exists($pathString)) {
throw new InvalidArgumentException("File does not exist: {$pathString}");
}
$size = Byte::fromBytes($storage->size($pathString));
$lastModified = Timestamp::fromFloat((float)$storage->lastModified($pathString));
$scanTime = Timestamp::now();
$mimeType = $storage->getMimeType($pathString);
$isReadable = $storage->isReadable($pathString);
$isWritable = $storage->isWritable($pathString);
// Berechne Prüfsumme nur wenn gewünscht (kann bei großen Dateien teuer sein)
$checksum = $calculateChecksum
? self::calculateChecksum($pathString, $storage)
: self::emptyHash(HashAlgorithm::XXHASH64);
// Zusätzliche Metadaten sammeln
$additionalData = [
'extension' => pathinfo($pathString, PATHINFO_EXTENSION),
];
return new self(
path: $path,
size: $size,
lastModified: $lastModified,
checksum: $checksum,
scanTime: $scanTime,
mimeType: $mimeType,
isReadable: $isReadable,
isWritable: $isWritable,
additionalData: $additionalData
);
}
/**
* Erstellt eine leere Hash-Instanz mit dem angegebenen Algorithmus
*/
public static function emptyHash(HashAlgorithm $algorithm): Hash
{
// Erzeuge einen leeren String mit der richtigen Länge für den Algorithmus
$emptyValue = str_repeat('0', $algorithm->getLength());
return new Hash($emptyValue, $algorithm);
}
/**
* Berechnet die Prüfsumme einer Datei
*/
private static function calculateChecksum(string $filePath, Storage $storage): Hash
{
$content = $storage->get($filePath);
return Hash::create($content, HashAlgorithm::XXHASH64);
}
/**
* Erstellt Metadaten aus einem Array (für Deserialisierung)
*/
public static function fromArray(array $data): self
{
return new self(
path: $data['path'],
size: Byte::fromBytes($data['size']),
lastModified: Timestamp::fromFloat((float)$data['last_modified']),
checksum: isset($data['checksum']) ? new Hash($data['checksum'], HashAlgorithm::XXHASH64) : null,
scanTime: isset($data['scan_time']) ? Timestamp::fromFloat((float)$data['scan_time']) : null,
mimeType: $data['mime_type'] ?? '',
isReadable: $data['is_readable'] ?? false,
isWritable: $data['is_writable'] ?? false,
additionalData: $data['additional_data'] ?? []
);
}
/**
* Konvertiert Metadaten in ein Array (für Serialisierung)
*/
public function toArray(): array
{
return [
'path' => $this->path instanceof FilePath ? $this->path->toString() : $this->path,
'size' => $this->size instanceof Byte ? $this->size->toBytes() : $this->size,
'last_modified' => $this->lastModified instanceof Timestamp ? $this->lastModified->toTimestamp() : $this->lastModified,
'checksum' => $this->checksum?->toString(),
'scan_time' => $this->scanTime?->toTimestamp(),
'mime_type' => $this->mimeType,
'is_readable' => $this->isReadable,
'is_writable' => $this->isWritable,
'additional_data' => $this->additionalData,
];
}
/**
* Prüft, ob sich die Datei seit dem letzten Scan geändert hat
* Verwendet mehrere Kriterien für zuverlässigere Erkennung
*
* @param FilePath|string $filePath Pfad zur Datei
* @param Storage $storage Storage-Implementierung
* @return bool True wenn die Datei sich geändert hat
*/
public function hasChanged(FilePath|string $filePath, Storage $storage): bool
{
$pathString = $filePath instanceof FilePath ? $filePath->toString() : $filePath;
if (! $storage->exists($pathString)) {
return true; // Datei wurde gelöscht
}
// Prüfe Größe und Änderungszeit (schnelle Prüfung)
$currentSize = Byte::fromBytes($storage->size($pathString));
$currentLastModified = Timestamp::fromFloat((float)$storage->lastModified($pathString));
if (! $currentSize->equals($this->size) || ! $currentLastModified->equals($this->lastModified)) {
return true;
}
// Wenn Prüfsumme vorhanden ist, verwende sie für präzisere Erkennung
if ($this->checksum !== null && ! $this->isEmptyChecksum()) {
$currentChecksum = self::calculateChecksum($pathString, $storage);
return ! $currentChecksum->equals($this->checksum);
}
// Fallback: Nur Größe und Änderungszeit
return false;
}
/**
* Prüft, ob die Prüfsumme leer ist (nur Nullen)
*/
private function isEmptyChecksum(): bool
{
if ($this->checksum === null) {
return true;
}
$emptyHash = self::emptyHash($this->checksum->getAlgorithm());
return $this->checksum->equals($emptyHash);
}
/**
* Aktualisiert die Metadaten für eine Datei
*
* @param FilePath|string $filePath Pfad zur Datei
* @param Storage $storage Storage-Implementierung
* @param bool $calculateChecksum Ob eine Prüfsumme berechnet werden soll
* @return self Neue Instanz mit aktualisierten Metadaten
*/
public function update(FilePath|string $filePath, Storage $storage, bool $calculateChecksum = true): self
{
return self::fromFile($filePath, $storage, $calculateChecksum);
}
/**
* Gibt den relativen Pfad zurück (für bessere Lesbarkeit)
*
* @param FilePath|string $basePath Basis-Pfad
* @return string Relativer Pfad
*/
public function getRelativePath(FilePath|string $basePath): string
{
$pathString = $this->path instanceof FilePath ? $this->path->toString() : $this->path;
$basePathString = $basePath instanceof FilePath ? $basePath->toString() : $basePath;
return str_replace($basePathString, '', $pathString);
}
/**
* Gibt eine lesbare Darstellung der Dateigröße zurück
*/
public function getFormattedSize(): string
{
return $this->size instanceof Byte ? $this->size->toHumanReadable(2) : Byte::fromBytes($this->size)->toHumanReadable(2);
}
/**
* Gibt einen lesbaren Zeitstempel zurück
*/
public function getFormattedTime(): string
{
return $this->lastModified instanceof Timestamp
? $this->lastModified->format('Y-m-d H:i:s')
: date('Y-m-d H:i:s', $this->lastModified);
}
/**
* Erstellt eine Instanz mit dem aktuellen Zeitstempel als scanTime
*/
public function withCurrentScanTime(): self
{
return new self(
path: $this->path,
size: $this->size,
lastModified: $this->lastModified,
checksum: $this->checksum,
scanTime: Timestamp::now(),
mimeType: $this->mimeType,
isReadable: $this->isReadable,
isWritable: $this->isWritable,
additionalData: $this->additionalData
);
}
}

View File

@@ -0,0 +1,451 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Byte;
use InvalidArgumentException;
/**
* Immutable file path value object with cross-platform support
*/
final readonly class FilePath
{
private const int MAX_PATH_LENGTH = 4096;
private const array WINDOWS_RESERVED_NAMES = [
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
];
private string $normalized;
private string $directory;
private string $filename;
private string $extension;
public function __construct(string $path)
{
if (empty($path)) {
throw new InvalidArgumentException('Path cannot be empty');
}
if (strlen($path) > self::MAX_PATH_LENGTH) {
throw new InvalidArgumentException('Path exceeds maximum length of ' . self::MAX_PATH_LENGTH);
}
$this->normalized = $this->normalizePath($path);
$this->validateSecurity($this->normalized);
// Eager loading - compute all properties once
$dirname = dirname($this->normalized);
$this->directory = $dirname === '.' ? '' : $dirname;
$this->filename = basename($this->normalized);
$dotPos = strrpos($this->filename, '.');
$this->extension = $dotPos !== false ? substr($this->filename, $dotPos + 1) : '';
}
/**
* Create FilePath from string
*/
public static function create(string $path): self
{
return new self($path);
}
/**
* Create FilePath from current working directory
*/
public static function cwd(): self
{
return new self(getcwd() ?: '/');
}
/**
* Create temporary file path
*/
public static function temp(?string $filename = null): self
{
$tempDir = sys_get_temp_dir();
$filename ??= 'tmp_' . uniqid();
return new self($tempDir . DIRECTORY_SEPARATOR . $filename);
}
/**
* Get the normalized path as string
*/
public function toString(): string
{
return $this->normalized;
}
/**
* Magic method for string conversion
*/
public function __toString(): string
{
return $this->normalized;
}
/**
* Get the directory part of the path
*/
public function getDirectory(): self
{
return $this->directory ? new self($this->directory) : self::create('/');
}
/**
* Get the filename (including extension)
*/
public function getFilename(): string
{
return $this->filename;
}
/**
* Get the filename without extension
*/
public function getBasename(): string
{
$filename = $this->getFilename();
$extension = $this->getExtension();
if ($extension) {
return substr($filename, 0, -strlen($extension) - 1);
}
return $filename;
}
/**
* Get the file extension (without dot)
*/
public function getExtension(): string
{
return $this->extension;
}
/**
* Check if path has extension
*/
public function hasExtension(?string $extension = null): bool
{
$currentExtension = $this->getExtension();
if ($extension === null) {
return ! empty($currentExtension);
}
return strcasecmp($currentExtension, ltrim($extension, '.')) === 0;
}
/**
* Resolve relative path against this path
*/
public function resolve(string $relativePath): self
{
if ($this->isAbsolutePath($relativePath)) {
return new self($relativePath);
}
$basePath = $this->isFile() ? $this->getDirectory()->toString() : $this->normalized;
return new self($basePath . DIRECTORY_SEPARATOR . $relativePath);
}
/**
* Make path relative to base path
*/
public function relativeTo(self $basePath): self
{
$base = rtrim($basePath->toString(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$current = $this->normalized;
if (str_starts_with($current, $base)) {
return new self(substr($current, strlen($base)));
}
// Calculate relative path with ../
$baseParts = explode(DIRECTORY_SEPARATOR, trim($basePath->toString(), DIRECTORY_SEPARATOR));
$currentParts = explode(DIRECTORY_SEPARATOR, trim($current, DIRECTORY_SEPARATOR));
// Find common path
$commonLength = 0;
$minLength = min(count($baseParts), count($currentParts));
for ($i = 0; $i < $minLength; $i++) {
if ($baseParts[$i] === $currentParts[$i]) {
$commonLength++;
} else {
break;
}
}
// Build relative path
$upLevels = count($baseParts) - $commonLength;
$downParts = array_slice($currentParts, $commonLength);
$relativeParts = array_merge(
array_fill(0, $upLevels, '..'),
$downParts
);
return new self(implode(DIRECTORY_SEPARATOR, $relativeParts));
}
/**
* Join paths together
*/
public function join(string ...$parts): self
{
$path = $this->normalized;
foreach ($parts as $part) {
$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($part, DIRECTORY_SEPARATOR);
}
return new self($path);
}
/**
* Add extension to path
*/
public function withExtension(string $extension): self
{
$extension = ltrim($extension, '.');
$basePath = $this->getDirectory()->join($this->getBasename());
return new self($basePath . '.' . $extension);
}
/**
* Change filename keeping directory and extension
*/
public function withFilename(string $filename): self
{
$extension = $this->getExtension();
$newPath = $this->getDirectory()->join($filename);
return $extension ? $newPath->withExtension($extension) : $newPath;
}
/**
* Change directory keeping filename
*/
public function withDirectory(self $directory): self
{
return $directory->join($this->getFilename());
}
/**
* Check if path is absolute
*/
public function isAbsolute(): bool
{
return $this->isAbsolutePath($this->normalized);
}
/**
* Check if path is relative
*/
public function isRelative(): bool
{
return ! $this->isAbsolute();
}
/**
* Check if path exists in filesystem
*/
public function exists(): bool
{
return file_exists($this->normalized);
}
/**
* Check if path is a file
*/
public function isFile(): bool
{
return is_file($this->normalized);
}
/**
* Check if path is a directory
*/
public function isDirectory(): bool
{
return is_dir($this->normalized);
}
/**
* Check if path is readable
*/
public function isReadable(): bool
{
return is_readable($this->normalized);
}
/**
* Check if path is writable
*/
public function isWritable(): bool
{
return is_writable($this->normalized);
}
/**
* Get file size as Byte object
*/
public function getSize(): Byte
{
if (! $this->isFile()) {
return Byte::zero();
}
$size = filesize($this->normalized);
return Byte::fromBytes($size ?: 0);
}
/**
* Get last modification time
*/
public function getModifiedTime(): int
{
return filemtime($this->normalized) ?: 0;
}
/**
* Check if this path contains another path (security check)
*/
public function contains(self $other): bool
{
$basePath = rtrim($this->normalized, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$checkPath = $other->normalized;
return str_starts_with($checkPath, $basePath);
}
/**
* Get path components as array
*
* @return string[]
*/
public function getComponents(): array
{
return explode(DIRECTORY_SEPARATOR, trim($this->normalized, DIRECTORY_SEPARATOR));
}
/**
* Get depth (number of directory levels)
*/
public function getDepth(): int
{
return count($this->getComponents());
}
/**
* Compare paths for equality
*/
public function equals(self $other): bool
{
return $this->normalized === $other->normalized;
}
/**
* Normalize path across platforms
*/
private function normalizePath(string $path): string
{
// Convert backslashes to forward slashes
$path = str_replace('\\', DIRECTORY_SEPARATOR, $path);
// Handle multiple separators
$path = preg_replace('#' . preg_quote(DIRECTORY_SEPARATOR) . '+#', DIRECTORY_SEPARATOR, $path);
// Resolve . and .. components
$parts = explode(DIRECTORY_SEPARATOR, $path);
$resolved = [];
$isAbsolute = $this->isAbsolutePath($path);
foreach ($parts as $part) {
if ($part === '' && ! $isAbsolute) {
continue;
}
if ($part === '.') {
continue;
}
if ($part === '..') {
if (! empty($resolved) && end($resolved) !== '..') {
array_pop($resolved);
} elseif (! $isAbsolute) {
$resolved[] = '..';
}
continue;
}
$resolved[] = $part;
}
$normalized = implode(DIRECTORY_SEPARATOR, $resolved);
// Ensure absolute paths start with separator
if ($isAbsolute && ! str_starts_with($normalized, DIRECTORY_SEPARATOR)) {
$normalized = DIRECTORY_SEPARATOR . $normalized;
}
return $normalized ?: DIRECTORY_SEPARATOR;
}
/**
* Check if path is absolute (cross-platform)
*/
private function isAbsolutePath(string $path): bool
{
// Unix absolute path
if (str_starts_with($path, '/')) {
return true;
}
// Windows absolute path (C:\ or \\server\share)
if (PHP_OS_FAMILY === 'Windows') {
return preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) || str_starts_with($path, '\\\\');
}
return false;
}
/**
* Validate path for security issues
*/
private function validateSecurity(string $path): void
{
// Check for null bytes
if (str_contains($path, "\0")) {
throw new InvalidArgumentException('Path contains null bytes');
}
// Check for Windows reserved names
if (PHP_OS_FAMILY === 'Windows') {
$basename = strtoupper($this->getBasename());
if (in_array($basename, self::WINDOWS_RESERVED_NAMES, true)) {
throw new InvalidArgumentException("Path contains Windows reserved name: {$basename}");
}
}
// Check for suspicious patterns (basic path traversal)
if (str_contains($path, '..')) {
// Allow .. in normalized paths, but check final result doesn't escape intended boundaries
// This is a basic check - more sophisticated validation can be added
}
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Filesystem\ValueObjects\FileCollection;
use App\Framework\Filesystem\ValueObjects\FilePattern;
use App\Framework\Logging\Logger;
use App\Framework\Performance\MemoryMonitor;
use FilesystemIterator;
use Generator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use Throwable;
/**
* Generic file scanner for filesystem operations
* Moved from Discovery module to be reusable across the framework
*/
class FileScanner implements FileScannerInterface
{
private ?Logger $logger;
private MemoryMonitor $memoryMonitor;
private FileSystemService $fileSystemService;
public function __construct(
?Logger $logger = null,
?MemoryMonitor $memoryMonitor = null,
?FileSystemService $fileSystemService = null
) {
$this->logger = $logger;
$this->memoryMonitor = $memoryMonitor ?? new MemoryMonitor();
$this->fileSystemService = $fileSystemService ?? new FileSystemService();
}
/**
* Find all files matching a pattern in a directory
*/
public function findFiles(FilePath $directoryPath, ?FilePattern $pattern = null): FileCollection
{
if (! $directoryPath->exists()) {
return FileCollection::empty();
}
try {
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directoryPath->toString(), FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
if ($pattern !== null) {
$iterator = new RegexIterator($iterator, $pattern->toRegex());
}
foreach ($iterator as $fileInfo) {
try {
$file = File::fromSplFileInfo($fileInfo);
$files[] = $file;
} catch (Throwable $e) {
$this->logger?->warning("Skipping file {$fileInfo->getPathname()}: {$e->getMessage()}");
continue;
}
}
return FileCollection::fromFiles($files);
} catch (Throwable $e) {
$this->logger?->error("Failed to scan directory {$directoryPath->toString()}: {$e->getMessage()}");
return FileCollection::empty();
}
}
/**
* Get the latest modification time of all files matching pattern
*/
public function getLatestModificationTime(FilePath $directoryPath, ?FilePattern $pattern = null): Timestamp
{
$files = $this->findFiles($directoryPath, $pattern);
if ($files->isEmpty()) {
return Timestamp::now();
}
$latestTime = 0;
foreach ($files as $file) {
$modTime = $file->getModificationTime()->toFloat();
if ($modTime > $latestTime) {
$latestTime = $modTime;
}
}
return Timestamp::fromFloat($latestTime);
}
/**
* Find files that have been modified since a given timestamp
*/
public function findFilesModifiedSince(FilePath $directoryPath, Timestamp $since, ?FilePattern $pattern = null): FileCollection
{
$allFiles = $this->findFiles($directoryPath, $pattern);
return $allFiles->modifiedAfter($since);
}
/**
* Stream files one by one for memory-efficient processing
* @return Generator<File>
*/
public function streamFiles(FilePath $directoryPath, ?FilePattern $pattern = null): Generator
{
if (! $directoryPath->exists()) {
return;
}
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directoryPath->toString(), FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
if ($pattern !== null) {
$iterator = new RegexIterator($iterator, $pattern->toRegex());
}
foreach ($iterator as $fileInfo) {
try {
yield File::fromSplFileInfo($fileInfo);
} catch (Throwable $e) {
$this->logger?->warning("Skipping file {$fileInfo->getPathname()}: {$e->getMessage()}");
continue;
}
}
} catch (Throwable $e) {
$this->logger?->error("Failed to scan directory {$directoryPath->toString()}: {$e->getMessage()}");
}
}
/**
* Stream files in batches for balanced memory/performance
* @return Generator<FileCollection>
*/
public function streamFilesInBatches(FilePath $directoryPath, ?FilePattern $pattern = null, int $batchSize = 100): Generator
{
$batch = [];
$count = 0;
foreach ($this->streamFiles($directoryPath, $pattern) as $file) {
$batch[] = $file;
$count++;
if ($count >= $batchSize) {
yield FileCollection::fromFiles($batch);
$batch = [];
$count = 0;
}
}
// Yield remaining files
if (! empty($batch)) {
yield FileCollection::fromFiles($batch);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Filesystem\ValueObjects\FileCollection;
use App\Framework\Filesystem\ValueObjects\FilePattern;
use Generator;
/**
* Interface for all FileScanner implementations
* Generic filesystem scanning interface with type-safe Value Objects
*/
interface FileScannerInterface
{
/**
* Find all files matching a pattern in a directory
*/
public function findFiles(FilePath $directoryPath, ?FilePattern $pattern = null): FileCollection;
/**
* Get the latest modification time of all files matching pattern
*/
public function getLatestModificationTime(FilePath $directoryPath, ?FilePattern $pattern = null): Timestamp;
/**
* Find files that have been modified since a given timestamp
*/
public function findFilesModifiedSince(FilePath $directoryPath, Timestamp $since, ?FilePattern $pattern = null): FileCollection;
/**
* Stream files one by one for memory-efficient processing
* @return Generator<File>
*/
public function streamFiles(FilePath $directoryPath, ?FilePattern $pattern = null): Generator;
/**
* Stream files in batches for balanced memory/performance
* @return Generator<FileCollection>
*/
public function streamFilesInBatches(FilePath $directoryPath, ?FilePattern $pattern = null, int $batchSize = 100): Generator;
}

View File

@@ -1,87 +1,185 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Async\FiberManager;
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
use App\Framework\Filesystem\Exceptions\DirectoryListException;
use App\Framework\Filesystem\Exceptions\FileCopyException;
use App\Framework\Filesystem\Exceptions\FileDeleteException;
use App\Framework\Filesystem\Exceptions\FileMetadataException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FilePermissionException;
use App\Framework\Filesystem\Exceptions\FileReadException;
use App\Framework\Filesystem\Exceptions\FileWriteException;
use App\Framework\Filesystem\Traits\AppendableStorageTrait;
use App\Framework\Filesystem\Traits\AtomicStorageTrait;
use App\Framework\Filesystem\Traits\CompressibleStorageTrait;
use App\Framework\Filesystem\Traits\LockableStorageTrait;
use App\Framework\Filesystem\Traits\StreamableStorageTrait;
final readonly class FileStorage implements Storage
final readonly class FileStorage implements Storage, AtomicStorage, AppendableStorage, StreamableStorage, LockableStorage, CompressibleStorage
{
use StorageTrait;
use AtomicStorageTrait;
use AppendableStorageTrait;
use StreamableStorageTrait;
use LockableStorageTrait;
use CompressibleStorageTrait;
public readonly PermissionChecker $permissions;
public readonly FiberManager $fiberManager;
public function __construct(
private string $basePath = '/',
?FiberManager $fiberManager = null
) {
$this->permissions = new PermissionChecker($this);
$this->fiberManager = $fiberManager ?? $this->createDefaultFiberManager();
}
private function createDefaultFiberManager(): FiberManager
{
// Create minimal dependencies for FiberManager
$clock = new \App\Framework\DateTime\SystemClock();
$timer = new \App\Framework\DateTime\SystemTimer($clock);
return new FiberManager($clock, $timer);
}
private function resolvePath(string $path): string
{
// Absolute path bleibt unverändert
if (str_starts_with($path, '/')) {
return $path;
}
// Relative paths werden an basePath gehängt
return rtrim($this->basePath, '/') . '/' . ltrim($path, '/');
}
public function get(string $path): string
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
$content = @file_get_contents($path);
if (! is_readable($resolvedPath)) {
throw FilePermissionException::read($path, 'File is not readable');
}
$content = @file_get_contents($resolvedPath);
if ($content === false) {
$error = error_get_last();
if ($error && str_contains($error['message'], 'Permission denied')) {
throw FilePermissionException::read($path, $error['message']);
}
throw new FileReadException($path);
}
return $content;
}
public function put(string $path, string $content): void
{
$dir = dirname($path);
if (!is_dir($dir)) {
if (!@mkdir($dir, 0777, true) && !is_dir($dir)) {
$resolvedPath = $this->resolvePath($path);
$dir = dirname($resolvedPath);
// Prüfe Directory-Permissions
if (! is_dir($dir)) {
if (! @mkdir($dir, 0777, true) && ! is_dir($dir)) {
$error = error_get_last();
if ($error && str_contains($error['message'], 'Permission denied')) {
throw FilePermissionException::createDirectory($dir, $error['message']);
}
throw new DirectoryCreateException($dir);
}
} elseif (! is_writable($dir)) {
throw FilePermissionException::write($path, 'Directory is not writable: ' . $dir);
}
if (@file_put_contents($path, $content) === false) {
// Prüfe File-Permissions wenn Datei bereits existiert
if (is_file($resolvedPath) && ! is_writable($resolvedPath)) {
throw FilePermissionException::write($path, 'File is not writable');
}
if (@file_put_contents($resolvedPath, $content) === false) {
$error = error_get_last();
if ($error && str_contains($error['message'], 'Permission denied')) {
throw FilePermissionException::write($path, $error['message']);
}
throw new FileWriteException($path);
}
}
public function exists(string $path): bool
{
return is_file($path);
return is_file($this->resolvePath($path));
}
public function delete(string $path): void
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
if (!@unlink($path)) {
// Prüfe Directory-Permissions
$dir = dirname($resolvedPath);
if (! is_writable($dir)) {
throw FilePermissionException::delete($path, 'Directory is not writable: ' . $dir);
}
// Prüfe File-Permissions
if (! is_writable($resolvedPath)) {
throw FilePermissionException::delete($path, 'File is not writable');
}
if (! @unlink($resolvedPath)) {
$error = error_get_last();
if ($error && str_contains($error['message'], 'Permission denied')) {
throw FilePermissionException::delete($path, $error['message']);
}
throw new FileDeleteException($path);
}
}
public function copy(string $source, string $destination): void
{
if (!is_file($source)) {
$resolvedSource = $this->resolvePath($source);
$resolvedDestination = $this->resolvePath($destination);
if (! is_file($resolvedSource)) {
throw new FileNotFoundException($source);
}
// Stelle sicher, dass das Zielverzeichnis existiert
$dir = dirname($destination);
if (!is_dir($dir)) {
$this->createDirectory($dir);
$dir = dirname($resolvedDestination);
if (! is_dir($dir)) {
$this->createDirectory($destination);
}
if (!@copy($source, $destination)) {
if (! @copy($resolvedSource, $resolvedDestination)) {
throw new FileCopyException($source, $destination);
}
}
public function size(string $path): int
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
$size = @filesize($path);
$size = @filesize($resolvedPath);
if ($size === false) {
throw new FileReadException($path);
}
@@ -91,11 +189,12 @@ final readonly class FileStorage implements Storage
public function lastModified(string $path): int
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
$time = @filemtime($path);
$time = @filemtime($resolvedPath);
if ($time === false) {
throw new FileReadException($path);
}
@@ -105,11 +204,12 @@ final readonly class FileStorage implements Storage
public function getMimeType(string $path): string
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
$mimeType = @mime_content_type($path);
$mimeType = @mime_content_type($resolvedPath);
if ($mimeType === false) {
throw new FileMetadataException($path);
}
@@ -119,29 +219,32 @@ final readonly class FileStorage implements Storage
public function isReadable(string $path): bool
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
return is_readable($path);
return is_readable($resolvedPath);
}
public function isWritable(string $path): bool
{
if (!is_file($path)) {
$resolvedPath = $this->resolvePath($path);
if (! is_file($resolvedPath)) {
throw new FileNotFoundException($path);
}
return is_writable($path);
return is_writable($resolvedPath);
}
public function listDirectory(string $directory): array
{
if (!is_dir($directory)) {
$resolvedDirectory = $this->resolvePath($directory);
if (! is_dir($resolvedDirectory)) {
throw new DirectoryCreateException($directory);
}
$files = @scandir($directory);
$files = @scandir($resolvedDirectory);
if ($files === false) {
throw new DirectoryListException($directory);
}
@@ -151,10 +254,15 @@ final readonly class FileStorage implements Storage
return $file !== '.' && $file !== '..';
});
// Vollständige Pfade erstellen
// Relative Pfade zurückgeben (ohne basePath)
$result = [];
foreach ($files as $file) {
$result[] = $directory . DIRECTORY_SEPARATOR . $file;
$fullPath = $resolvedDirectory . DIRECTORY_SEPARATOR . $file;
// Entferne basePath für relativen Pfad
$relativePath = str_starts_with($fullPath, $this->basePath)
? substr($fullPath, strlen(rtrim($this->basePath, '/')) + 1)
: $fullPath;
$result[] = $relativePath;
}
return array_values($result);
@@ -162,12 +270,67 @@ final readonly class FileStorage implements Storage
public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void
{
if (is_dir($path)) {
$resolvedPath = $this->resolvePath($path);
if (is_dir($resolvedPath)) {
return; // Verzeichnis existiert bereits
}
if (!@mkdir($path, $permissions, $recursive) && !is_dir($path)) {
// Prüfe Parent-Directory Permissions
$parentDir = dirname($resolvedPath);
if (is_dir($parentDir) && ! is_writable($parentDir)) {
throw FilePermissionException::createDirectory($path, 'Parent directory is not writable: ' . $parentDir);
}
if (! @mkdir($resolvedPath, $permissions, $recursive) && ! is_dir($resolvedPath)) {
$error = error_get_last();
if ($error && str_contains($error['message'], 'Permission denied')) {
throw FilePermissionException::createDirectory($path, $error['message']);
}
throw new DirectoryCreateException($path);
}
}
// Batch-Operationen für explizite Parallelverarbeitung
public function batch(array $operations): array
{
return $this->fiberManager->batch($operations);
}
public function getMultiple(array $paths): array
{
$operations = [];
foreach ($paths as $path) {
$operations[$path] = fn () => $this->get($path);
}
return $this->batch($operations);
}
public function putMultiple(array $files): void
{
$operations = [];
foreach ($files as $path => $content) {
$operations[$path] = fn () => $this->put($path, $content);
}
$this->batch($operations);
}
public function getMetadataMultiple(array $paths): array
{
$operations = [];
foreach ($paths as $path) {
$operations[$path] = fn () => new FileMetadata(
size: $this->size($path),
lastModified: $this->lastModified($path),
mimeType: $this->getMimeType($path),
isReadable: $this->isReadable($path),
isWritable: $this->isWritable($path)
);
}
return $this->batch($operations);
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Service for file system operations.
* Handles all file I/O operations separate from File value objects.
*/
final class FileSystemService
{
public function __construct(
private readonly Storage $storage = new FileStorage()
) {
}
/**
* Read file content
*/
public function readFile(File $file): string
{
return $this->storage->get($file->getPathString());
}
/**
* Write content to file
*/
public function writeFile(File $file, string $content): void
{
$this->storage->put($file->getPathString(), $content);
}
/**
* Create File object from path (loads metadata automatically)
*/
public function createFile(FilePath $path): File
{
$pathString = $path->toString();
$metadata = new FileMetadata(
size: $this->storage->size($pathString),
lastModified: $this->storage->lastModified($pathString),
mimeType: $this->storage->getMimeType($pathString),
isReadable: $this->storage->isReadable($pathString),
isWritable: $this->storage->isWritable($pathString)
);
return new File($path, $metadata);
}
/**
* Check if file exists
*/
public function fileExists(FilePath $path): bool
{
return $this->storage->exists($path->toString());
}
/**
* Delete file
*/
public function deleteFile(File $file): void
{
$this->storage->delete($file->getPathString());
}
/**
* Copy file to destination
*/
public function copyFile(File $file, FilePath $destination): File
{
$this->storage->copy($file->getPathString(), $destination->toString());
return $this->createFile($destination);
}
/**
* Get fresh metadata for existing file
*/
public function getMetadata(FilePath $path): FileMetadata
{
$pathString = $path->toString();
return new FileMetadata(
size: $this->storage->size($pathString),
lastModified: $this->storage->lastModified($pathString),
mimeType: $this->storage->getMimeType($pathString),
isReadable: $this->storage->isReadable($pathString),
isWritable: $this->storage->isWritable($pathString)
);
}
/**
* Refresh file with current metadata
*/
public function refreshFile(File $file): File
{
$metadata = $this->getMetadata($file->getPath());
return new File($file->getPath(), $metadata);
}
/**
* Stream file content line by line for memory-efficient processing
* @return Generator<string>
*/
public function streamFileLines(File $file): \Generator
{
$handle = fopen($file->getPathString(), 'r');
if ($handle === false) {
throw new \RuntimeException("Cannot open file: " . $file->getPathString());
}
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
fclose($handle);
}
}
/**
* Stream file content in chunks for large file processing
* @return Generator<string>
*/
public function streamFileChunks(File $file, int $chunkSize = 8192): \Generator
{
$handle = fopen($file->getPathString(), 'r');
if ($handle === false) {
throw new \RuntimeException("Cannot open file: " . $file->getPathString());
}
try {
while (! feof($handle)) {
$chunk = fread($handle, $chunkSize);
if ($chunk !== false && $chunk !== '') {
yield $chunk;
}
}
} finally {
fclose($handle);
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Konfiguration für das Filesystem-Modul
*
* Definiert Storage-Backends, Pfade und weitere Filesystem-Einstellungen
* entsprechend der Framework-Konventionen.
*/
final readonly class FilesystemConfig
{
/**
* @param array<string, array{type: string, path?: string, options?: array<string, mixed>}> $storages
*/
public function __construct(
public string $defaultStorage = 'local',
public array $storages = [
'local' => [
'type' => 'file',
'path' => '/var/www/html/storage',
],
],
public bool $enableCompression = false,
public string $defaultCompressionAlgorithm = 'gzip',
public bool $enableAtomicWrites = true,
public bool $enableFileLocking = true,
public int $defaultFilePermissions = 0644,
public int $defaultDirectoryPermissions = 0755,
public bool $createDirectoriesAutomatically = true,
public int $streamBufferSize = 8192,
public array $serializers = [
'json' => 'json',
'php' => 'php',
'csv' => 'csv',
]
) {
}
public function getStorageConfig(string $name): array
{
if (! isset($this->storages[$name])) {
throw new \InvalidArgumentException("Storage '{$name}' not configured");
}
return $this->storages[$name];
}
public function hasStorage(string $name): bool
{
return isset($this->storages[$name]);
}
public function getDefaultStorageConfig(): array
{
return $this->getStorageConfig($this->defaultStorage);
}
/**
* @return array<string>
*/
public function getAvailableStorages(): array
{
return array_keys($this->storages);
}
public function isCompressionEnabled(): bool
{
return $this->enableCompression && extension_loaded('zlib');
}
public function isSerializerAvailable(string $type): bool
{
return isset($this->serializers[$type]);
}
/**
* @return array<string>
*/
public function getAvailableSerializers(): array
{
return array_keys($this->serializers);
}
}

View File

@@ -1,51 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LoggerFactory;
use ReflectionClass;
/**
* Factory für Filesystem-Objekte mit Lazy-Loading-Unterstützung.
*/
final class FilesystemFactory
final readonly class FilesystemFactory
{
/**
* Erstellt ein File-Objekt mit Lazy-Loading für schwere Eigenschaften.
*
* @param string $path Pfad zur Datei
* @param FilePath|string $path Pfad zur Datei
* @param Storage $storage Storage-Implementierung
* @param int|null $cacheTimeoutSeconds Optional, Zeit in Sekunden, nach der der Cache ungültig wird
* @param bool $lazyLoad Optional, ob Lazy-Loading verwendet werden soll
* @param DefaultLogger|null $logger Optional, Logger für Debug-Informationen
*/
public static function createFile(
string $path,
FilePath|string $path,
Storage $storage,
?int $cacheTimeoutSeconds = null,
bool $lazyLoad = true,
?DefaultLogger $logger = null
): File {
$logger ??= LoggerFactory::getDefaultLogger();
$pathString = $path instanceof FilePath ? $path->toString() : $path;
// Direkte Instanziierung ohne Lazy-Loading
if (!$lazyLoad) {
$logger->debug("Erstelle File-Objekt ohne Lazy-Loading: {$path}");
if (! $lazyLoad) {
$logger->debug("Erstelle File-Objekt ohne Lazy-Loading: {$pathString}");
// Nur laden wenn die Datei existiert
if (!$storage->exists($path)) {
if (! $storage->exists($pathString)) {
return new File($path, $storage);
}
return new File(
path: $path,
storage: $storage,
contents: $storage->get($path),
size: $storage->size($path),
lastModified: $storage->lastModified($path)
contents: $storage->get($pathString),
size: $storage->size($pathString),
lastModified: $storage->lastModified($pathString)
);
}
@@ -55,54 +56,60 @@ final class FilesystemFactory
// LazyProxy verwenden für individuelle Property-Callbacks
return $reflection->newLazyProxy([
// Dateiinhalt wird erst beim ersten Zugriff geladen
'contents' => function(File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$logger->debug("Lazy-Loading contents für {$file->path}");
'contents' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$pathStr = $file->getPathString();
$logger->debug("Lazy-Loading contents für {$pathStr}");
// Cache-Invalidierung basierend auf Zeit
if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) {
$logger->debug("Cache-Timeout erreicht für {$file->path}, lade neu");
$logger->debug("Cache-Timeout erreicht für {$pathStr}, lade neu");
}
if (!$file->exists()) {
$logger->debug("Datei existiert nicht: {$file->path}");
if (! $file->exists()) {
$logger->debug("Datei existiert nicht: {$pathStr}");
return '';
}
return $file->storage->get($file->path);
return $file->storage->get($pathStr);
},
// Dateigröße wird erst beim ersten Zugriff ermittelt
'size' => function(File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$logger->debug("Lazy-Loading size für {$file->path}");
'size' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$pathStr = $file->getPathString();
$logger->debug("Lazy-Loading size für {$pathStr}");
// Cache-Invalidierung basierend auf Zeit
if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) {
$logger->debug("Cache-Timeout erreicht für {$file->path}, lade neu");
$logger->debug("Cache-Timeout erreicht für {$pathStr}, lade neu");
}
if (!$file->exists()) {
$logger->debug("Datei existiert nicht: {$file->path}");
if (! $file->exists()) {
$logger->debug("Datei existiert nicht: {$pathStr}");
return 0;
}
return $file->storage->size($file->path);
return $file->storage->size($pathStr);
},
// Zeitstempel wird erst beim ersten Zugriff ermittelt
'lastModified' => function(File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$logger->debug("Lazy-Loading lastModified für {$file->path}");
'lastModified' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$pathStr = $file->getPathString();
$logger->debug("Lazy-Loading lastModified für {$pathStr}");
// Cache-Invalidierung basierend auf Zeit
if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) {
$logger->debug("Cache-Timeout erreicht für {$file->path}, lade neu");
$logger->debug("Cache-Timeout erreicht für {$pathStr}, lade neu");
}
if (!$file->exists()) {
$logger->debug("Datei existiert nicht: {$file->path}");
if (! $file->exists()) {
$logger->debug("Datei existiert nicht: {$pathStr}");
return 0;
}
return $file->storage->lastModified($file->path);
return $file->storage->lastModified($pathStr);
},
]);
}
@@ -110,26 +117,27 @@ final class FilesystemFactory
/**
* Erstellt ein Directory-Objekt mit Lazy-Loading für schwere Eigenschaften.
*
* @param string $path Pfad zum Verzeichnis
* @param FilePath|string $path Pfad zum Verzeichnis
* @param Storage $storage Storage-Implementierung
* @param bool $lazyLoad Optional, ob Lazy-Loading verwendet werden soll
* @param DefaultLogger|null $logger Optional, Logger für Debug-Informationen
*/
public static function createDirectory(
string $path,
FilePath|string $path,
Storage $storage,
bool $lazyLoad = true,
?DefaultLogger $logger = null
): Directory {
$logger ??= LoggerFactory::getDefaultLogger();
$pathString = $path instanceof FilePath ? $path->toString() : $path;
// Direkte Instanziierung ohne Lazy-Loading
if (!$lazyLoad) {
$logger->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$path}");
if (! $lazyLoad) {
$logger->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$pathString}");
$contents = [];
if (is_dir($path)) {
$contents = $storage->listDirectory($path);
if (is_dir($pathString)) {
$contents = $storage->listDirectory($pathString);
}
return new Directory($path, $storage, $contents);
@@ -140,17 +148,19 @@ final class FilesystemFactory
// LazyGhost verwenden - alle Eigenschaften werden beim ersten Zugriff initialisiert
$lazyDir = $reflection->newLazyGhost(
// Initializer-Callback
function(Directory $directory) use ($logger): void {
$logger->debug("Lazy-Loading Directory-Inhalt für {$directory->path}");
function (Directory $directory) use ($logger): void {
$pathStr = $directory->getPathString();
$logger->debug("Lazy-Loading Directory-Inhalt für {$pathStr}");
// Verzeichnisinhalt wird erst beim ersten Zugriff auf eine Eigenschaft geladen
if ($directory->exists()) {
$directory->contents = $directory->storage->listDirectory($directory->path);
$directory->contents = $directory->storage->listDirectory($pathStr);
} else {
$directory->contents = [];
}
}
);
/** @var Directory $lazyDir */
return $lazyDir;
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Filesystem\Serializers\CsvSerializer;
use App\Framework\Filesystem\Serializers\JsonSerializer;
use App\Framework\Filesystem\Serializers\PhpSerializer;
/**
* Filesystem-System Initializer
*
* Registriert alle Filesystem-Komponenten im DI-Container
* entsprechend der Framework-Konventionen.
*/
final readonly class FilesystemInitializer
{
#[Initializer]
public function initializeFilesystem(Container $container): void
{
// Filesystem Config
$container->singleton(FilesystemConfig::class, function () {
return new FilesystemConfig(
defaultStorage: $_ENV['FILESYSTEM_DEFAULT_STORAGE'] ?? 'local',
storages: [
'local' => [
'type' => 'file',
'path' => $_ENV['FILESYSTEM_LOCAL_PATH'] ?? '/var/www/html/storage',
],
'temp' => [
'type' => 'file',
'path' => $_ENV['FILESYSTEM_TEMP_PATH'] ?? '/tmp',
],
'analytics' => [
'type' => 'file',
'path' => $_ENV['ANALYTICS_DATA_PATH'] ?? '/var/www/html/storage/analytics',
],
],
enableCompression: filter_var($_ENV['FILESYSTEM_COMPRESSION'] ?? 'false', FILTER_VALIDATE_BOOLEAN),
enableAtomicWrites: filter_var($_ENV['FILESYSTEM_ATOMIC_WRITES'] ?? 'true', FILTER_VALIDATE_BOOLEAN),
enableFileLocking: filter_var($_ENV['FILESYSTEM_FILE_LOCKING'] ?? 'true', FILTER_VALIDATE_BOOLEAN)
);
});
// Serializers
$container->singleton(JsonSerializer::class, function () {
return new JsonSerializer();
});
$container->singleton(PhpSerializer::class, function () {
return new PhpSerializer();
});
$container->singleton(CsvSerializer::class, function () {
return new CsvSerializer();
});
// Storage Instances
$this->registerStorages($container);
// Filesystem Manager
$container->singleton(FilesystemManager::class, function (Container $container) {
$config = $container->get(FilesystemConfig::class);
$manager = new FilesystemManager($config->defaultStorage);
// Registriere alle konfigurierten Storages
foreach ($config->getAvailableStorages() as $storageName) {
$storage = $container->get("filesystem.storage.{$storageName}");
$manager->registerStorage($storageName, $storage);
}
// Registriere Serializers
$manager->registerSerializer('json', $container->get(JsonSerializer::class));
$manager->registerSerializer('php', $container->get(PhpSerializer::class));
$manager->registerSerializer('csv', $container->get(CsvSerializer::class));
return $manager;
});
}
private function registerStorages(Container $container): void
{
$container->singleton(Storage::class, function () {
return new FileStorage('/');
});
$container->singleton(FileStorage::class, function () {
return new FileStorage('/');
});
$container->singleton('filesystem.storage.local', function (Container $container) {
$config = $container->get(FilesystemConfig::class);
$storageConfig = $config->getStorageConfig('local');
return new FileStorage($storageConfig['path']);
});
$container->singleton('filesystem.storage.temp', function (Container $container) {
$config = $container->get(FilesystemConfig::class);
$storageConfig = $config->getStorageConfig('temp');
return new FileStorage($storageConfig['path']);
});
$container->singleton('filesystem.storage.analytics', function (Container $container) {
$config = $container->get(FilesystemConfig::class);
$storageConfig = $config->getStorageConfig('analytics');
return new FileStorage($storageConfig['path']);
});
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Zentraler Filesystem-Manager mit verschiedenen Storage-Backends und Serializer-Support
*
* Ermöglicht die Verwendung verschiedener Storage-Implementierungen
* und Serializer für verschiedene Datenformate.
*/
final class FilesystemManager
{
/** @var array<string, Storage> */
private array $storages;
/** @var array<string, Serializer> */
private array $serializers = [];
public function __construct(
private string $defaultStorageName = 'default'
) {
$this->storages = [];
$this->serializers = [];
}
/**
* Holt ein Storage-Backend by Name
*/
public function storage(string $name = ''): Storage
{
$storageName = $name ?: $this->defaultStorageName;
if (! isset($this->storages[$storageName])) {
throw new \InvalidArgumentException("Storage '{$storageName}' not found");
}
return $this->storages[$storageName];
}
/**
* Registriert ein neues Storage-Backend
*/
public function registerStorage(string $name, Storage $storage): void
{
$this->storages[$name] = $storage;
}
/**
* Registriert einen Serializer
*/
public function registerSerializer(string $name, Serializer $serializer): void
{
$this->serializers[$name] = $serializer;
}
/**
* Holt einen Serializer by Name
*/
public function getSerializer(string $name): Serializer
{
if (! isset($this->serializers[$name])) {
throw new \InvalidArgumentException("Serializer '{$name}' not found");
}
return $this->serializers[$name];
}
// === Basic Storage Operations ===
public function get(string $path, string $storage = ''): string
{
return $this->storage($storage)->get($path);
}
public function put(string $path, string $content, string $storage = ''): void
{
$this->storage($storage)->put($path, $content);
}
public function exists(string $path, string $storage = ''): bool
{
return $this->storage($storage)->exists($path);
}
public function delete(string $path, string $storage = ''): void
{
$this->storage($storage)->delete($path);
}
public function copy(string $source, string $destination, string $storage = ''): void
{
$this->storage($storage)->copy($source, $destination);
}
// === Serialized Storage Operations ===
/**
* Speichert Daten mit einem Serializer
*/
public function putSerialized(string $path, mixed $data, Serializer $serializer, string $storage = ''): void
{
$content = $serializer->serialize($data);
$this->storage($storage)->put($path, $content);
}
/**
* Lädt und deserialisiert Daten
*/
public function getSerialized(string $path, Serializer $serializer, string $storage = ''): mixed
{
$content = $this->storage($storage)->get($path);
return $serializer->deserialize($content);
}
// === Convenience Methods für häufig verwendete Serializer ===
/**
* JSON-Operationen
*/
public function putJson(string $path, array $data, string $storage = ''): void
{
$this->putSerialized($path, $data, $this->getSerializer('json'), $storage);
}
public function getJson(string $path, string $storage = ''): array
{
return $this->getSerialized($path, $this->getSerializer('json'), $storage);
}
/**
* CSV-Operationen (wenn CSV-Serializer registriert ist)
*/
public function putCsv(string $path, array $data, string $storage = ''): void
{
$this->putSerialized($path, $data, $this->getSerializer('csv'), $storage);
}
public function getCsv(string $path, string $storage = ''): array
{
return $this->getSerialized($path, $this->getSerializer('csv'), $storage);
}
// === Extended Operations ===
/**
* Atomic Write (falls vom Storage unterstützt)
*/
public function putAtomic(string $path, string $content, string $storage = ''): void
{
$storageInstance = $this->storage($storage);
// Erweiterte Storage-Implementierungen können AtomicStorage Interface implementieren
if (method_exists($storageInstance, 'putAtomic')) {
$storageInstance->putAtomic($path, $content);
} else {
// Fallback: Normale put-Operation
$storageInstance->put($path, $content);
}
}
/**
* Append-Operation (falls vom Storage unterstützt)
*/
public function append(string $path, string $content, string $storage = ''): void
{
$storageInstance = $this->storage($storage);
if (method_exists($storageInstance, 'append')) {
$storageInstance->append($path, $content);
} else {
// Fallback: Lesen, anhängen, schreiben
$existing = $storageInstance->exists($path) ? $storageInstance->get($path) : '';
$storageInstance->put($path, $existing . $content);
}
}
// === Storage Management ===
/**
* Multi-Storage Operationen
*/
public function copyBetweenStorages(string $source, string $destination, string $sourceStorage = '', string $destStorage = ''): void
{
$sourceStorageName = $sourceStorage ?: $this->defaultStorageName;
$destStorageName = $destStorage ?: $this->defaultStorageName;
$content = $this->storage($sourceStorageName)->get($source);
$this->storage($destStorageName)->put($destination, $content);
}
/**
* Storage-Statistiken
*/
public function getStorageInfo(): array
{
$info = [];
foreach ($this->storages as $name => $storage) {
$info[$name] = [
'type' => get_class($storage),
'available' => true, // Könnte erweitert werden für Health-Checks
];
}
return $info;
}
/**
* Serializer-Statistiken
*/
public function getSerializerInfo(): array
{
$info = [];
foreach ($this->serializers as $name => $serializer) {
$info[$name] = [
'type' => get_class($serializer),
'mime_type' => $serializer->getMimeType(),
'extension' => $serializer->getFileExtension(),
];
}
return $info;
}
/**
* Factory-Methode für Standard-Setup
*/
public static function create(array $config = []): self
{
$defaultStorage = 'default';
$namedStorages = [];
$manager = new self($defaultStorage, $namedStorages);
// Weitere Standard-Serializer können hier registriert werden
if (isset($config['serializers'])) {
foreach ($config['serializers'] as $name => $serializer) {
$manager->registerSerializer($name, $serializer);
}
}
return $manager;
}
}

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Async\FiberManager;
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FileReadException;
@@ -21,9 +23,28 @@ final class InMemoryStorage implements Storage
/** @var array<string, int> */
private array $timestamps = [];
public readonly PermissionChecker $permissions;
public readonly FiberManager $fiberManager;
public function __construct()
{
$this->permissions = new PermissionChecker($this);
$this->fiberManager = $this->createDefaultFiberManager();
}
private function createDefaultFiberManager(): FiberManager
{
// Create minimal dependencies for FiberManager
$clock = new \App\Framework\DateTime\SystemClock();
$timer = new \App\Framework\DateTime\SystemTimer($clock);
return new FiberManager($clock, $timer);
}
public function get(string $path): string
{
if (!isset($this->files[$path])) {
if (! isset($this->files[$path])) {
throw new FileNotFoundException($path);
}
@@ -33,7 +54,7 @@ final class InMemoryStorage implements Storage
public function put(string $path, string $content): void
{
$dir = dirname($path);
if (!isset($this->directories[$dir])) {
if (! isset($this->directories[$dir])) {
$this->createDirectory($dir);
}
@@ -48,7 +69,7 @@ final class InMemoryStorage implements Storage
public function delete(string $path): void
{
if (!isset($this->files[$path])) {
if (! isset($this->files[$path])) {
throw new FileNotFoundException($path);
}
@@ -58,12 +79,12 @@ final class InMemoryStorage implements Storage
public function copy(string $source, string $destination): void
{
if (!isset($this->files[$source])) {
if (! isset($this->files[$source])) {
throw new FileNotFoundException($source);
}
$dir = dirname($destination);
if (!isset($this->directories[$dir])) {
if (! isset($this->directories[$dir])) {
$this->createDirectory($dir);
}
@@ -73,7 +94,7 @@ final class InMemoryStorage implements Storage
public function size(string $path): int
{
if (!isset($this->files[$path])) {
if (! isset($this->files[$path])) {
throw new FileNotFoundException($path);
}
@@ -82,7 +103,7 @@ final class InMemoryStorage implements Storage
public function lastModified(string $path): int
{
if (!isset($this->files[$path])) {
if (! isset($this->files[$path])) {
throw new FileNotFoundException($path);
}
@@ -91,7 +112,7 @@ final class InMemoryStorage implements Storage
public function listDirectory(string $directory): array
{
if (!isset($this->directories[$directory])) {
if (! isset($this->directories[$directory])) {
throw new DirectoryCreateException($directory);
}
@@ -104,7 +125,7 @@ final class InMemoryStorage implements Storage
if (str_starts_with($path, $prefix)) {
// Nur direkte Kinder, keine tieferen Ebenen
$relativePath = substr($path, $prefixLength);
if (!str_contains($relativePath, DIRECTORY_SEPARATOR)) {
if (! str_contains($relativePath, DIRECTORY_SEPARATOR)) {
$result[] = $path;
}
}
@@ -115,7 +136,7 @@ final class InMemoryStorage implements Storage
if ($path !== $directory && strpos($path, $prefix) === 0) {
// Nur direkte Kinder
$relativePath = substr($path, $prefixLength);
if (!str_contains($relativePath, DIRECTORY_SEPARATOR)) {
if (! str_contains($relativePath, DIRECTORY_SEPARATOR)) {
$result[] = $path;
}
}
@@ -180,7 +201,7 @@ final class InMemoryStorage implements Storage
public function getMimeType(string $path): string
{
if (!isset($this->files[$path])) {
if (! isset($this->files[$path])) {
throw new FileNotFoundException($path);
}
@@ -204,4 +225,45 @@ final class InMemoryStorage implements Storage
{
return true;
}
public function batch(array $operations): array
{
return $this->fiberManager->batch($operations);
}
public function getMultiple(array $paths): array
{
$operations = [];
foreach ($paths as $path) {
$operations[$path] = fn () => $this->get($path);
}
return $this->batch($operations);
}
public function putMultiple(array $files): void
{
$operations = [];
foreach ($files as $path => $content) {
$operations[$path] = fn () => $this->put($path, $content);
}
$this->batch($operations);
}
public function getMetadataMultiple(array $paths): array
{
$operations = [];
foreach ($paths as $path) {
$operations[$path] = fn () => new FileMetadata(
path: $path,
size: $this->size($path),
lastModified: $this->lastModified($path),
mimeType: $this->getMimeType($path),
isReadable: $this->isReadable($path),
isWritable: $this->isWritable($path)
);
}
return $this->batch($operations);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Interface für File-Locking Storage-Operationen
*
* Ermöglicht exklusiven Zugriff auf Dateien für kritische Operationen.
* Verhindert Data Corruption bei Concurrent Access.
*/
interface LockableStorage
{
/**
* Führt Operation mit exklusivem File-Lock aus
*/
public function withExclusiveLock(string $path, callable $operation): mixed;
/**
* Führt Operation mit Shared-Lock (Read-Lock) aus
*/
public function withSharedLock(string $path, callable $operation): mixed;
/**
* Prüft ob Datei gesperrt ist
*/
public function isLocked(string $path): bool;
/**
* Schreibt mit File-Lock
*/
public function putWithLock(string $path, string $content): void;
/**
* Update mit File-Lock
*/
public function updateWithLock(string $path, callable $updateCallback): void;
/**
* Append mit File-Lock
*/
public function appendWithLock(string $path, string $content): void;
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
@@ -13,6 +14,10 @@ final class LoggableStorage implements Storage
{
use StorageTrait;
public PermissionChecker $permissions { get => $this->storage->permissions; }
public \App\Framework\Async\FiberManager $fiberManager { get => $this->storage->fiberManager; }
public function __construct(
private readonly Storage $storage,
private ?DefaultLogger $logger = null
@@ -23,6 +28,7 @@ final class LoggableStorage implements Storage
public function get(string $path): string
{
$this->logger->debug("Lese Datei: {$path}");
return $this->storage->get($path);
}
@@ -36,6 +42,7 @@ final class LoggableStorage implements Storage
{
$exists = $this->storage->exists($path);
$this->logger->debug("Prüfe Existenz: {$path} - " . ($exists ? 'existiert' : 'existiert nicht'));
return $exists;
}
@@ -55,6 +62,7 @@ final class LoggableStorage implements Storage
{
$size = $this->storage->size($path);
$this->logger->debug("Dateigröße: {$path} - {$size} Bytes");
return $size;
}
@@ -62,6 +70,7 @@ final class LoggableStorage implements Storage
{
$lastModified = $this->storage->lastModified($path);
$this->logger->debug("Letzte Änderung: {$path} - " . date('Y-m-d H:i:s', $lastModified));
return $lastModified;
}
@@ -69,6 +78,7 @@ final class LoggableStorage implements Storage
{
$mimeType = $this->storage->getMimeType($path);
$this->logger->debug("MIME-Typ: {$path} - {$mimeType}");
return $mimeType;
}
@@ -76,6 +86,7 @@ final class LoggableStorage implements Storage
{
$isReadable = $this->storage->isReadable($path);
$this->logger->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein'));
return $isReadable;
}
@@ -83,6 +94,7 @@ final class LoggableStorage implements Storage
{
$isWritable = $this->storage->isWritable($path);
$this->logger->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein'));
return $isWritable;
}
@@ -91,6 +103,7 @@ final class LoggableStorage implements Storage
$this->logger->debug("Liste Verzeichnis: {$directory}");
$files = $this->storage->listDirectory($directory);
$this->logger->debug("Gefundene Dateien: " . count($files));
return $files;
}
@@ -99,4 +112,52 @@ final class LoggableStorage implements Storage
$this->logger->debug("Erstelle Verzeichnis: {$path}");
$this->storage->createDirectory($path, $permissions, $recursive);
}
public function file(string $path): File
{
$this->logger->debug("Erstelle File-Objekt: {$path}");
return $this->storage->file($path);
}
public function directory(string $path): Directory
{
$this->logger->debug("Erstelle Directory-Objekt: {$path}");
return $this->storage->directory($path);
}
public function batch(array $operations): array
{
$this->logger->debug("Führe Batch-Operation aus mit " . count($operations) . " Operationen");
$results = $this->storage->batch($operations);
$this->logger->debug("Batch-Operation abgeschlossen");
return $results;
}
public function getMultiple(array $paths): array
{
$this->logger->debug("Lese " . count($paths) . " Dateien parallel");
$results = $this->storage->getMultiple($paths);
$this->logger->debug("Parallel-Lesen abgeschlossen");
return $results;
}
public function putMultiple(array $files): void
{
$this->logger->debug("Schreibe " . count($files) . " Dateien parallel");
$this->storage->putMultiple($files);
$this->logger->debug("Parallel-Schreiben abgeschlossen");
}
public function getMetadataMultiple(array $paths): array
{
$this->logger->debug("Lade Metadaten für " . count($paths) . " Dateien parallel");
$results = $this->storage->getMetadataMultiple($paths);
$this->logger->debug("Metadaten-Laden abgeschlossen");
return $results;
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Helper-Klasse für Filesystem-Permissions-Checks
*/
final readonly class PermissionChecker
{
public function __construct(
private Storage $storage
) {
}
private function resolvePath(string $path): string
{
// Use reflection to access resolvePath from FileStorage
// or implement basic path resolution logic
if (method_exists($this->storage, 'resolvePath')) {
$reflection = new \ReflectionMethod($this->storage, 'resolvePath');
return $reflection->invoke($this->storage, $path);
}
// Fallback: return path as-is if storage doesn't have resolvePath
return $path;
}
public function isDirectoryWritable(string $path): bool
{
$resolvedPath = $this->resolvePath($path);
if (! is_dir($resolvedPath)) {
return false;
}
return is_writable($resolvedPath);
}
public function canCreateDirectory(string $path): bool
{
$resolvedPath = $this->resolvePath($path);
// Prüfe, ob Parent-Directory existiert und schreibbar ist
$parentDir = dirname($resolvedPath);
if (! is_dir($parentDir)) {
return $this->canCreateDirectory(dirname($path));
}
return is_writable($parentDir);
}
public function canWriteFile(string $path): bool
{
$resolvedPath = $this->resolvePath($path);
// Wenn Datei existiert, prüfe ob sie schreibbar ist
if (file_exists($resolvedPath)) {
return is_writable($resolvedPath);
}
// Wenn Datei nicht existiert, prüfe ob Directory schreibbar ist
$dir = dirname($path);
return $this->isDirectoryWritable($dir) || $this->canCreateDirectory($dir);
}
public function canReadFile(string $path): bool
{
$resolvedPath = $this->resolvePath($path);
return file_exists($resolvedPath) && is_readable($resolvedPath);
}
public function canDeleteFile(string $path): bool
{
$resolvedPath = $this->resolvePath($path);
if (! file_exists($resolvedPath)) {
return false;
}
// Datei muss schreibbar sein UND Directory muss schreibbar sein
$dir = dirname($resolvedPath);
return is_writable($resolvedPath) && is_writable($dir);
}
public function getPermissionString(string $path): string
{
$resolvedPath = $this->resolvePath($path);
if (! file_exists($resolvedPath)) {
return 'not found';
}
$perms = fileperms($resolvedPath);
$info = '';
// Type
if (($perms & 0xC000) == 0xC000) {
$info = 's'; // Socket
} elseif (($perms & 0xA000) == 0xA000) {
$info = 'l'; // Symbolic Link
} elseif (($perms & 0x8000) == 0x8000) {
$info = 'f'; // Regular file
} elseif (($perms & 0x6000) == 0x6000) {
$info = 'b'; // Block special
} elseif (($perms & 0x4000) == 0x4000) {
$info = 'd'; // Directory
} elseif (($perms & 0x2000) == 0x2000) {
$info = 'c'; // Character special
} elseif (($perms & 0x1000) == 0x1000) {
$info = 'p'; // FIFO pipe
} else {
$info = 'u'; // Unknown
}
// Owner
$info .= (($perms & 0x0100) ? 'r' : '-');
$info .= (($perms & 0x0080) ? 'w' : '-');
$info .= (($perms & 0x0040) ?
(($perms & 0x0800) ? 's' : 'x') :
(($perms & 0x0800) ? 'S' : '-'));
// Group
$info .= (($perms & 0x0020) ? 'r' : '-');
$info .= (($perms & 0x0010) ? 'w' : '-');
$info .= (($perms & 0x0008) ?
(($perms & 0x0400) ? 's' : 'x') :
(($perms & 0x0400) ? 'S' : '-'));
// World
$info .= (($perms & 0x0004) ? 'r' : '-');
$info .= (($perms & 0x0002) ? 'w' : '-');
$info .= (($perms & 0x0001) ?
(($perms & 0x0200) ? 't' : 'x') :
(($perms & 0x0200) ? 'T' : '-'));
return $info;
}
public function getDiagnosticInfo(string $path): array
{
$resolvedPath = $this->resolvePath($path);
$realPath = realpath($resolvedPath) ?: $resolvedPath;
return [
'path' => $path,
'resolved_path' => $resolvedPath,
'real_path' => $realPath,
'exists' => file_exists($resolvedPath),
'is_file' => is_file($resolvedPath),
'is_dir' => is_dir($resolvedPath),
'is_readable' => is_readable($resolvedPath),
'is_writable' => is_writable($resolvedPath),
'permissions' => $this->getPermissionString($path),
'owner' => file_exists($resolvedPath) ? posix_getpwuid(fileowner($resolvedPath))['name'] ?? 'unknown' : null,
'group' => file_exists($resolvedPath) ? posix_getgrgid(filegroup($resolvedPath))['name'] ?? 'unknown' : null,
'parent_dir' => dirname($resolvedPath),
'parent_writable' => is_dir(dirname($resolvedPath)) ? is_writable(dirname($resolvedPath)) : false,
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Interface für Daten-Serializer
*/
interface Serializer
{
/**
* Serialisiert Daten zu String
*/
public function serialize(mixed $data): string;
/**
* Deserialisiert String zu Daten
*/
public function deserialize(string $data): mixed;
/**
* Gibt den MIME-Type für serialisierte Daten zurück
*/
public function getMimeType(): string;
/**
* Gibt die Standard-Dateiendung zurück
*/
public function getFileExtension(): string;
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Serializers;
use App\Framework\Filesystem\Serializer;
/**
* CSV-Serializer für tabellarische Daten
*/
final readonly class CsvSerializer implements Serializer
{
public function __construct(
private string $delimiter = ',',
private string $enclosure = '"',
private string $escape = '\\'
) {
}
public function serialize(mixed $data): string
{
if (! is_array($data)) {
throw new \InvalidArgumentException('CSV serializer requires array data');
}
if (empty($data)) {
return '';
}
$output = fopen('php://temp', 'r+');
// Wenn es ein assoziatives Array ist, Header schreiben
if (is_array($data[0] ?? null)) {
$headers = array_keys($data[0]);
fputcsv($output, $headers, $this->delimiter, $this->enclosure, $this->escape);
}
foreach ($data as $row) {
if (is_array($row)) {
fputcsv($output, $row, $this->delimiter, $this->enclosure, $this->escape);
} else {
fputcsv($output, [$row], $this->delimiter, $this->enclosure, $this->escape);
}
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
public function deserialize(string $data): mixed
{
if (empty($data)) {
return [];
}
$lines = str_getcsv($data, "\n");
$result = [];
$headers = null;
foreach ($lines as $line) {
if (empty(trim($line))) {
continue;
}
$row = str_getcsv($line, $this->delimiter, $this->enclosure, $this->escape);
if ($headers === null) {
$headers = $row;
continue;
}
if (count($row) === count($headers)) {
$result[] = array_combine($headers, $row);
} else {
$result[] = $row;
}
}
return $result;
}
public function getMimeType(): string
{
return 'text/csv';
}
public function getFileExtension(): string
{
return 'csv';
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Serializers;
use App\Framework\Filesystem\Serializer;
/**
* JSON-Serializer für strukturierte Daten
*/
final readonly class JsonSerializer implements Serializer
{
public function __construct(
private int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
private int $depth = 512
) {
}
public function serialize(mixed $data): string
{
$json = json_encode($data, $this->flags, $this->depth);
if ($json === false) {
throw new \RuntimeException('Failed to encode JSON: ' . json_last_error_msg());
}
return $json;
}
public function deserialize(string $data): mixed
{
if (empty($data)) {
return null;
}
return json_decode($data, true, $this->depth, JSON_THROW_ON_ERROR);
}
public function getMimeType(): string
{
return 'application/json';
}
public function getFileExtension(): string
{
return 'json';
}
/**
* Factory für verschiedene JSON-Modi
*/
public static function compact(): self
{
return new self(JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
public static function pretty(): self
{
return new self(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
public static function minimal(): self
{
return new self(0);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Serializers;
use App\Framework\Filesystem\Serializer;
/**
* PHP-native Serializer für Objekte und komplexe Datenstrukturen
*/
final readonly class PhpSerializer implements Serializer
{
public function __construct(
private array $options = []
) {
}
public function serialize(mixed $data): string
{
return serialize($data);
}
public function deserialize(string $data): mixed
{
if (empty($data)) {
return null;
}
$result = unserialize($data, $this->options);
if ($result === false && $data !== serialize(false)) {
throw new \RuntimeException('Failed to unserialize PHP data');
}
return $result;
}
public function getMimeType(): string
{
return 'application/x-php-serialized';
}
public function getFileExtension(): string
{
return 'ser';
}
/**
* Factory für sichere Deserialisierung (nur bestimmte Klassen erlaubt)
*/
public static function safe(array $allowedClasses = []): self
{
return new self(['allowed_classes' => $allowedClasses]);
}
/**
* Factory für unsichere Deserialisierung (alle Klassen erlaubt)
*/
public static function unsafe(): self
{
return new self(['allowed_classes' => true]);
}
}

View File

@@ -1,9 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Async\FiberManager;
interface Storage
{
/**
* Permissions-Checker für dieses Storage
*/
public PermissionChecker $permissions { get; }
/**
* Fiber-Manager für asynchrone Operationen
*/
public FiberManager $fiberManager { get; }
/**
* Liest den Inhalt einer Datei
*/
@@ -75,4 +89,35 @@ interface Storage
* Erstellt ein Directory-Objekt mit Lazy-Loading
*/
public function directory(string $path): Directory;
/**
* Führt mehrere Operationen parallel aus (explizite Batch-Operation)
*
* @param array<string, callable> $operations
* @return array<string, mixed>
*/
public function batch(array $operations): array;
/**
* Lädt mehrere Dateien parallel
*
* @param array<string> $paths
* @return array<string, string>
*/
public function getMultiple(array $paths): array;
/**
* Speichert mehrere Dateien parallel
*
* @param array<string, string> $files [path => content]
*/
public function putMultiple(array $files): void;
/**
* Lädt Metadata für mehrere Dateien parallel
*
* @param array<string> $paths
* @return array<string, FileMetadata>
*/
public function getMetadataMultiple(array $paths): array;
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Factory für Storage-Instanzen
*
* Ermöglicht flexible Storage-Erstellung basierend auf Konfiguration
* und macht es einfach, neue Storage-Typen hinzuzufügen.
*/
final class StorageFactory
{
public static function create(array $config): Storage
{
$type = $config['type'] ?? 'file';
return match ($type) {
'file' => new FileStorage($config['path'] ?? '/'),
'memory' => new MemoryStorage(),
// 'redis' => new RedisStorage($config),
// 's3' => new S3Storage($config),
default => throw new \InvalidArgumentException("Unknown storage type: {$type}")
};
}
/**
* @return array<string>
*/
public static function getSupportedTypes(): array
{
return ['file', 'memory'];
}
public static function isTypeSupported(string $type): bool
{
return in_array($type, self::getSupportedTypes(), true);
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Interface für Stream-basierte Storage-Operationen
*
* Ermöglicht das Arbeiten mit großen Dateien ohne komplettes Laden in den Speicher.
* Ideal für große Analytics-Dateien, Logs und Media-Files.
*/
interface StreamableStorage
{
/**
* Öffnet einen Read-Stream für eine Datei
*
* @return resource
*/
public function readStream(string $path);
/**
* Öffnet einen Write-Stream für eine Datei
*
* @return resource
*/
public function writeStream(string $path);
/**
* Öffnet einen Append-Stream für eine Datei
*
* @return resource
*/
public function appendStream(string $path);
/**
* Schreibt von einem Stream in eine Datei
*
* @param resource $stream
*/
public function putStream(string $path, $stream): void;
/**
* Kopiert eine Datei Stream-basiert (für große Dateien)
*/
public function copyStream(string $source, string $destination): void;
/**
* Liest Datei Zeile für Zeile (Generator für Memory-Effizienz)
*
* @return \Generator<string>
*/
public function readLines(string $path): \Generator;
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Traits;
use App\Framework\Filesystem\Exceptions\FileWriteException;
/**
* Trait für Append Storage-Operationen
*
* Implementiert Append-Funktionalität für Logs,
* Analytics und Stream-artige Daten.
*/
trait AppendableStorageTrait
{
public function append(string $path, string $content): void
{
$resolvedPath = $this->resolvePath($path);
$dir = dirname($resolvedPath);
if (! is_dir($dir)) {
$this->createDirectory($path);
}
if (@file_put_contents($resolvedPath, $content, FILE_APPEND | LOCK_EX) === false) {
throw new FileWriteException($path);
}
}
public function appendLine(string $path, string $line): void
{
$this->append($path, $line . PHP_EOL);
}
public function appendJson(string $path, array $data): void
{
$this->appendLine($path, json_encode($data));
}
public function appendCsv(string $path, array $row): void
{
$resolvedPath = $this->resolvePath($path);
$handle = @fopen($resolvedPath, 'a');
if ($handle === false) {
throw new FileWriteException($path);
}
if (@fputcsv($handle, $row) === false) {
@fclose($handle);
throw new FileWriteException($path);
}
@fclose($handle);
}
public function appendWithTimestamp(string $path, string $content): void
{
$timestampedContent = '[' . date('Y-m-d H:i:s') . '] ' . $content;
$this->appendLine($path, $timestampedContent);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Traits;
use App\Framework\Filesystem\Exceptions\FileWriteException;
/**
* Trait für Atomic Storage-Operationen
*
* Implementiert atomare Schreiboperationen durch temporäre Dateien
* und rename()-Operation für Data Consistency.
*/
trait AtomicStorageTrait
{
public function putAtomic(string $path, string $content): void
{
$tempPath = $path . '.tmp.' . uniqid();
$this->put($tempPath, $content);
$resolvedPath = $this->resolvePath($path);
$resolvedTempPath = $this->resolvePath($tempPath);
if (! @rename($resolvedTempPath, $resolvedPath)) {
@unlink($resolvedTempPath);
throw new FileWriteException($path);
}
}
public function updateAtomic(string $path, callable $updateCallback): void
{
$originalContent = $this->exists($path) ? $this->get($path) : '';
$newContent = $updateCallback($originalContent);
$this->putAtomic($path, $newContent);
}
public function updateJsonAtomic(string $path, callable $updateCallback): void
{
$originalData = $this->exists($path) ? json_decode($this->get($path), true) ?? [] : [];
$newData = $updateCallback($originalData);
$this->putAtomic($path, json_encode($newData, JSON_PRETTY_PRINT));
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Traits;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FileReadException;
use App\Framework\Filesystem\Exceptions\FileWriteException;
/**
* Trait für Compression Storage-Operationen
*
* Implementiert automatische Komprimierung für große Dateien,
* ideal für Analytics-Daten, Logs und Archive.
*/
trait CompressibleStorageTrait
{
public function putCompressed(string $path, string $content, string $algorithm = 'gzip'): void
{
$compressedContent = match ($algorithm) {
'gzip' => gzencode($content),
'deflate' => gzdeflate($content),
'bzip2' => bzcompress($content),
default => throw new \InvalidArgumentException("Unsupported compression algorithm: {$algorithm}")
};
if ($compressedContent === false) {
throw new FileWriteException($path);
}
$this->put($path . '.' . $algorithm, $compressedContent);
}
public function getCompressed(string $path): string
{
// Automatische Erkennung der Komprimierung basierend auf Dateiendung
$pathInfo = pathinfo($path);
$algorithm = $pathInfo['extension'] ?? '';
if (! in_array($algorithm, ['gzip', 'gz', 'deflate', 'bzip2', 'bz2'])) {
throw new \InvalidArgumentException("Cannot detect compression algorithm from path: {$path}");
}
if (! $this->exists($path)) {
throw new FileNotFoundException($path);
}
$compressedContent = $this->get($path);
$content = match ($algorithm) {
'gzip', 'gz' => gzdecode($compressedContent),
'deflate' => gzinflate($compressedContent),
'bzip2', 'bz2' => bzdecompress($compressedContent),
default => throw new \InvalidArgumentException("Unsupported compression algorithm: {$algorithm}")
};
if ($content === false) {
throw new FileReadException($path);
}
return $content;
}
public function compress(string $path, string $algorithm = 'gzip'): void
{
if (! $this->exists($path)) {
throw new FileNotFoundException($path);
}
$content = $this->get($path);
$compressedPath = $path . '.' . $algorithm;
$this->putCompressed($path, $content, $algorithm);
$this->delete($path);
// Komprimierte Datei zurück zum ursprünglichen Pfad verschieben
$this->copy($compressedPath, $path);
$this->delete($compressedPath);
}
public function decompress(string $path): void
{
$content = $this->getCompressed($path);
// Entferne Komprimierungs-Extension
$pathInfo = pathinfo($path);
$algorithm = $pathInfo['extension'] ?? '';
$decompressedPath = $pathInfo['dirname'] . '/' . $pathInfo['filename'];
$this->put($decompressedPath, $content);
$this->delete($path);
}
public function isCompressed(string $path): bool
{
if (! $this->exists($path)) {
return false;
}
$pathInfo = pathinfo($path);
$extension = $pathInfo['extension'] ?? '';
return in_array($extension, ['gzip', 'gz', 'deflate', 'bzip2', 'bz2']);
}
public function getAvailableAlgorithms(): array
{
$algorithms = [];
if (function_exists('gzencode')) {
$algorithms[] = 'gzip';
}
if (function_exists('gzdeflate')) {
$algorithms[] = 'deflate';
}
if (function_exists('bzcompress')) {
$algorithms[] = 'bzip2';
}
return $algorithms;
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Traits;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FileReadException;
use App\Framework\Filesystem\Exceptions\FileWriteException;
/**
* Trait für File-Locking Storage-Operationen
*
* Implementiert File-Locking für kritische Operationen
* und verhindert Data Corruption bei Concurrent Access.
*/
trait LockableStorageTrait
{
public function withExclusiveLock(string $path, callable $operation): mixed
{
$resolvedPath = $this->resolvePath($path);
$handle = @fopen($resolvedPath, 'c+');
if ($handle === false) {
throw new FileWriteException($path);
}
if (! @flock($handle, LOCK_EX)) {
@fclose($handle);
throw new FileWriteException($path);
}
try {
return $operation($handle);
} finally {
@flock($handle, LOCK_UN);
@fclose($handle);
}
}
public function withSharedLock(string $path, callable $operation): mixed
{
if (! $this->exists($path)) {
throw new FileNotFoundException($path);
}
$resolvedPath = $this->resolvePath($path);
$handle = @fopen($resolvedPath, 'r');
if ($handle === false) {
throw new FileReadException($path);
}
if (! @flock($handle, LOCK_SH)) {
@fclose($handle);
throw new FileReadException($path);
}
try {
return $operation($handle);
} finally {
@flock($handle, LOCK_UN);
@fclose($handle);
}
}
public function isLocked(string $path): bool
{
if (! $this->exists($path)) {
return false;
}
$resolvedPath = $this->resolvePath($path);
$handle = @fopen($resolvedPath, 'r');
if ($handle === false) {
return false;
}
$isLocked = ! @flock($handle, LOCK_EX | LOCK_NB);
if (! $isLocked) {
@flock($handle, LOCK_UN);
}
@fclose($handle);
return $isLocked;
}
public function putWithLock(string $path, string $content): void
{
$this->withExclusiveLock($path, function ($handle) use ($content) {
if (@ftruncate($handle, 0) === false || @rewind($handle) === false) {
throw new FileWriteException('Lock operation failed');
}
if (@fwrite($handle, $content) === false) {
throw new FileWriteException('Lock operation failed');
}
});
}
public function updateWithLock(string $path, callable $updateCallback): void
{
$this->withExclusiveLock($path, function ($handle) use ($updateCallback) {
$originalContent = '';
if (@rewind($handle) !== false) {
$originalContent = @stream_get_contents($handle) ?: '';
}
$newContent = $updateCallback($originalContent);
if (@ftruncate($handle, 0) === false || @rewind($handle) === false) {
throw new FileWriteException('Lock operation failed');
}
if (@fwrite($handle, $newContent) === false) {
throw new FileWriteException('Lock operation failed');
}
});
}
public function appendWithLock(string $path, string $content): void
{
$resolvedPath = $this->resolvePath($path);
$dir = dirname($resolvedPath);
if (! is_dir($dir)) {
$this->createDirectory($path);
}
$handle = @fopen($resolvedPath, 'a');
if ($handle === false) {
throw new FileWriteException($path);
}
if (! @flock($handle, LOCK_EX)) {
@fclose($handle);
throw new FileWriteException($path);
}
try {
if (@fwrite($handle, $content) === false) {
throw new FileWriteException($path);
}
} finally {
@flock($handle, LOCK_UN);
@fclose($handle);
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\Traits;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FileReadException;
use App\Framework\Filesystem\Exceptions\FileWriteException;
/**
* Trait für Stream-basierte Storage-Operationen
*
* Implementiert Stream-Funktionalität für große Dateien
* ohne komplettes Laden in den Speicher.
*/
trait StreamableStorageTrait
{
public function readStream(string $path)
{
if (! $this->exists($path)) {
throw new FileNotFoundException($path);
}
$resolvedPath = $this->resolvePath($path);
$stream = @fopen($resolvedPath, 'r');
if ($stream === false) {
throw new FileReadException($path);
}
return $stream;
}
public function writeStream(string $path)
{
$resolvedPath = $this->resolvePath($path);
$dir = dirname($resolvedPath);
if (! is_dir($dir)) {
$this->createDirectory($path);
}
$stream = @fopen($resolvedPath, 'w');
if ($stream === false) {
throw new FileWriteException($path);
}
return $stream;
}
public function appendStream(string $path)
{
$resolvedPath = $this->resolvePath($path);
$dir = dirname($resolvedPath);
if (! is_dir($dir)) {
$this->createDirectory($path);
}
$stream = @fopen($resolvedPath, 'a');
if ($stream === false) {
throw new FileWriteException($path);
}
return $stream;
}
public function putStream(string $path, $stream): void
{
$writeStream = $this->writeStream($path);
if (@stream_copy_to_stream($stream, $writeStream) === false) {
@fclose($writeStream);
throw new FileWriteException($path);
}
@fclose($writeStream);
}
public function copyStream(string $source, string $destination): void
{
$sourceStream = $this->readStream($source);
$this->putStream($destination, $sourceStream);
@fclose($sourceStream);
}
public function readLines(string $path): \Generator
{
$stream = $this->readStream($path);
try {
while (($line = @fgets($stream)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
@fclose($stream);
}
}
}

View File

@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\ValueObjects;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Filesystem\File;
use ArrayIterator;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
/**
* Type-safe collection of File objects
* @implements IteratorAggregate<int, File>
*/
final readonly class FileCollection implements IteratorAggregate, Countable
{
/** @var File[] */
private array $files;
/**
* @param File[] $files
*/
private function __construct(array $files)
{
// Validate all items are File objects
foreach ($files as $file) {
if (! $file instanceof File) {
throw new InvalidArgumentException('All items must be File objects');
}
}
$this->files = array_values($files); // Re-index array
}
/**
* @param File[] $files
*/
public static function fromFiles(array $files): self
{
return new self($files);
}
public static function empty(): self
{
return new self([]);
}
/**
* Create from SplFileInfo objects
* @param \SplFileInfo[] $splFiles
*/
public static function fromSplFiles(array $splFiles): self
{
$files = array_map(fn (\SplFileInfo $splFile) => File::fromSplFileInfo($splFile), $splFiles);
return new self($files);
}
/**
* Get iterator for foreach loops
* @return ArrayIterator<int, File>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->files);
}
public function count(): int
{
return count($this->files);
}
public function isEmpty(): bool
{
return empty($this->files);
}
/**
* Get all files as array
* @return File[]
*/
public function toArray(): array
{
return $this->files;
}
/**
* Filter files by pattern
*/
public function filterByPattern(FilePattern $pattern): self
{
$filtered = array_filter(
$this->files,
fn (File $file) => $pattern->matches($file->getBasename())
);
return new self($filtered);
}
/**
* Filter files by extension
*/
public function filterByExtension(string $extension): self
{
$extension = ltrim($extension, '.');
$filtered = array_filter(
$this->files,
fn (File $file) => $file->getExtension() === $extension
);
return new self($filtered);
}
/**
* Get files modified after timestamp
*/
public function modifiedAfter(Timestamp $timestamp): self
{
$filtered = array_filter(
$this->files,
fn (File $file) => $file->lastModified > $timestamp->getValue()
);
return new self($filtered);
}
/**
* Get files modified before timestamp
*/
public function modifiedBefore(Timestamp $timestamp): self
{
$filtered = array_filter(
$this->files,
fn (File $file) => $file->lastModified < $timestamp->getValue()
);
return new self($filtered);
}
/**
* Sort files by modification time (newest first)
*/
public function sortByModificationTime(bool $descending = true): self
{
$files = $this->files;
usort($files, function (File $a, File $b) use ($descending) {
$comparison = $a->lastModified <=> $b->lastModified;
return $descending ? -$comparison : $comparison;
});
return new self($files);
}
/**
* Sort files by name
*/
public function sortByName(bool $ascending = true): self
{
$files = $this->files;
usort($files, function (File $a, File $b) use ($ascending) {
$comparison = strcmp($a->getBasename(), $b->getBasename());
return $ascending ? $comparison : -$comparison;
});
return new self($files);
}
/**
* Get the most recently modified file
*/
public function getLatestModified(): ?File
{
if (empty($this->files)) {
return null;
}
return $this->sortByModificationTime(true)->files[0];
}
/**
* Get the oldest modified file
*/
public function getOldestModified(): ?File
{
if (empty($this->files)) {
return null;
}
return $this->sortByModificationTime(false)->files[0];
}
/**
* Get file at specific index
*/
public function get(int $index): ?File
{
return $this->files[$index] ?? null;
}
/**
* Get first file
*/
public function first(): ?File
{
return $this->files[0] ?? null;
}
/**
* Get last file
*/
public function last(): ?File
{
return empty($this->files) ? null : $this->files[count($this->files) - 1];
}
/**
* Merge with another collection
*/
public function merge(self $other): self
{
return new self(array_merge($this->files, $other->files));
}
/**
* Get unique files (based on path)
*/
public function unique(): self
{
$unique = [];
$seen = [];
foreach ($this->files as $file) {
$path = $file->getPath()->toString();
if (! isset($seen[$path])) {
$unique[] = $file;
$seen[$path] = true;
}
}
return new self($unique);
}
/**
* Group files by extension
* @return array<string, self>
*/
public function groupByExtension(): array
{
$groups = [];
foreach ($this->files as $file) {
$extension = $file->getExtension() ?: 'no-extension';
if (! isset($groups[$extension])) {
$groups[$extension] = [];
}
$groups[$extension][] = $file;
}
return array_map(fn (array $files) => new self($files), $groups);
}
/**
* Get total size of all files
*/
public function getTotalSize(): int
{
return array_sum(array_map(fn (File $file) => $file->getSize(), $this->files));
}
/**
* Check if collection contains a specific file
*/
public function contains(File $file): bool
{
foreach ($this->files as $existingFile) {
if ($existingFile->getPath()->equals($file->getPath())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\ValueObjects;
use InvalidArgumentException;
/**
* Value object for file patterns used in discovery and filesystem operations
* Supports glob patterns like "*.php", "*.js", "test-*.txt", and multi-extension patterns
*/
final readonly class FilePattern
{
private function __construct(
public string $pattern
) {
$this->validate($pattern);
}
public static function create(string $pattern): self
{
return new self($pattern);
}
public static function php(): self
{
return new self('*.php');
}
public static function javascript(): self
{
return new self('*.js');
}
public static function css(): self
{
return new self('*.css');
}
public static function all(): self
{
return new self('*');
}
public static function fromExtension(string $extension): self
{
// Remove leading dot if present
$extension = ltrim($extension, '.');
return new self("*.{$extension}");
}
public static function forExtension(string $extension): self
{
return self::fromExtension($extension);
}
public static function forExtensions(array $extensions): self
{
if (empty($extensions)) {
throw new InvalidArgumentException('Extensions array cannot be empty');
}
if (count($extensions) === 1) {
return self::forExtension($extensions[0]);
}
$cleanExtensions = array_map(fn ($ext) => '*.' . ltrim($ext, '.'), $extensions);
return new self('{' . implode(',', $cleanExtensions) . '}');
}
public function toString(): string
{
return $this->pattern;
}
public function __toString(): string
{
return $this->pattern;
}
/**
* Konvertiert das Glob-Muster in einen regulären Ausdruck
*/
public function toRegex(): string
{
$regex = preg_quote($this->pattern, '/');
// Ersetze Glob-Platzhalter durch Regex-Äquivalente
$regex = str_replace(
['\*', '\?', '\[', '\]'],
['.*', '.', '[', ']'],
$regex
);
return '/^' . $regex . '$/i';
}
/**
* Prüft ob ein Dateiname dem Muster entspricht
*/
public function matches(string $filename): bool
{
return fnmatch($this->pattern, $filename);
}
public function isGlobPattern(): bool
{
return str_contains($this->pattern, '*') ||
str_contains($this->pattern, '?') ||
str_contains($this->pattern, '[');
}
public function getExtensions(): array
{
// Handle simple extension patterns like "*.php"
if (preg_match('/^\*\.(\w+)$/', $this->pattern, $matches)) {
return [$matches[1]];
}
// Handle multiple extension patterns like "{*.php,*.js}"
if (preg_match('/^\{(.+)\}$/', $this->pattern, $matches)) {
$parts = explode(',', $matches[1]);
$extensions = [];
foreach ($parts as $part) {
if (preg_match('/^\*\.(\w+)$/', trim($part), $extMatches)) {
$extensions[] = $extMatches[1];
}
}
return $extensions;
}
return [];
}
/**
* Kombiniert mehrere Muster zu einem OR-Muster
* @param FilePattern[] $patterns
*/
public static function anyOf(array $patterns): self
{
if (empty($patterns)) {
throw new InvalidArgumentException('At least one pattern is required');
}
if (count($patterns) === 1) {
return $patterns[0];
}
// Für Glob-Muster verwenden wir {pattern1,pattern2} Syntax
$patternStrings = array_map(fn (self $p) => $p->pattern, $patterns);
return new self('{' . implode(',', $patternStrings) . '}');
}
private function validate(string $pattern): void
{
if (empty(trim($pattern))) {
throw new InvalidArgumentException('File pattern cannot be empty');
}
// Prüfe auf ungültige Zeichen für Dateisysteme
if (preg_match('/[<>:"|]/', $pattern)) {
throw new InvalidArgumentException('File pattern contains invalid characters');
}
}
public function equals(self $other): bool
{
return $this->pattern === $other->pattern;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Performance\MemoryMonitor;
/**
* Represents memory usage statistics for the scanner
*/
final readonly class ScannerMemoryUsage
{
public function __construct(
public Byte $current,
public Byte $peak,
public Byte $limit,
public Percentage $usage
) {
}
public static function fromMemoryMonitor(MemoryMonitor $monitor): self
{
return new self(
current: $monitor->getCurrentMemory(),
peak: $monitor->getPeakMemory(),
limit: $monitor->getMemoryLimit(),
usage: $monitor->getMemoryUsagePercentage()
);
}
public static function current(): self
{
$monitor = new MemoryMonitor();
return self::fromMemoryMonitor($monitor);
}
/**
* Check if memory usage is critical (>90%)
*/
public function isCritical(): bool
{
return $this->usage->greaterThan(Percentage::from(90.0));
}
/**
* Check if memory usage is high (>80%)
*/
public function isHigh(): bool
{
return $this->usage->greaterThan(Percentage::from(80.0));
}
/**
* Get available memory
*/
public function getAvailable(): Byte
{
return $this->limit->subtract($this->current);
}
/**
* Check if we have enough memory for an operation
*/
public function hasEnoughMemoryFor(Byte $required): bool
{
return $this->getAvailable()->greaterThan($required);
}
public function toArray(): array
{
return [
'current' => $this->current->toBytes(),
'current_human' => $this->current->toHumanReadable(),
'peak' => $this->peak->toBytes(),
'peak_human' => $this->peak->toHumanReadable(),
'limit' => $this->limit->toBytes(),
'limit_human' => $this->limit->toHumanReadable(),
'usage_percentage' => $this->usage->getValue(),
'is_critical' => $this->isCritical(),
'is_high' => $this->isHigh(),
'available' => $this->getAvailable()->toBytes(),
'available_human' => $this->getAvailable()->toHumanReadable(),
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem\ValueObjects;
/**
* Detailed metrics about scanner operations
*/
final readonly class ScannerMetrics
{
public function __construct(
public int $totalFilesScanned,
public int $totalDirectoriesScanned,
public int $skippedFiles,
public int $permissionErrors,
public int $recoverableErrors,
public int $fatalErrors,
public int $retryAttempts,
public ScannerMemoryUsage $memoryStats
) {
}
public static function empty(): self
{
return new self(
totalFilesScanned: 0,
totalDirectoriesScanned: 0,
skippedFiles: 0,
permissionErrors: 0,
recoverableErrors: 0,
fatalErrors: 0,
retryAttempts: 0,
memoryStats: ScannerMemoryUsage::current()
);
}
public function getTotalErrors(): int
{
return $this->permissionErrors + $this->recoverableErrors + $this->fatalErrors;
}
public function getErrorRate(): float
{
if ($this->totalFilesScanned === 0) {
return 0.0;
}
return $this->getTotalErrors() / $this->totalFilesScanned;
}
public function hasErrors(): bool
{
return $this->getTotalErrors() > 0;
}
public function hasFatalErrors(): bool
{
return $this->fatalErrors > 0;
}
public function toArray(): array
{
return [
'total_files_scanned' => $this->totalFilesScanned,
'total_directories_scanned' => $this->totalDirectoriesScanned,
'skipped_files' => $this->skippedFiles,
'permission_errors' => $this->permissionErrors,
'recoverable_errors' => $this->recoverableErrors,
'fatal_errors' => $this->fatalErrors,
'retry_attempts' => $this->retryAttempts,
'memory_stats' => $this->memoryStats->toArray(),
'total_errors' => $this->getTotalErrors(),
'error_rate' => $this->getErrorRate(),
'has_errors' => $this->hasErrors(),
'has_fatal_errors' => $this->hasFatalErrors(),
];
}
}