docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use DateTimeImmutable;
/**
* Represents a file system change event
*/
final readonly class FileChangeEvent
{
public function __construct(
private string $path,
private FileChangeType $type,
private DateTimeImmutable $timestamp
) {
}
public static function create(string $path, FileChangeType $type): self
{
return new self($path, $type, new DateTimeImmutable());
}
public function getPath(): string
{
return $this->path;
}
public function getType(): FileChangeType
{
return $this->type;
}
public function getTimestamp(): DateTimeImmutable
{
return $this->timestamp;
}
public function isType(FileChangeType $type): bool
{
return $this->type === $type;
}
public function isModification(): bool
{
return $this->type === FileChangeType::MODIFIED;
}
public function isCreation(): bool
{
return $this->type === FileChangeType::CREATED;
}
public function isDeletion(): bool
{
return $this->type === FileChangeType::DELETED;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
/**
* Types of file system changes
*/
enum FileChangeType: string
{
case CREATED = 'created';
case MODIFIED = 'modified';
case DELETED = 'deleted';
}

View File

@@ -20,8 +20,11 @@ final readonly class FilePath
];
public readonly string $normalized;
public readonly string $directory;
public readonly string $filename;
public readonly string $extension;
public function __construct(string $path)

View File

@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\SystemTimer;
use App\Framework\DateTime\Timer;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* File system watcher for detecting file changes
*/
final class FileWatcher
{
private array $fileHashes = [];
private array $fileMtimes = [];
private bool $running = false;
private FilePath $basePath;
private Timer $timer;
public function __construct(?FilePath $basePath = null, ?Timer $timer = null)
{
$this->basePath = $basePath ?? FilePath::fromString(dirname(__DIR__, 3));
$this->timer = $timer ?? new SystemTimer();
}
/**
* Watch for file changes
*/
public function watch(
array $patterns,
array $ignore,
callable $callback,
?Duration $pollInterval = null
): void {
$this->running = true;
$pollInterval ??= Duration::fromSeconds(1);
// Initial scan
$this->scanFiles($patterns, $ignore);
// Polling loop
while ($this->running) {
$changes = $this->detectChanges($patterns, $ignore);
foreach ($changes as $change) {
$callback($change);
}
// Sleep for poll interval
$this->timer->sleep($pollInterval);
}
}
/**
* Watch for changes once (non-blocking)
*/
public function watchOnce(array $patterns, array $ignore): array
{
return $this->detectChanges($patterns, $ignore);
}
/**
* Stop watching
*/
public function stop(): void
{
$this->running = false;
}
/**
* Scan files and build initial hash map
*/
private function scanFiles(array $patterns, array $ignore): void
{
$files = $this->getMatchingFiles($patterns, $ignore);
foreach ($files as $filePath) {
if ($filePath->exists() && $filePath->isFile()) {
$path = $filePath->toString();
$this->fileHashes[$path] = md5_file($path);
$this->fileMtimes[$path] = filemtime($path);
}
}
}
/**
* Detect changes in watched files
* @return FileChangeEvent[]
*/
private function detectChanges(array $patterns, array $ignore): array
{
$changes = [];
$currentFiles = $this->getMatchingFiles($patterns, $ignore);
// Check for new and modified files
foreach ($currentFiles as $filePath) {
if (! $filePath->exists() || ! $filePath->isFile()) {
continue;
}
$path = $filePath->toString();
$currentHash = md5_file($path);
$currentMtime = filemtime($path);
if (! isset($this->fileHashes[$path])) {
// New file
$changes[] = FileChangeEvent::create($path, FileChangeType::CREATED);
$this->fileHashes[$path] = $currentHash;
$this->fileMtimes[$path] = $currentMtime;
} elseif ($this->fileHashes[$path] !== $currentHash || $this->fileMtimes[$path] !== $currentMtime) {
// Modified file
$changes[] = FileChangeEvent::create($path, FileChangeType::MODIFIED);
$this->fileHashes[$path] = $currentHash;
$this->fileMtimes[$path] = $currentMtime;
}
}
// Check for deleted files
$currentPaths = array_map(fn (FilePath $fp) => $fp->toString(), $currentFiles);
foreach ($this->fileHashes as $path => $hash) {
if (! in_array($path, $currentPaths, true)) {
$changes[] = FileChangeEvent::create($path, FileChangeType::DELETED);
unset($this->fileHashes[$path]);
unset($this->fileMtimes[$path]);
}
}
return $changes;
}
/**
* Get files matching patterns
* @return FilePath[]
*/
private function getMatchingFiles(array $patterns, array $ignore): array
{
$files = [];
foreach ($patterns as $pattern) {
$files = array_merge($files, $this->globRecursive($pattern, $ignore));
}
// Remove duplicates based on path string
$uniquePaths = [];
foreach ($files as $filePath) {
$pathStr = $filePath->toString();
$uniquePaths[$pathStr] = $filePath;
}
return array_values($uniquePaths);
}
/**
* Recursive glob with ignore patterns
* @return FilePath[]
*/
private function globRecursive(string $pattern, array $ignore): array
{
$files = [];
$pattern = $this->basePath->join($pattern)->toString();
// Handle ** wildcard for recursive search
if (str_contains($pattern, '**')) {
$basePath = substr($pattern, 0, strpos($pattern, '**'));
$filePattern = substr($pattern, strpos($pattern, '**') + 3);
$basePathObj = FilePath::fromString($basePath);
if (! $basePathObj->isDirectory()) {
return [];
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($basePath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
$filePath = FilePath::fromString($file->getPathname());
// Check ignore patterns
if ($this->shouldIgnore($filePath, $ignore)) {
continue;
}
// Match file pattern
if ($this->matchesPattern($filePath, $filePattern)) {
$files[] = $filePath;
}
}
} else {
// Simple glob
$matches = glob($pattern);
if ($matches !== false) {
foreach ($matches as $file) {
$filePath = FilePath::fromString($file);
if (! $this->shouldIgnore($filePath, $ignore)) {
$files[] = $filePath;
}
}
}
}
return $files;
}
/**
* Check if file should be ignored
*/
private function shouldIgnore(FilePath $file, array $ignorePatterns): bool
{
$fileStr = $file->toString();
foreach ($ignorePatterns as $pattern) {
$fullPattern = $this->basePath->join($pattern)->toString();
if (str_contains($fullPattern, '**')) {
$regex = str_replace('**', '.*', $fullPattern);
$regex = str_replace('/', '\/', $regex);
if (preg_match('/^' . $regex . '/', $fileStr)) {
return true;
}
} elseif (fnmatch($fullPattern, $fileStr)) {
return true;
}
}
return false;
}
/**
* Check if file matches pattern
*/
private function matchesPattern(FilePath $file, string $pattern): bool
{
// Convert glob pattern to regex
$pattern = str_replace('*', '.*', $pattern);
$pattern = str_replace('/', '\/', $pattern);
return (bool)preg_match('/' . $pattern . '$/', $file->toString());
}
}