refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives

This commit is contained in:
2025-11-04 13:44:27 +01:00
parent 980714f656
commit bfce93ce77
110 changed files with 2828 additions and 774 deletions

View File

@@ -285,7 +285,7 @@ final class PerformanceBasedAnalyticsStorage implements AnalyticsStorage
return;
}
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$filename = $this->dataPath . '/raw_' . date('Y-m-d_H-i-s') . '_' . $generator->generate() . '.' . $this->serializer->getFileExtension();
$content = $this->serializer->serialize($this->rawDataBuffer);

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Audit\ValueObjects;
use App\Framework\DateTime\Clock;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
/**
* Audit Entry ID value object (ULID-based)

View File

@@ -17,6 +17,7 @@ enum EnvKey: string
// Feature Flags
case ENABLE_CONTEXT_AWARE_INITIALIZERS = 'ENABLE_CONTEXT_AWARE_INITIALIZERS';
case MCP_SERVER_MODE = 'MCP_SERVER_MODE';
case LOG_COLOR_OUTPUT = 'LOG_COLOR_OUTPUT';
// Database
case DB_DRIVER = 'DB_DRIVER';

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\ValueObjects\TerminalStream;
/**
* Value Object für CLI SAPI mit allen Standard-Streams.
*
* Zentrale Abstraktion für CLI I/O Operationen.
* Bietet alle drei Streams (stdin, stdout, stderr) als Properties
* und delegiert Terminal-Detection an TerminalDetector.
*/
final readonly class CliSapi
{
/**
* @param TerminalStream $stdin Standard-Input Stream
* @param TerminalStream $stdout Standard-Output Stream
* @param TerminalStream $stderr Standard-Error Stream
* @param bool $isCli Ob aktueller Kontext CLI ist
*/
private function __construct(
public TerminalStream $stdin,
public TerminalStream $stdout,
public TerminalStream $stderr,
private bool $isCli
) {
}
/**
* Erstellt CliSapi automatisch basierend auf SAPI-Detection.
*
* Erkennt automatisch ob CLI oder Web-Kontext.
*/
public static function detect(): self
{
$isCli = PHP_SAPI === 'cli';
return new self(
stdin: TerminalStream::stdin(),
stdout: TerminalStream::stdout(),
stderr: TerminalStream::stderr(),
isCli: $isCli
);
}
/**
* Erstellt CliSapi explizit für CLI-Kontext.
*/
public static function forCli(): self
{
return new self(
stdin: TerminalStream::stdin(),
stdout: TerminalStream::stdout(),
stderr: TerminalStream::stderr(),
isCli: true
);
}
/**
* Erstellt CliSapi explizit für Web-Kontext.
*/
public static function forWeb(): self
{
return new self(
stdin: TerminalStream::stdin(),
stdout: TerminalStream::stdout(),
stderr: TerminalStream::stderr(),
isCli: false
);
}
/**
* Prüft ob aktueller Kontext CLI ist.
*/
public function isCli(): bool
{
return $this->isCli;
}
/**
* Prüft ob aktueller Kontext Web ist.
*/
public function isWeb(): bool
{
return !$this->isCli;
}
/**
* Prüft ob ein Stream ein Terminal ist.
*
* Delegiert an TerminalDetector.
*
* @param TerminalStream|null $stream Terminal Stream (oder null für stdout)
* @return bool True wenn Stream ein Terminal ist
*/
public function isTerminal(?TerminalStream $stream = null): bool
{
return TerminalDetector::isTerminal($stream ?? $this->stdout);
}
/**
* Prüft ob ein Stream ein Terminal ist und Farben unterstützt.
*
* Delegiert an TerminalDetector.
*
* @param TerminalStream|null $stream Terminal Stream (oder null für stdout)
* @return bool True wenn Terminal vorhanden und Farben unterstützt werden
*/
public function supportsColors(?TerminalStream $stream = null): bool
{
return TerminalDetector::supportsColors($stream ?? $this->stdout);
}
/**
* Prüft ob STDOUT ein Terminal ist.
*/
public function isStdoutTerminal(): bool
{
return $this->isTerminal($this->stdout);
}
/**
* Prüft ob STDERR ein Terminal ist.
*/
public function isStderrTerminal(): bool
{
return $this->isTerminal($this->stderr);
}
/**
* Prüft ob STDOUT Farben unterstützt.
*/
public function stdoutSupportsColors(): bool
{
return $this->supportsColors($this->stdout);
}
/**
* Prüft ob STDERR Farben unterstützt.
*/
public function stderrSupportsColors(): bool
{
return $this->supportsColors($this->stderr);
}
}

View File

@@ -0,0 +1,548 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\CommandGroupRegistry;
use App\Framework\Console\CommandHelp;
use App\Framework\Console\CommandHelpGenerator;
use App\Framework\Console\CommandHistory;
use App\Framework\Console\CommandList;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ErrorRecovery\CommandSuggestionEngine;
use App\Framework\Console\ExitCode;
use App\Framework\Console\ParameterInspector;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryRegistry;
/**
* Dialog mode orchestrator - simple text-based interactive console
* Similar to AI assistant chat interface
*/
final readonly class ConsoleDialog
{
private bool $readlineAvailable = false;
private CommandSuggestionEngine $suggestionEngine;
private CommandHelpGenerator $helpGenerator;
public function __construct(
private ConsoleOutputInterface $output,
private DiscoveryRegistry $discoveryRegistry,
private CommandHistory $commandHistory,
private CommandGroupRegistry $groupRegistry,
private DialogCommandExecutor $commandExecutor,
private CommandList $commandList,
private Container $container,
private string $prompt = 'console> '
) {
$this->readlineAvailable = extension_loaded('readline');
$this->suggestionEngine = new CommandSuggestionEngine($commandList);
$this->helpGenerator = new CommandHelpGenerator(new ParameterInspector());
if ($this->readlineAvailable) {
$this->setupReadline();
}
}
/**
* Run the dialog mode
*/
public function run(): ExitCode
{
try {
$this->showWelcome();
$this->mainLoop();
} catch (\Throwable $e) {
$this->output->writeError('Fatal error: ' . $e->getMessage());
if ($this->output->isTerminal()) {
$this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY);
}
return ExitCode::GENERAL_ERROR;
} finally {
$this->cleanup();
}
return ExitCode::SUCCESS;
}
/**
* Setup readline for history and completion
*/
private function setupReadline(): void
{
// Load history from CommandHistory
$history = $this->commandHistory->getRecentHistory(100);
foreach ($history as $entry) {
if (isset($entry['command'])) {
readline_add_history($entry['command']);
}
}
// Set completion function
readline_completion_function([$this, 'completeCommand']);
// Enable completion
readline_info('completion_append_character', ' ');
readline_info('completion_suppress_append', false);
}
/**
* Readline completion callback
*
* @param string $text
* @param int $start
* @param int $end
* @return array<int, string>
*/
public function completeCommand(string $text, int $start, int $end): array
{
$line = readline_info('line_buffer');
$words = explode(' ', $line);
$currentWord = $words[$start] ?? '';
// If we're at the first word (command name), suggest commands
if ($start === 0) {
return $this->getCommandSuggestions($currentWord);
}
// For subsequent words, could implement argument completion
// For now, return empty
return [];
}
/**
* Get command suggestions for autocomplete
*
* @return array<int, string>
*/
private function getCommandSuggestions(string $partial): array
{
$suggestions = [];
// Get suggestions from history first
$historySuggestions = $this->commandHistory->getSuggestions($partial, 10);
foreach ($historySuggestions as $suggestion) {
$suggestions[] = $suggestion['command'];
}
// Get suggestions from all commands
$commands = $this->commandList->getAllCommands();
foreach ($commands as $command) {
if (str_starts_with(strtolower($command->name), strtolower($partial))) {
if (! in_array($command->name, $suggestions)) {
$suggestions[] = $command->name;
}
}
}
return array_unique($suggestions);
}
/**
* Main dialog loop
*/
private function mainLoop(): void
{
$running = true;
while ($running) {
// Handle signals
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
// Read input
$input = $this->readInput();
// Handle empty input
if ($input === null || trim($input) === '') {
continue;
}
$input = trim($input);
// Handle exit commands
if (in_array(strtolower($input), ['exit', 'quit', 'q', ':q'])) {
$this->output->writeLine('Goodbye! 👋', ConsoleColor::CYAN);
break;
}
// Handle help command
if (in_array(strtolower($input), ['help', '?', ':help', 'h'])) {
$this->showHelp();
continue;
}
// Handle help <command> syntax
if (preg_match('/^help\s+(.+)$/i', $input, $matches)) {
$commandName = trim($matches[1]);
$this->showCommandHelp($commandName);
continue;
}
// Handle history command
if (strtolower($input) === 'history' || strtolower($input) === ':history') {
$this->showHistory();
continue;
}
// Handle clear command
if (strtolower($input) === 'clear' || strtolower($input) === ':clear') {
$this->clearScreen();
continue;
}
// Parse command and arguments
$parts = $this->parseInput($input);
$commandName = $parts['command'];
$arguments = $parts['arguments'];
// Check if command exists
if (! $this->commandList->has($commandName)) {
$this->handleCommandNotFound($commandName);
continue;
}
// Execute command
$exitCode = $this->commandExecutor->executeCommand($commandName, $arguments);
// Add to readline history if available
if ($this->readlineAvailable) {
readline_add_history($input);
}
// Show suggestions if command failed
if ($exitCode->value !== 0) {
$this->showContextualHelp($commandName);
}
}
}
/**
* Read input from user
*/
private function readInput(): ?string
{
if ($this->readlineAvailable) {
$input = readline($this->prompt);
if ($input === false) {
return null;
}
return $input;
}
// Fallback to fgets if readline not available
$this->output->write($this->prompt, ConsoleColor::BRIGHT_CYAN);
$input = fgets(STDIN);
if ($input === false) {
return null;
}
return rtrim($input, "\n\r");
}
/**
* Parse input into command and arguments
*
* @return array{command: string, arguments: array<int, string>}
*/
private function parseInput(string $input): array
{
// Handle quoted strings
$parts = [];
$current = '';
$inQuotes = false;
$quoteChar = '';
for ($i = 0; $i < strlen($input); $i++) {
$char = $input[$i];
if (($char === '"' || $char === "'") && ($i === 0 || $input[$i - 1] !== '\\')) {
if ($inQuotes && $char === $quoteChar) {
$inQuotes = false;
$quoteChar = '';
} elseif (! $inQuotes) {
$inQuotes = true;
$quoteChar = $char;
}
} elseif ($char === ' ' && ! $inQuotes) {
if ($current !== '') {
$parts[] = $current;
$current = '';
}
} else {
$current .= $char;
}
}
if ($current !== '') {
$parts[] = $current;
}
if (empty($parts)) {
return ['command' => '', 'arguments' => []];
}
return [
'command' => $parts[0],
'arguments' => array_slice($parts, 1),
];
}
/**
* Handle command not found
*/
private function handleCommandNotFound(string $commandName): void
{
$this->output->writeLine('');
$this->output->writeError("Command '{$commandName}' not found.");
// Get suggestions
$suggestions = $this->suggestionEngine->suggestCommand($commandName);
if ($suggestions->hasSuggestions()) {
$this->output->writeLine('');
$this->output->writeLine('Did you mean one of these?', ConsoleColor::YELLOW);
foreach ($suggestions->suggestions as $suggestion) {
$confidence = round($suggestion->similarity * 100);
$this->output->writeLine(
"{$suggestion->command->name} ({$confidence}% match)",
ConsoleColor::CYAN
);
if ($suggestion->command->description) {
$this->output->writeLine(
" {$suggestion->command->description}",
ConsoleColor::GRAY
);
}
}
}
$this->output->writeLine('');
$this->output->writeLine('Type "help" to see all available commands.', ConsoleColor::GRAY);
$this->output->writeLine('');
}
/**
* Show contextual help for a command
*/
private function showContextualHelp(string $commandName): void
{
if (! $this->commandList->has($commandName)) {
return;
}
$command = $this->commandList->get($commandName);
$this->output->writeLine('');
$this->output->writeLine('💡 Tip: Use "help ' . $commandName . '" for detailed help.', ConsoleColor::CYAN);
if ($command->description) {
$this->output->writeLine("Description: {$command->description}", ConsoleColor::GRAY);
}
$this->output->writeLine('');
}
/**
* Show welcome message
*/
private function showWelcome(): void
{
$this->output->writeLine('');
$this->output->writeLine('🤖 Console Dialog Mode', ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY);
$this->output->writeLine('');
$commandCount = $this->commandList->count();
$this->output->writeLine("Available commands: {$commandCount}", ConsoleColor::GRAY);
if ($this->readlineAvailable) {
$this->output->writeLine('✓ Readline support enabled (Tab completion, ↑/↓ history)', ConsoleColor::GREEN);
} else {
$this->output->writeLine('⚠ Readline not available (install php-readline for better experience)', ConsoleColor::YELLOW);
}
$this->output->writeLine('');
$this->output->writeLine('Type "help" for available commands or "exit" to quit.', ConsoleColor::GRAY);
$this->output->writeLine('');
}
/**
* Show help
*/
private function showHelp(): void
{
$categories = $this->groupRegistry->getOrganizedCommands();
$this->output->writeLine('');
$this->output->writeLine('📚 Available Commands', ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY);
$this->output->writeLine('');
foreach ($categories as $category => $commands) {
$this->output->writeLine("{$category}:", ConsoleColor::BRIGHT_YELLOW);
foreach ($commands as $command) {
$name = $command->name ?? 'unknown';
$description = $command->description ?? 'No description';
$this->output->writeLine(" {$name}", ConsoleColor::WHITE);
$this->output->writeLine(" {$description}", ConsoleColor::GRAY);
}
$this->output->writeLine('');
}
$this->output->writeLine('💡 Tips:', ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(' • Type a command name to execute it', ConsoleColor::GRAY);
$this->output->writeLine(' • Use Tab for command completion', ConsoleColor::GRAY);
$this->output->writeLine(' • Use ↑/↓ to navigate command history', ConsoleColor::GRAY);
$this->output->writeLine(' • Type "help <command>" for detailed help', ConsoleColor::GRAY);
$this->output->writeLine(' • Type "exit" or "quit" to leave', ConsoleColor::GRAY);
$this->output->writeLine('');
}
/**
* Show detailed help for a specific command
*/
private function showCommandHelp(string $commandName): void
{
$this->output->writeLine('');
$this->output->writeLine("📖 Command Help: {$commandName}", ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY);
$this->output->writeLine('');
if (! $this->commandList->has($commandName)) {
$this->output->writeError("Command '{$commandName}' not found.");
$suggestions = $this->suggestionEngine->suggestCommand($commandName);
if ($suggestions->hasSuggestions()) {
$this->output->writeLine('');
$this->output->writeLine('Did you mean one of these?', ConsoleColor::YELLOW);
foreach ($suggestions->suggestions as $suggestion) {
$this->output->writeLine("{$suggestion->command->name}", ConsoleColor::CYAN);
}
}
$this->output->writeLine('');
return;
}
try {
$command = $this->commandList->get($commandName);
$discoveredAttributes = $this->discoveryRegistry->attributes()->get(\App\Framework\Console\ConsoleCommand::class);
// Find the command object
$commandObject = null;
foreach ($discoveredAttributes as $discovered) {
$attribute = $discovered->createAttributeInstance();
if ($attribute && $attribute->name === $commandName) {
$className = $discovered->className->getFullyQualified();
$commandObject = $this->container->get($className);
break;
}
}
if ($commandObject) {
$help = $this->helpGenerator->generateHelp($commandObject);
$this->displayFormattedHelp($help);
} else {
// Fallback to basic info
$this->output->writeLine("Name: {$command->name}", ConsoleColor::WHITE);
if ($command->description) {
$this->output->writeLine("Description: {$command->description}", ConsoleColor::GRAY);
}
}
} catch (\Throwable $e) {
$this->output->writeError("Failed to generate help: {$e->getMessage()}");
if ($this->output->isTerminal()) {
$this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY);
}
}
$this->output->writeLine('');
}
/**
* Display formatted help
*/
private function displayFormattedHelp(CommandHelp $help): void
{
$sections = $help->formatAsColoredText();
foreach ($sections as $section) {
$color = match ($section['color']) {
'BRIGHT_CYAN' => ConsoleColor::BRIGHT_CYAN,
'BRIGHT_YELLOW' => ConsoleColor::BRIGHT_YELLOW,
'BRIGHT_WHITE' => ConsoleColor::BRIGHT_WHITE,
'BRIGHT_GREEN' => ConsoleColor::BRIGHT_GREEN,
'WHITE' => ConsoleColor::WHITE,
'GRAY' => ConsoleColor::GRAY,
'YELLOW' => ConsoleColor::YELLOW,
'RED' => ConsoleColor::RED,
default => ConsoleColor::WHITE
};
$this->output->writeLine($section['text'], $color);
}
}
/**
* Show command history
*/
private function showHistory(): void
{
$history = $this->commandHistory->getRecentHistory(20);
$this->output->writeLine('');
$this->output->writeLine('📜 Command History', ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY);
$this->output->writeLine('');
if (empty($history)) {
$this->output->writeLine('No commands in history yet.', ConsoleColor::GRAY);
} else {
foreach ($history as $index => $entry) {
$command = $entry['command'] ?? 'unknown';
$count = $entry['count'] ?? 1;
$timestamp = isset($entry['timestamp']) ? date('Y-m-d H:i:s', $entry['timestamp']) : 'unknown';
$this->output->writeLine(
sprintf('%3d. %s (used %d times, last: %s)', $index + 1, $command, $count, $timestamp),
ConsoleColor::WHITE
);
}
}
$this->output->writeLine('');
}
/**
* Clear screen
*/
private function clearScreen(): void
{
if (function_exists('shell_exec') && $this->output->isTerminal()) {
// ANSI clear screen sequence
$this->output->write("\033[2J\033[H");
}
}
/**
* Cleanup resources
*/
private function cleanup(): void
{
if ($this->readlineAvailable) {
// Save history
readline_write_history(null);
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\CommandHistory;
use App\Framework\Console\CommandRegistry;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutputInterface;
use App\Framework\Console\ExitCode;
/**
* Handles command execution for dialog mode
*/
final readonly class DialogCommandExecutor
{
public function __construct(
private ConsoleOutputInterface $output,
private CommandRegistry $commandRegistry,
private CommandHistory $commandHistory,
private string $scriptName = 'console'
) {
}
/**
* Execute a command by name with arguments
*
* @param array<int, string> $arguments
*/
public function executeCommand(string $commandName, array $arguments = []): ExitCode
{
$this->commandHistory->addToHistory($commandName);
$this->output->writeLine('');
$this->output->writeLine("Executing: {$commandName}", ConsoleColor::CYAN);
if (! empty($arguments)) {
$this->output->writeLine('Arguments: ' . implode(' ', $arguments), ConsoleColor::GRAY);
}
$this->output->writeLine(str_repeat('─', 60));
try {
$exitCode = $this->commandRegistry->executeCommand($commandName, $arguments, $this->output);
$this->output->writeLine(str_repeat('─', 60));
if ($exitCode->value === 0) {
$this->output->writeLine('✓ Command completed successfully', ConsoleColor::GREEN);
} else {
$this->output->writeLine('✗ Command failed with exit code: ' . $exitCode->value, ConsoleColor::RED);
}
return $exitCode;
} catch (\Exception $e) {
$this->output->writeLine(str_repeat('─', 60));
$this->output->writeLine('✗ Error: ' . $e->getMessage(), ConsoleColor::RED);
if ($this->output->isTerminal()) {
$this->output->writeLine('Stack trace:', ConsoleColor::GRAY);
$this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY);
}
return ExitCode::GENERAL_ERROR;
}
}
}

View File

@@ -25,7 +25,7 @@ final class Table
private ?ConsoleStyle $headerStyle = null,
private ?ConsoleStyle $rowStyle = null,
private ?ConsoleStyle $borderStyle = null,
private bool $showBorders = true
private readonly bool $showBorders = true
) {
$this->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD);
$this->rowStyle ??= ConsoleStyle::create();

View File

@@ -31,8 +31,8 @@ final class TreeHelper
private array $nodes = [];
public function __construct(
private string $title = '',
private ConsoleOutput $output = new ConsoleOutput(),
private string $title = '',
#private readonly ConsoleOutput $output = new ConsoleOutput(),
) {
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
@@ -115,13 +115,13 @@ final class TreeHelper
/**
* Zeigt die vollständige Baumstruktur an.
*/
public function display(): void
public function display(ConsoleOutput $output): void
{
if (! empty($this->title)) {
$this->output->writeLine($this->title, $this->nodeStyle);
$output->writeLine($this->title, $this->nodeStyle);
}
$this->displayTree();
$this->displayTree($output);
}
/**
@@ -156,7 +156,7 @@ final class TreeHelper
* Zeigt die Baumstruktur mit dem aktuellen Präfix an.
* (Interne Methode für rekursives Rendern)
*/
private function displayTree(): void
private function displayTree(ConsoleOutput $output): void
{
$count = count($this->nodes);
@@ -172,7 +172,7 @@ final class TreeHelper
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $linePrefix . $item['title'];
$this->output->writeLine(
$output->writeLine(
$this->lineStyle->apply($linePrefix) .
$style->apply($item['title'])
);
@@ -181,7 +181,7 @@ final class TreeHelper
if (! $item['isLeaf'] && $item['node'] !== null) {
$item['node']
->setPrefix($nodePrefix, $isLast)
->displayTree();
->displayTree($output);
}
}
}

View File

@@ -5,7 +5,9 @@ declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Config\AppConfig;
use App\Framework\Console\Components\ConsoleDialog;
use App\Framework\Console\Components\ConsoleTUI;
use App\Framework\Console\Components\DialogCommandExecutor;
use App\Framework\Console\Components\TuiCommandExecutor;
use App\Framework\Console\Components\TuiInputHandler;
use App\Framework\Console\Components\TuiRenderer;
@@ -179,6 +181,11 @@ final class ConsoleApplication
return $this->launchInteractiveTUI();
}
// Handle dialog mode launch flags
if (in_array($commandName, ['--dialog', '--chat'])) {
return $this->launchDialogMode();
}
// Handle built-in commands
if (in_array($commandName, ['help', '--help', '-h'])) {
// Spezifische Command-Hilfe
@@ -376,6 +383,8 @@ final class ConsoleApplication
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} # Interaktive TUI starten");
$this->output->writeLine(" php {$this->scriptName} --interactive # Interaktive TUI explizit starten");
$this->output->writeLine(" php {$this->scriptName} --dialog # Dialog-Modus starten (AI-Assistent-ähnlich)");
$this->output->writeLine(" php {$this->scriptName} --chat # Dialog-Modus starten (Alias)");
$this->output->writeLine(" php {$this->scriptName} <kategorie> # Commands einer Kategorie anzeigen");
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente] # Kommando direkt ausführen");
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Hilfe für spezifisches Kommando");
@@ -384,6 +393,7 @@ final class ConsoleApplication
$this->output->writeLine("Hinweis:", ConsoleColor::CYAN);
$this->output->writeLine(" Ohne Argumente wird automatisch die interaktive TUI gestartet.");
$this->output->writeLine(" Die TUI bietet eine grafische Navigation durch alle verfügbaren Commands.");
$this->output->writeLine(" Der Dialog-Modus bietet eine einfache Text-Eingabe mit Tab-Completion und History.");
}
/**
@@ -614,6 +624,63 @@ final class ConsoleApplication
}
}
/**
* Startet den Dialog-Modus
*/
private function launchDialogMode(): int
{
try {
// Get DiscoveryRegistry for dialog components
$discoveryRegistry = $this->container->get(DiscoveryRegistry::class);
// Create CommandHistory
$commandHistory = new CommandHistory();
// Create new services
$groupRegistry = new CommandGroupRegistry($discoveryRegistry);
$commandList = $this->commandRegistry->getCommandList();
// Create dialog components
$commandExecutor = new DialogCommandExecutor(
$this->output,
$this->commandRegistry,
$commandHistory,
$this->scriptName
);
// Create dialog instance
$dialog = new ConsoleDialog(
$this->output,
$discoveryRegistry,
$commandHistory,
$groupRegistry,
$commandExecutor,
$commandList,
$this->container,
$this->scriptName . '> '
);
// Start dialog
return $dialog->run()->value;
} catch (Throwable $e) {
$this->output->writeError("Failed to launch dialog mode: " . $e->getMessage());
$config = $this->container->get(AppConfig::class);
if ($config->isDevelopment()) {
$this->output->writeLine("Stack trace:", ConsoleColor::RED);
$this->output->writeLine($e->getTraceAsString());
}
// Fallback to help
$this->output->newLine();
$this->output->writeLine("Falling back to command-line help:", ConsoleColor::YELLOW);
$this->showHelp();
return ExitCode::SOFTWARE_ERROR->value;
}
}
/**
* Prüft ob das Terminal für TUI kompatibel ist
*/

View File

@@ -34,10 +34,10 @@ final readonly class TableResult implements ConsoleResult
* @param ExitCode $exitCode Exit code (default: SUCCESS)
*/
public function __construct(
public readonly array $headers,
public readonly array $rows,
public readonly ?string $title = null,
public readonly ExitCode $exitCode = ExitCode::SUCCESS,
public array $headers,
public array $rows,
public ?string $title = null,
public ExitCode $exitCode = ExitCode::SUCCESS,
) {
$this->data = [
'headers' => $this->headers,
@@ -78,7 +78,7 @@ final readonly class TableResult implements ConsoleResult
*/
public function render(ConsoleOutputInterface $output): void
{
$table = new Table($output);
$table = new Table();
if ($this->title !== null) {
$table->setTitle($this->title);

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\ValueObjects\TerminalStream;
/**
* Utility-Klasse für Terminal-Detection und Farb-Support-Prüfung.
*
* Kann von Console-Modul und Logging-Modul verwendet werden.
*
* Verwendet intern CliSapi für Default-Streams.
*/
final readonly class TerminalDetector
{
/**
* Prüft ob ein Stream ein Terminal ist.
*
* Verwendet intern CliSapi für Default-Streams.
*
* @param TerminalStream|null $stream Terminal Stream (oder null für STDOUT)
* @return bool True wenn Stream ein Terminal ist
*/
public static function isTerminal(?TerminalStream $stream = null): bool
{
if (!function_exists('posix_isatty')) {
return false;
}
$stream ??= CliSapi::detect()->stdout;
$streamResource = $stream->getStream();
return posix_isatty($streamResource);
}
/**
* Prüft ob STDOUT ein Terminal ist.
*
* @return bool True wenn STDOUT ein Terminal ist
*/
public static function isStdoutTerminal(): bool
{
return self::isTerminal(CliSapi::detect()->stdout);
}
/**
* Prüft ob STDERR ein Terminal ist.
*
* @return bool True wenn STDERR ein Terminal ist
*/
public static function isStderrTerminal(): bool
{
return self::isTerminal(CliSapi::detect()->stderr);
}
/**
* Prüft ob ein Stream ein Terminal ist und Farben unterstützt.
*
* Kombiniert Terminal-Detection mit Farb-Support-Prüfung.
* Terminal muss vorhanden sein und TERM-Environment-Variable sollte nicht "dumb" sein.
*
* Verwendet intern CliSapi für Default-Streams.
*
* @param TerminalStream|null $stream Terminal Stream (oder null für STDOUT)
* @return bool True wenn Terminal vorhanden und Farben unterstützt werden
*/
public static function supportsColors(?TerminalStream $stream = null): bool
{
if (!self::isTerminal($stream)) {
return false;
}
// Prüfe TERM Environment-Variable
// "dumb" bedeutet kein Farb-Support
$term = getenv('TERM');
if ($term !== false && strtolower($term) === 'dumb') {
return false;
}
// Prüfe NO_COLOR Environment-Variable (standardisiert)
if (getenv('NO_COLOR') !== false) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\ValueObjects;
use InvalidArgumentException;
/**
* Value Object für Terminal Streams (STDIN/STDOUT/STDERR).
*
* Wrappt Streams type-safe und ermöglicht Terminal-Detection
* ohne direkte resource Type-Hints.
*
* Unterstützt sowohl CLI-Kontext (mit STDOUT/STDERR/STDIN Konstanten)
* als auch Web-Kontext (mit php:// Streams).
*/
final readonly class TerminalStream
{
/**
* @param mixed $stream Stream-Resource (STDIN, STDOUT, STDERR, etc.)
*/
private function __construct(
private mixed $stream
) {
}
/**
* Erstellt ein TerminalStream für STDIN.
*
* Kompatibel mit CLI und Web-Kontext:
* - CLI: Verwendet STDIN-Konstante falls verfügbar
* - Web: Öffnet php://stdin Stream
*/
public static function stdin(): self
{
if (defined('STDIN') && STDIN !== null) {
return new self(STDIN);
}
// Web-Kontext: php://stdin öffnen
$stream = fopen('php://stdin', 'r');
if ($stream === false) {
throw new InvalidArgumentException('Failed to open php://stdin');
}
return new self($stream);
}
/**
* Erstellt ein TerminalStream für STDOUT.
*
* Kompatibel mit CLI und Web-Kontext:
* - CLI: Verwendet STDOUT-Konstante falls verfügbar
* - Web: Öffnet php://stdout Stream
*/
public static function stdout(): self
{
if (defined('STDOUT') && STDOUT !== null) {
return new self(STDOUT);
}
// Web-Kontext: php://stdout öffnen
$stream = fopen('php://stdout', 'w');
if ($stream === false) {
throw new InvalidArgumentException('Failed to open php://stdout');
}
return new self($stream);
}
/**
* Erstellt ein TerminalStream für STDERR.
*
* Kompatibel mit CLI und Web-Kontext:
* - CLI: Verwendet STDERR-Konstante falls verfügbar
* - Web: Öffnet php://stderr Stream
*/
public static function stderr(): self
{
if (defined('STDERR') && STDERR !== null) {
return new self(STDERR);
}
// Web-Kontext: php://stderr öffnen
$stream = fopen('php://stderr', 'w');
if ($stream === false) {
throw new InvalidArgumentException('Failed to open php://stderr');
}
return new self($stream);
}
/**
* Erstellt ein TerminalStream aus einem beliebigen Stream.
*
* @param mixed $stream Stream-Resource
* @throws InvalidArgumentException Wenn Stream nicht gültig ist
*/
public static function fromStream(mixed $stream): self
{
if ($stream === null) {
throw new InvalidArgumentException('Stream cannot be null');
}
return new self($stream);
}
/**
* Gibt den wrapped Stream zurück.
*
* @return mixed Der Stream
*/
public function getStream(): mixed
{
return $this->stream;
}
/**
* Prüft ob dieser Stream STDIN ist.
*/
public function isStdin(): bool
{
return defined('STDIN') && $this->stream === STDIN;
}
/**
* Prüft ob dieser Stream STDOUT ist.
*/
public function isStdout(): bool
{
return defined('STDOUT') && $this->stream === STDOUT;
}
/**
* Prüft ob dieser Stream STDERR ist.
*/
public function isStderr(): bool
{
return defined('STDERR') && $this->stream === STDERR;
}
}

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace App\Framework\Core\System\Ini;
final class IniDirective
final readonly class IniDirective implements Stringable
{
public function __construct(
public string $name,
@@ -12,9 +12,18 @@ final class IniDirective
private int $accessMask,
) {}
public function getAccess(): int
public function getAccess(): Access
{
return Access::fromBitmask($this->accessMask);
}
public function getAccessMask(): int
{
$access = Access::fromBitmask($this->accessMask);
return $this->accessMask;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@@ -57,6 +57,16 @@ enum IniKey: string
case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX = "opcache.enable_file_override_from_index";
case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX_IF_EXISTS = "opcache.enable_file_override_from_index_if_exists";
case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX_IF_EXISTS_IF_EMPTY = "opcache.enable_file_override_from_index_if_exists_if_empty";
case MAX_EXECUTION_TIME = "max_execution_time";
case MAX_INPUT_TIME = "max_input_time";
case UPLOAD_MAX_FILESIZE = "upload_max_filesize";
case POST_MAX_SIZE = "post_max_size";
case MAX_FILE_UPLOADS = "max_file_uploads";
case DISPLAY_ERRORS = "display_errors";
case DISPLAY_STARTUP_ERRORS = "display_startup_errors";
case ERROR_LOG = "error_log";
case DATE_TIMEZONE = "date.timezone";
case EXPOSE_PHP = "expose_php";
case REALPATH_CACHE_SIZE = "realpath_cache_size";
case REALPATH_CACHE_TTL = "realpath_cache_ttl";
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\System\Ini;
final class IniManager
{
/**
* @var array<string, IniDirective> All ini directives loaded once in constructor
*/
private array $directives;
public function __construct()
{
$this->directives = $this->loadDirectives();
}
/**
* Get an ini value by key.
* Returns null if the key does not exist.
*
* @param IniKey|string $key The ini key (enum or string)
* @return IniDirective|null The ini value or null if not found
*/
public function get(IniKey|string $key): IniDirective|null
{
$keyString = $this->normalizeKey($key);
$directive = $this->directives[$keyString] ?? null;
return $directive ?? null;
}
/**
* Set an ini value.
* Only works for keys that are modifiable (INI_USER or INI_ALL).
* Updates the cached directive after successful set.
*
* @param IniKey $key The ini key
* @param string $value The value to set
* @return bool True on success, false on failure
*/
public function set(IniKey $key, string $value): bool
{
$result = ini_set($key->value, $value);
if ($result === false) {
return false;
}
// Update cached directive with new value
if (isset($this->directives[$key->value])) {
$directive = $this->directives[$key->value];
$this->directives[$key->value] = new IniDirective(
name: $directive->name,
value: $value,
global: $directive->global,
accessMask: $directive->getAccessMask()
);
}
return true;
}
/**
* Get a complete IniDirective object with access information.
*
* @param IniKey $key The ini key
* @return IniDirective|null The directive object or null if not found
*/
public function getDirective(IniKey $key): ?IniDirective
{
return $this->directives[$key->value] ?? null;
}
/**
* Get all ini directives as IniDirective objects.
*
* @return array<string, IniDirective> Array of directive name => IniDirective
*/
public function getAll(): array
{
return $this->directives;
}
/**
* Get all ini directives filtered by access level.
*
* @param Access $access The access level to filter by
* @return array<string, IniDirective> Array of directive name => IniDirective
*/
public function getAllByAccess(Access $access): array
{
$filtered = [];
foreach ($this->directives as $name => $directive) {
if ($directive->getAccess() === $access) {
$filtered[$name] = $directive;
}
}
return $filtered;
}
/**
* Check if an ini directive is modifiable.
* A directive is modifiable if it has INI_USER or INI_ALL access.
*
* @param IniKey $key The ini key
* @return bool True if modifiable, false otherwise
*/
public function isModifiable(IniKey $key): bool
{
$directive = $this->getDirective($key);
if ($directive === null) {
return false;
}
$access = $directive->getAccess();
return $access === Access::USER || $access === Access::ALL;
}
/**
* Load all ini directives from ini_get_all() and convert to IniDirective objects.
*
* @return array<string, IniDirective> Array of directive name => IniDirective
*/
private function loadDirectives(): array
{
$all = ini_get_all();
$directives = [];
foreach ($all as $name => $directive) {
$directives[$name] = new IniDirective(
name: $name,
value: $directive['local_value'] ?? $directive['global_value'] ?? '',
global: $directive['global_value'] ?? '',
accessMask: $directive['access'] ?? INI_ALL
);
}
return $directives;
}
/**
* Normalize a key to a string.
* Converts IniKey enum to its string value, or returns the string as-is.
*
* @param IniKey|string $key The key to normalize
* @return string The normalized string key
*/
private function normalizeKey(IniKey|string $key): string
{
return $key instanceof IniKey ? $key->value : $key;
}
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\System;
use Stringable;
final readonly class PhpIni implements Stringable
{
public string $path;
public function __construct(
) {
$path = php_ini_loaded_file();
if($path === false) {
$path = "";
}
$this->path = $path;
}
public function isLoaded(): bool
{
return $this->path !== "";
}
public function __toString(): string
{
return $this->path;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\System;
use App\Framework\Config\Environment;
use App\Framework\Core\System\Ini\IniManager;
/**
* SystemConfig provides unified access to system configuration sources.
*
* This wrapper class exposes IniManager and Environment as public properties,
* allowing direct access to all methods of both services.
*
* Usage:
* $config->ini->get(IniKey::MEMORY_LIMIT)
* $config->env->get('APP_ENV')
* $config->env->getBool('APP_DEBUG')
*/
final readonly class SystemConfig
{
public function __construct(
public readonly IniManager $ini,
public readonly Environment $env
) {}
}

View File

@@ -18,12 +18,12 @@ use App\Framework\DateTime\Clock;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
use App\Framework\Id\Ulid\UlidGenerator;
use App\Framework\Logging\Logger;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\OperationTracker;
use App\Framework\Performance\PerformanceReporter;
use App\Framework\Performance\Repository\PerformanceMetricsRepository;
use App\Framework\Ulid\UlidGenerator;
final readonly class MigrationRunner
{

View File

@@ -264,7 +264,7 @@ final readonly class EntityPersister
if ($caster !== null) {
$result = $caster->toDatabase($value);
// Debug logging for ULID issues
if ($valueType === 'App\Framework\Ulid\Ulid') {
if ($valueType === 'App\Framework\Id\Ulid\Ulid') {
error_log("ULID converted: " . var_export($result, true) . " (length: " . strlen($result) . ")");
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Database\TypeCaster;
use App\Framework\DateTime\SystemClock;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
use InvalidArgumentException;
final class UlidCaster implements TypeCasterInterface

View File

@@ -7,7 +7,7 @@ namespace App\Framework\ErrorAggregation;
use App\Framework\Exception\Core\ErrorSeverity;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
/**
* Represents a single error event for aggregation and analysis

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Framework\ErrorAggregation;
use App\Framework\Exception\Core\ErrorSeverity;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
/**
* Represents a pattern of similar errors for analysis and alerting

View File

@@ -9,7 +9,7 @@ use App\Framework\ErrorAggregation\ErrorEvent;
use App\Framework\ErrorAggregation\ErrorPattern;
use App\Framework\Exception\Core\ErrorSeverity;
use App\Framework\Exception\ErrorCode;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
/**
* Database-based error storage implementation
@@ -25,8 +25,8 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
{
$sql = "
INSERT INTO error_events (
id, service, component, operation, error_code, error_message,
severity, occurred_at, context, metadata, request_id, user_id,
id, service, component, operation, error_code, error_message,
severity, occurred_at, context, metadata, request_id, user_id,
client_ip, is_security_event, stack_trace, user_agent
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
";
@@ -59,8 +59,8 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
$sql = "
INSERT INTO error_events (
id, service, component, operation, error_code, error_message,
severity, occurred_at, context, metadata, request_id, user_id,
id, service, component, operation, error_code, error_message,
severity, occurred_at, context, metadata, request_id, user_id,
client_ip, is_security_event, stack_trace, user_agent
) VALUES " . str_repeat('(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?),', count($events));
@@ -95,9 +95,9 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
{
$sql = "
INSERT INTO error_patterns (
id, fingerprint, service, component, operation, error_code,
normalized_message, severity, occurrence_count, first_occurrence,
last_occurrence, affected_users, affected_ips, is_active,
id, fingerprint, service, component, operation, error_code,
normalized_message, severity, occurrence_count, first_occurrence,
last_occurrence, affected_users, affected_ips, is_active,
is_acknowledged, acknowledged_by, acknowledged_at, resolution, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
@@ -163,8 +163,8 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
public function getActivePatterns(int $limit = 50, int $offset = 0): array
{
$sql = "
SELECT * FROM error_patterns
WHERE is_active = 1
SELECT * FROM error_patterns
WHERE is_active = 1
ORDER BY last_occurrence DESC, occurrence_count DESC
LIMIT ? OFFSET ?
";
@@ -177,7 +177,7 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
public function getPatternsByService(string $service, int $limit = 50): array
{
$sql = "
SELECT * FROM error_patterns
SELECT * FROM error_patterns
WHERE service = ? AND is_active = 1
ORDER BY last_occurrence DESC, occurrence_count DESC
LIMIT ?
@@ -209,14 +209,14 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
public function getStatistics(\DateTimeImmutable $from, \DateTimeImmutable $to): array
{
$sql = "
SELECT
SELECT
COUNT(*) as total_events,
COUNT(DISTINCT service) as services_affected,
COUNT(DISTINCT user_id) as users_affected,
COUNT(DISTINCT client_ip) as ips_affected,
severity,
COUNT(*) as severity_count
FROM error_events
FROM error_events
WHERE occurred_at BETWEEN ? AND ?
GROUP BY severity
";
@@ -259,11 +259,11 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
};
$sql = "
SELECT
SELECT
DATE_FORMAT(occurred_at, ?) as time_bucket,
severity,
COUNT(*) as count
FROM error_events
FROM error_events
WHERE occurred_at BETWEEN ? AND ?
GROUP BY time_bucket, severity
ORDER BY time_bucket ASC
@@ -290,7 +290,7 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface
public function getTopPatterns(int $limit = 10, ?string $service = null): array
{
$sql = "
SELECT * FROM error_patterns
SELECT * FROM error_patterns
WHERE is_active = 1
";
$params = [];

View File

@@ -56,7 +56,7 @@ final readonly class ErrorBoundaryMiddleware implements HttpMiddleware
*/
private function createJsonFallbackResponse($request): JsonResponse
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$errorData = [
'error' => [
'code' => 'SERVICE_TEMPORARILY_UNAVAILABLE',
@@ -75,7 +75,7 @@ final readonly class ErrorBoundaryMiddleware implements HttpMiddleware
*/
private function createHtmlFallbackResponse($request, MiddlewareContext $context)
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$fallbackHtml = $this->getFallbackHtmlContent($request);
return new ViewResult($fallbackHtml, [

View File

@@ -23,6 +23,19 @@ final readonly class Directory
) {
}
/**
* Erstellt eine FilesystemFactory-Instanz mit Logger vom Storage (falls verfügbar)
*/
private function getFactory(): FilesystemFactory
{
$logger = null;
if (property_exists($this->storage, 'logger')) {
$logger = $this->storage->logger ?? null;
}
return FilesystemFactory::create($logger);
}
/**
* Get path as FilePath object
*/
@@ -65,9 +78,10 @@ final readonly class Directory
$paths = $this->storage->listDirectory($this->getPathString());
$files = [];
$factory = $this->getFactory();
foreach ($paths as $path) {
if (is_file($path)) {
$files[] = FilesystemFactory::createFile($path, $this->storage);
$files[] = $factory->createFile($path, $this->storage);
}
}
@@ -84,9 +98,10 @@ final readonly class Directory
$paths = $this->storage->listDirectory($this->getPathString());
$directories = [];
$factory = $this->getFactory();
foreach ($paths as $path) {
if (is_dir($path)) {
$directories[] = FilesystemFactory::createDirectory($path, $this->storage);
$directories[] = $factory->createDirectory($path, $this->storage);
}
}
@@ -104,11 +119,12 @@ final readonly class Directory
$files = [];
$directories = [];
$factory = $this->getFactory();
foreach ($paths as $path) {
if (is_file($path)) {
$files[] = FilesystemFactory::createFile($path, $this->storage);
$files[] = $factory->createFile($path, $this->storage);
} elseif (is_dir($path)) {
$directories[] = FilesystemFactory::createDirectory($path, $this->storage);
$directories[] = $factory->createDirectory($path, $this->storage);
}
}
@@ -122,7 +138,7 @@ final readonly class Directory
{
$filePath = $this->getPath()->join($filename);
return FilesystemFactory::createFile($filePath, $this->storage);
return $this->getFactory()->createFile($filePath, $this->storage);
}
/**
@@ -132,7 +148,7 @@ final readonly class Directory
{
$dirPath = $this->getPath()->join($name);
return FilesystemFactory::createDirectory($dirPath, $this->storage);
return $this->getFactory()->createDirectory($dirPath, $this->storage);
}
/**
@@ -140,7 +156,7 @@ final readonly class Directory
*/
public function refresh(): Directory
{
return FilesystemFactory::createDirectory($this->path, $this->storage);
return $this->getFactory()->createDirectory($this->path, $this->storage);
}
/**
@@ -158,7 +174,7 @@ final readonly class Directory
{
$parentPath = $this->getPath()->getDirectory();
return FilesystemFactory::createDirectory($parentPath, $this->storage);
return $this->getFactory()->createDirectory($parentPath, $this->storage);
}
/**
@@ -273,10 +289,11 @@ final readonly class Directory
continue;
}
$factory = $this->getFactory();
if ($result['is_file']) {
$files[] = FilesystemFactory::createFile($result['path'], $this->storage);
$files[] = $factory->createFile($result['path'], $this->storage);
} elseif ($result['is_dir']) {
$directories[] = FilesystemFactory::createDirectory($result['path'], $this->storage);
$directories[] = $factory->createDirectory($result['path'], $this->storage);
}
}

View File

@@ -5,8 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\LoggerFactory;
use App\Framework\Logging\Logger;
use ReflectionClass;
/**
@@ -14,6 +13,22 @@ use ReflectionClass;
*/
final readonly class FilesystemFactory
{
public function __construct(
private ?Logger $logger = null
) {
}
/**
* Erstellt eine neue FilesystemFactory-Instanz mit Logger.
*
* @param Logger|null $logger Optional logger instance
* @return self
*/
public static function create(?Logger $logger = null): self
{
return new self($logger);
}
/**
* Erstellt ein File-Objekt mit Lazy-Loading für schwere Eigenschaften.
*
@@ -21,21 +36,18 @@ final readonly class FilesystemFactory
* @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(
public function createFile(
FilePath|string $path,
Storage $storage,
?int $cacheTimeoutSeconds = null,
bool $lazyLoad = true,
?DefaultLogger $logger = null
bool $lazyLoad = true
): 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: {$pathString}");
$this->logger?->debug("Erstelle File-Objekt ohne Lazy-Loading: {$pathString}");
// Nur laden wenn die Datei existiert
if (! $storage->exists($pathString)) {
@@ -53,21 +65,22 @@ final readonly class FilesystemFactory
$reflection = new ReflectionClass(File::class);
$loadTime = time();
$logger = $this->logger;
// 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) {
$pathStr = $file->getPathString();
$logger->debug("Lazy-Loading contents für {$pathStr}");
$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 {$pathStr}, lade neu");
$logger?->debug("Cache-Timeout erreicht für {$pathStr}, lade neu");
}
if (! $file->exists()) {
$logger->debug("Datei existiert nicht: {$pathStr}");
$logger?->debug("Datei existiert nicht: {$pathStr}");
return '';
}
@@ -78,15 +91,15 @@ final readonly class FilesystemFactory
// Dateigröße wird erst beim ersten Zugriff ermittelt
'size' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$pathStr = $file->getPathString();
$logger->debug("Lazy-Loading size für {$pathStr}");
$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 {$pathStr}, lade neu");
$logger?->debug("Cache-Timeout erreicht für {$pathStr}, lade neu");
}
if (! $file->exists()) {
$logger->debug("Datei existiert nicht: {$pathStr}");
$logger?->debug("Datei existiert nicht: {$pathStr}");
return 0;
}
@@ -97,15 +110,15 @@ final readonly class FilesystemFactory
// Zeitstempel wird erst beim ersten Zugriff ermittelt
'lastModified' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
$pathStr = $file->getPathString();
$logger->debug("Lazy-Loading lastModified für {$pathStr}");
$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 {$pathStr}, lade neu");
$logger?->debug("Cache-Timeout erreicht für {$pathStr}, lade neu");
}
if (! $file->exists()) {
$logger->debug("Datei existiert nicht: {$pathStr}");
$logger?->debug("Datei existiert nicht: {$pathStr}");
return 0;
}
@@ -121,20 +134,17 @@ final readonly class FilesystemFactory
* @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(
public function createDirectory(
FilePath|string $path,
Storage $storage,
bool $lazyLoad = true,
?DefaultLogger $logger = null
bool $lazyLoad = true
): 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: {$pathString}");
$this->logger?->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$pathString}");
$contents = [];
if (is_dir($pathString)) {
@@ -145,13 +155,14 @@ final readonly class FilesystemFactory
}
$reflection = new ReflectionClass(Directory::class);
$logger = $this->logger;
// LazyGhost verwenden - alle Eigenschaften werden beim ersten Zugriff initialisiert
$lazyDir = $reflection->newLazyGhost(
// Initializer-Callback
function (Directory $directory) use ($logger): void {
$pathStr = $directory->getPathString();
$logger->debug("Lazy-Loading Directory-Inhalt für {$pathStr}");
$logger?->debug("Lazy-Loading Directory-Inhalt für {$pathStr}");
// Verzeichnisinhalt wird erst beim ersten Zugriff auf eine Eigenschaft geladen
if ($directory->exists()) {

View File

@@ -210,7 +210,8 @@ final class InMemoryStorage implements Storage, StreamableStorage
*/
public function file(string $path): File
{
return FilesystemFactory::createFile($path, $this);
$factory = FilesystemFactory::create($this->logger ?? null);
return $factory->createFile($path, $this);
}
/**
@@ -218,7 +219,8 @@ final class InMemoryStorage implements Storage, StreamableStorage
*/
public function directory(string $path): Directory
{
return FilesystemFactory::createDirectory($path, $this);
$factory = FilesystemFactory::create($this->logger ?? null);
return $factory->createDirectory($path, $this);
}
public function getMimeType(string $path): string

View File

@@ -4,8 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\LoggerFactory;
use App\Framework\Logging\Logger;
/**
* Decorator für Storage-Implementierungen mit Logging-Unterstützung.
@@ -18,50 +17,52 @@ final class LoggableStorage implements Storage
public \App\Framework\Async\FiberManager $fiberManager { get => $this->storage->fiberManager; }
public ?Logger $logger;
public function __construct(
private readonly Storage $storage,
private ?DefaultLogger $logger = null
?Logger $logger = null
) {
$this->logger ??= LoggerFactory::getDefaultLogger();
$this->logger = $logger;
}
public function get(string $path): string
{
$this->logger->debug("Lese Datei: {$path}");
$this->logger?->debug("Lese Datei: {$path}");
return $this->storage->get($path);
}
public function put(string $path, string $content): void
{
$this->logger->debug("Schreibe Datei: {$path}");
$this->logger?->debug("Schreibe Datei: {$path}");
$this->storage->put($path, $content);
}
public function exists(string $path): bool
{
$exists = $this->storage->exists($path);
$this->logger->debug("Prüfe Existenz: {$path} - " . ($exists ? 'existiert' : 'existiert nicht'));
$this->logger?->debug("Prüfe Existenz: {$path} - " . ($exists ? 'existiert' : 'existiert nicht'));
return $exists;
}
public function delete(string $path): void
{
$this->logger->debug("Lösche Datei: {$path}");
$this->logger?->debug("Lösche Datei: {$path}");
$this->storage->delete($path);
}
public function copy(string $source, string $destination): void
{
$this->logger->debug("Kopiere Datei: {$source} -> {$destination}");
$this->logger?->debug("Kopiere Datei: {$source} -> {$destination}");
$this->storage->copy($source, $destination);
}
public function size(string $path): int
{
$size = $this->storage->size($path);
$this->logger->debug("Dateigröße: {$path} - {$size} Bytes");
$this->logger?->debug("Dateigröße: {$path} - {$size} Bytes");
return $size;
}
@@ -69,7 +70,7 @@ final class LoggableStorage implements Storage
public function lastModified(string $path): int
{
$lastModified = $this->storage->lastModified($path);
$this->logger->debug("Letzte Änderung: {$path} - " . date('Y-m-d H:i:s', $lastModified));
$this->logger?->debug("Letzte Änderung: {$path} - " . date('Y-m-d H:i:s', $lastModified));
return $lastModified;
}
@@ -77,7 +78,7 @@ final class LoggableStorage implements Storage
public function getMimeType(string $path): string
{
$mimeType = $this->storage->getMimeType($path);
$this->logger->debug("MIME-Typ: {$path} - {$mimeType}");
$this->logger?->debug("MIME-Typ: {$path} - {$mimeType}");
return $mimeType;
}
@@ -85,7 +86,7 @@ final class LoggableStorage implements Storage
public function isReadable(string $path): bool
{
$isReadable = $this->storage->isReadable($path);
$this->logger->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein'));
$this->logger?->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein'));
return $isReadable;
}
@@ -93,70 +94,70 @@ final class LoggableStorage implements Storage
public function isWritable(string $path): bool
{
$isWritable = $this->storage->isWritable($path);
$this->logger->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein'));
$this->logger?->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein'));
return $isWritable;
}
public function listDirectory(string $directory): array
{
$this->logger->debug("Liste Verzeichnis: {$directory}");
$this->logger?->debug("Liste Verzeichnis: {$directory}");
$files = $this->storage->listDirectory($directory);
$this->logger->debug("Gefundene Dateien: " . count($files));
$this->logger?->debug("Gefundene Dateien: " . count($files));
return $files;
}
public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void
{
$this->logger->debug("Erstelle Verzeichnis: {$path}");
$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}");
$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}");
$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");
$this->logger?->debug("Führe Batch-Operation aus mit " . count($operations) . " Operationen");
$results = $this->storage->batch($operations);
$this->logger->debug("Batch-Operation abgeschlossen");
$this->logger?->debug("Batch-Operation abgeschlossen");
return $results;
}
public function getMultiple(array $paths): array
{
$this->logger->debug("Lese " . count($paths) . " Dateien parallel");
$this->logger?->debug("Lese " . count($paths) . " Dateien parallel");
$results = $this->storage->getMultiple($paths);
$this->logger->debug("Parallel-Lesen abgeschlossen");
$this->logger?->debug("Parallel-Lesen abgeschlossen");
return $results;
}
public function putMultiple(array $files): void
{
$this->logger->debug("Schreibe " . count($files) . " Dateien parallel");
$this->logger?->debug("Schreibe " . count($files) . " Dateien parallel");
$this->storage->putMultiple($files);
$this->logger->debug("Parallel-Schreiben abgeschlossen");
$this->logger?->debug("Parallel-Schreiben abgeschlossen");
}
public function getMetadataMultiple(array $paths): array
{
$this->logger->debug("Lade Metadaten für " . count($paths) . " Dateien parallel");
$this->logger?->debug("Lade Metadaten für " . count($paths) . " Dateien parallel");
$results = $this->storage->getMetadataMultiple($paths);
$this->logger->debug("Metadaten-Laden abgeschlossen");
$this->logger?->debug("Metadaten-Laden abgeschlossen");
return $results;
}

View File

@@ -14,7 +14,8 @@ trait StorageTrait
*/
public function file(string $path): File
{
return FilesystemFactory::createFile($path, $this);
$factory = FilesystemFactory::create($this->logger ?? null);
return $factory->createFile($path, $this);
}
/**
@@ -22,6 +23,7 @@ trait StorageTrait
*/
public function directory(string $path): Directory
{
return FilesystemFactory::createDirectory($path, $this);
$factory = FilesystemFactory::create($this->logger ?? null);
return $factory->createDirectory($path, $this);
}
}

View File

@@ -16,7 +16,7 @@ trait AtomicStorageTrait
{
public function putAtomic(string $path, string $content): void
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$tempPath = $path . '.tmp.' . $generator->generate();
$this->put($tempPath, $content);

View File

@@ -79,7 +79,7 @@ final readonly class FilePath implements Stringable
*/
public static function temp(?string $filename = null): self
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$filename ??= 'tmp_' . $generator->generate();
return self::tempDir()->join($filename);

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Contracts;
/**
* Common interface for all ID generators
*/
interface IdGeneratorInterface
{
/**
* Generate a new ID
*
* @return IdInterface|string Returns either an IdInterface or string representation
*/
public function generate(): IdInterface|string;
/**
* Generate a batch of IDs
*
* @param int $count Number of IDs to generate
* @return array<int, IdInterface|string>
*/
public function generateBatch(int $count): array;
/**
* Validate if a string is a valid ID for this generator
*/
public function isValid(string $value): bool;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Contracts;
/**
* Common interface for all ID value objects
*/
interface IdInterface
{
/**
* Get the string representation of the ID
*/
public function toString(): string;
/**
* Get the string value
*/
public function getValue(): string;
/**
* Check equality with another ID
*/
public function equals(self $other): bool;
}

View File

@@ -2,10 +2,12 @@
declare(strict_types=1);
namespace App\Framework\Cuid;
namespace App\Framework\Id\Cuid;
use App\Framework\Id\Contracts\IdInterface;
use DateTimeImmutable;
use InvalidArgumentException;
use Stringable;
/**
* Cuid Value Object
@@ -16,7 +18,7 @@ use InvalidArgumentException;
* - Optimized for horizontal scaling and collision resistance
* - Always starts with 'c' for collision-resistant
*/
final readonly class Cuid implements Stringable
final readonly class Cuid implements IdInterface, Stringable
{
public const int LENGTH = 25;
public const string PREFIX = 'c';
@@ -167,8 +169,12 @@ final readonly class Cuid implements Stringable
/**
* Check equality with another Cuid
*/
public function equals(self $other): bool
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->value === $other->value;
}

View File

@@ -2,8 +2,9 @@
declare(strict_types=1);
namespace App\Framework\Cuid;
namespace App\Framework\Id\Cuid;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;
@@ -12,7 +13,7 @@ use InvalidArgumentException;
*
* Generates Collision-resistant Unique Identifiers with machine fingerprinting.
*/
final class CuidGenerator
final class CuidGenerator implements IdGeneratorInterface
{
private int $counter = 0;

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Id\Cuid\CuidGenerator;
use App\Framework\Id\Ksuid\KsuidGenerator;
use App\Framework\Id\NanoId\NanoId;
use App\Framework\Id\NanoId\NanoIdGenerator;
use App\Framework\Id\Ulid\UlidGenerator;
use App\Framework\Random\RandomGenerator;
use App\Framework\Random\SecureRandomGenerator;
/**
* Factory for creating ID generators
*/
final readonly class IdGeneratorFactory
{
public function __construct(
private RandomGenerator $randomGenerator,
private ?Clock $clock = null
) {
}
/**
* Create a generator for the specified ID type
*
* @param IdType|string $type The ID type to create a generator for
* @return IdGeneratorInterface
*/
public function create(IdType|string $type): IdGeneratorInterface
{
if (is_string($type)) {
$type = IdType::fromString($type);
}
return match ($type) {
IdType::CUID => new CuidGenerator($this->randomGenerator),
IdType::KSUID => new KsuidGenerator($this->randomGenerator),
IdType::NANOID => new NanoIdGenerator($this->randomGenerator),
IdType::ULID => new UlidGenerator($this->clock ?? new SystemClock()),
};
}
/**
* Create a CUID generator
*/
public function createCuid(?string $customFingerprint = null): CuidGenerator
{
return new CuidGenerator($this->randomGenerator, $customFingerprint);
}
/**
* Create a KSUID generator
*/
public function createKsuid(): KsuidGenerator
{
return new KsuidGenerator($this->randomGenerator);
}
/**
* Create a NanoId generator
*/
public function createNanoId(int $defaultSize = NanoId::DEFAULT_SIZE, string $defaultAlphabet = NanoId::DEFAULT_ALPHABET): NanoIdGenerator
{
return new NanoIdGenerator($this->randomGenerator, $defaultSize, $defaultAlphabet);
}
/**
* Create a ULID generator
*/
public function createUlid(?Clock $clock = null): UlidGenerator
{
return new UlidGenerator($clock ?? $this->clock ?? new SystemClock());
}
/**
* Create a factory with default dependencies
*/
public static function createDefault(): self
{
return new self(
new SecureRandomGenerator(),
new SystemClock()
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id;
/**
* Enum for ID types supported by the framework
*/
enum IdType: string
{
case CUID = 'cuid';
case KSUID = 'ksuid';
case NANOID = 'nanoid';
case ULID = 'ulid';
/**
* Get all available ID types
*
* @return array<string>
*/
public static function all(): array
{
return array_column(self::cases(), 'value');
}
/**
* Check if a string value is a valid ID type
*/
public static function isValid(string $value): bool
{
foreach (self::cases() as $case) {
if ($case->value === strtolower($value)) {
return true;
}
}
return false;
}
/**
* Create from string value
*/
public static function fromString(string $value): self
{
$value = strtolower($value);
foreach (self::cases() as $case) {
if ($case->value === $value) {
return $case;
}
}
throw new \InvalidArgumentException("Invalid ID type: {$value}. Valid types are: " . implode(', ', self::all()));
}
}

View File

@@ -2,11 +2,13 @@
declare(strict_types=1);
namespace App\Framework\Ksuid;
namespace App\Framework\Id\Ksuid;
use App\Framework\Id\Contracts\IdInterface;
use BcMath\Number;
use DateTimeImmutable;
use InvalidArgumentException;
use Stringable;
/**
* KSUID Value Object
@@ -17,7 +19,7 @@ use InvalidArgumentException;
* - Lexicographically sortable by creation time
* - URL-safe, case-sensitive
*/
final readonly class Ksuid
final readonly class Ksuid implements IdInterface, Stringable
{
public const int ENCODED_LENGTH = 27;
public const int TIMESTAMP_BYTES = 4;
@@ -159,8 +161,12 @@ final readonly class Ksuid
/**
* Check equality with another KSUID
*/
public function equals(self $other): bool
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->value === $other->value;
}

View File

@@ -2,8 +2,9 @@
declare(strict_types=1);
namespace App\Framework\Ksuid;
namespace App\Framework\Id\Ksuid;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Random\RandomGenerator;
use DateTimeImmutable;
use InvalidArgumentException;
@@ -13,7 +14,7 @@ use InvalidArgumentException;
*
* Generates K-Sortable Unique Identifiers with timestamp ordering.
*/
final readonly class KsuidGenerator
final readonly class KsuidGenerator implements IdGeneratorInterface
{
public function __construct(
private RandomGenerator $randomGenerator
@@ -183,7 +184,7 @@ final readonly class KsuidGenerator
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**@return array{min: \App\Framework\Ksuid\Ksuid, max: \App\Framework\Ksuid\Ksuid}
/**@return array{min: \App\Framework\Id\Ksuid\Ksuid, max: \App\Framework\Id\Ksuid\Ksuid}
* Generate KSUIDs for a time range (useful for queries)
*/
public function generateTimeRange(int $startTimestamp, int $endTimestamp): array

View File

@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Framework\NanoId;
namespace App\Framework\Id\NanoId;
use App\Framework\Id\Contracts\IdInterface;
use InvalidArgumentException;
use Stringable;
/**
* NanoId Value Object
@@ -14,7 +14,7 @@ use Stringable;
* Default alphabet: A-Za-z0-9_-
* Default size: 21 characters
*/
final readonly class NanoId implements Stringable
final readonly class NanoId implements IdInterface
{
public const int DEFAULT_SIZE = 21;
public const string DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-';
@@ -123,8 +123,12 @@ final readonly class NanoId implements Stringable
/**
* Check equality with another NanoId
*/
public function equals(self $other): bool
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->value === $other->value;
}

View File

@@ -2,8 +2,9 @@
declare(strict_types=1);
namespace App\Framework\NanoId;
namespace App\Framework\Id\NanoId;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;
@@ -12,7 +13,7 @@ use InvalidArgumentException;
*
* Provides flexible NanoId generation with various presets and configurations.
*/
final readonly class NanoIdGenerator
final readonly class NanoIdGenerator implements IdGeneratorInterface
{
private int $defaultSize;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Framework\Ulid;
namespace App\Framework\Id\Ulid;
use App\Framework\Core\Encoding\Base32Alphabet;
use App\Framework\Core\Encoding\Base32Encoder;

View File

@@ -2,9 +2,10 @@
declare(strict_types=1);
namespace App\Framework\Ulid;
namespace App\Framework\Id\Ulid;
use App\Framework\DateTime\Clock;
use App\Framework\Id\Contracts\IdInterface;
use DateTimeImmutable;
use InvalidArgumentException;
use JsonSerializable;
@@ -12,7 +13,7 @@ use JsonSerializable;
/**
* Objekt-Wrapper für ULIDs mit String-/JSON-API.
*/
final readonly class Ulid implements JsonSerializable
final readonly class Ulid implements IdInterface, JsonSerializable
{
private string $ulid;
@@ -62,6 +63,34 @@ final readonly class Ulid implements JsonSerializable
return $this->ulid;
}
/**
* @inheritDoc
*/
public function toString(): string
{
return $this->ulid;
}
/**
* @inheritDoc
*/
public function getValue(): string
{
return $this->ulid;
}
/**
* @inheritDoc
*/
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->ulid === $other->ulid;
}
public function jsonSerialize(): string
{
return $this->ulid;

View File

@@ -2,10 +2,11 @@
declare(strict_types=1);
namespace App\Framework\Ulid;
namespace App\Framework\Id\Ulid;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\Id\Contracts\IdGeneratorInterface;
/**
* ULID Generator - Universally Unique Lexicographically Sortable Identifier
@@ -17,7 +18,7 @@ use App\Framework\DateTime\SystemClock;
* - Production: new UlidGenerator() - uses SystemClock automatically
* - Testing: new UlidGenerator($mockClock) - inject mock for deterministic tests
*/
final readonly class UlidGenerator
final readonly class UlidGenerator implements IdGeneratorInterface
{
public function __construct(
private ?Clock $clock = null
@@ -61,4 +62,34 @@ final readonly class UlidGenerator
{
return $prefix . '_' . $this->generate();
}
/**
* @inheritDoc
*/
public function generateBatch(int $count): array
{
if ($count <= 0) {
throw new \InvalidArgumentException('Count must be positive');
}
if ($count > 10000) {
throw new \InvalidArgumentException('Batch size cannot exceed 10000');
}
$ids = [];
for ($i = 0; $i < $count; $i++) {
$ids[] = $this->generate();
}
return $ids;
}
/**
* @inheritDoc
*/
public function isValid(string $value): bool
{
return \App\Framework\Id\Ulid\Ulid::isValid($value);
}
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Framework\Ulid;
namespace App\Framework\Id\Ulid;
use App\Framework\DateTime\Clock;
use DateTimeImmutable;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Framework\Ulid;
namespace App\Framework\Id\Ulid;
/**
* Validiert ULID-Strings.

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Id\Contracts\IdInterface;
use InvalidArgumentException;
/**
* Unified ID Generator
*
* A wrapper that can generate any type of ID based on configuration.
* Provides a single interface for generating different ID formats.
*/
final readonly class UnifiedIdGenerator implements IdGeneratorInterface
{
public function __construct(
private IdGeneratorFactory $factory,
private IdType $defaultType = IdType::ULID
) {
}
/**
* Generate a new ID using the default type
*/
public function generate(): IdInterface|string
{
return $this->generateWithType($this->defaultType);
}
/**
* Generate a new ID of the specified type
*
* @param IdType|string $type The ID type to generate
* @return IdInterface|string
*/
public function generateWithType(IdType|string $type): IdInterface|string
{
$generator = $this->factory->create($type);
return $generator->generate();
}
/**
* Generate a batch of IDs using the default type
*
* @param int $count Number of IDs to generate
* @return array<int, IdInterface|string>
*/
public function generateBatch(int $count): array
{
return $this->generateBatchWithType($count, $this->defaultType);
}
/**
* Generate a batch of IDs of the specified type
*
* @param int $count Number of IDs to generate
* @param IdType|string $type The ID type to generate
* @return array<int, IdInterface|string>
*/
public function generateBatchWithType(int $count, IdType|string $type): array
{
$generator = $this->factory->create($type);
return $generator->generateBatch($count);
}
/**
* Validate if a string is a valid ID for the default type
*/
public function isValid(string $value): bool
{
return $this->isValidForType($value, $this->defaultType);
}
/**
* Validate if a string is a valid ID for the specified type
*
* @param string $value The ID string to validate
* @param IdType|string $type The ID type to validate against
*/
public function isValidForType(string $value, IdType|string $type): bool
{
$generator = $this->factory->create($type);
return $generator->isValid($value);
}
/**
* Get the default ID type
*/
public function getDefaultType(): IdType
{
return $this->defaultType;
}
/**
* Create a unified generator with default settings
*/
public static function createDefault(?IdType $defaultType = null): self
{
return new self(
IdGeneratorFactory::createDefault(),
$defaultType ?? IdType::ULID
);
}
/**
* Create a unified generator with a custom factory
*/
public static function create(IdGeneratorFactory $factory, ?IdType $defaultType = null): self
{
return new self(
$factory,
$defaultType ?? IdType::ULID
);
}
}

View File

@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Framework\Logging\Formatter;
use App\Framework\Console\CliSapi;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\TerminalDetector;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
@@ -17,6 +20,21 @@ final readonly class DevelopmentFormatter implements LogFormatter
private bool $colorOutput = true
) {
}
/**
* Prüft ob Farben verwendet werden sollen (berücksichtigt Terminal-Detection)
*/
private function shouldUseColors(): bool
{
// Wenn colorOutput explizit false, keine Farben
if (!$this->colorOutput) {
return false;
}
// Prüfe Terminal-Detection mit CliSapi
$cliSapi = CliSapi::detect();
return $cliSapi->supportsColors($cliSapi->stdout);
}
public function __invoke(LogRecord $record): string
{
@@ -26,7 +44,7 @@ final readonly class DevelopmentFormatter implements LogFormatter
$message = $record->getMessage();
// Color coding for levels
$levelString = $this->colorOutput ? $this->colorizeLevel($level) : $level->getName();
$levelString = $this->shouldUseColors() ? $this->colorizeLevel($level) : $level->getName();
$output = sprintf(
"%s [%s] %s.%s: %s\n",
@@ -54,20 +72,11 @@ final readonly class DevelopmentFormatter implements LogFormatter
private function colorizeLevel(LogLevel $level): string
{
if (! $this->colorOutput) {
return $level->getName();
}
return match($level) {
LogLevel::DEBUG => "\033[36m" . $level->getName() . "\033[0m", // Cyan
LogLevel::INFO => "\033[32m" . $level->getName() . "\033[0m", // Green
LogLevel::NOTICE => "\033[34m" . $level->getName() . "\033[0m", // Blue
LogLevel::WARNING => "\033[33m" . $level->getName() . "\033[0m", // Yellow
LogLevel::ERROR => "\033[31m" . $level->getName() . "\033[0m", // Red
LogLevel::CRITICAL => "\033[35m" . $level->getName() . "\033[0m", // Magenta
LogLevel::ALERT => "\033[41m" . $level->getName() . "\033[0m", // Red background
LogLevel::EMERGENCY => "\033[41;37m" . $level->getName() . "\033[0m", // Red bg, white text
};
// Verwende LogLevel::getConsoleColor() und ConsoleStyle statt hardcoded ANSI-Codes
$consoleColor = $level->getConsoleColor();
$style = ConsoleStyle::create(color: $consoleColor);
return $style->apply($level->getName());
}
private function formatContext(array $context): string

View File

@@ -4,15 +4,34 @@ declare(strict_types=1);
namespace App\Framework\Logging\Formatter;
use App\Framework\Console\CliSapi;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\TerminalDetector;
use App\Framework\Logging\LogRecord;
final readonly class LineFormatter implements LogFormatter
{
public function __construct(
private string $format = "[{timestamp}] {channel}.{level}: {message} {context}",
private string $timestampFormat = 'Y-m-d H:i:s'
private string $timestampFormat = 'Y-m-d H:i:s',
private bool $colorOutput = false
) {
}
/**
* Prüft ob Farben verwendet werden sollen (berücksichtigt Terminal-Detection)
*/
private function shouldUseColors(): bool
{
// Wenn colorOutput explizit false, keine Farben
if (!$this->colorOutput) {
return false;
}
// Prüfe Terminal-Detection mit CliSapi (für stderr, da Web-Requests auf stderr loggen)
$cliSapi = CliSapi::detect();
return $cliSapi->supportsColors($cliSapi->stderr);
}
public function __invoke(LogRecord $record): string
{
@@ -33,11 +52,17 @@ final readonly class LineFormatter implements LogFormatter
? "[{$record->getChannel()}] "
: '';
// Level-String mit optionalen Farben
$levelName = $record->level->getName();
$coloredLevel = $this->shouldUseColors()
? $this->colorizeLevel($record->level)
: $levelName;
$replacements = [
'{timestamp}' => $record->getFormattedTimestamp($this->timestampFormat),
'{channel}' => $record->channel ?? 'app',
'{level}' => $record->level->getName(),
'{level_name}' => $record->level->getName(),
'{level}' => $coloredLevel,
'{level_name}' => $coloredLevel,
'{message}' => $record->message,
'{context}' => $contextString,
'{request_id}' => $requestId,
@@ -45,4 +70,16 @@ final readonly class LineFormatter implements LogFormatter
return strtr($this->format, $replacements);
}
/**
* Färbt den Level-String basierend auf LogLevel
*/
private function colorizeLevel(\App\Framework\Logging\LogLevel $level): string
{
// Verwende LogLevel::getConsoleColor() und ConsoleStyle statt hardcoded ANSI-Codes
$consoleColor = $level->getConsoleColor();
$style = ConsoleStyle::create(color: $consoleColor);
return $style->apply($level->getName());
}
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace App\Framework\Logging;
use App\Framework\Config\EnvKey;
use App\Framework\Config\Environment;
use App\Framework\Config\TypedConfiguration;
use App\Framework\Console\CliSapi;
use App\Framework\Console\TerminalDetector;
use App\Framework\Core\PathProvider;
use App\Framework\Logging\Formatter\DevelopmentFormatter;
use App\Framework\Logging\Formatter\LineFormatter;
use App\Framework\Logging\Handlers\ConsoleHandler;
use App\Framework\Logging\Handlers\DockerJsonHandler;
use App\Framework\Logging\Handlers\FileHandler;
use App\Framework\Logging\Handlers\MultiFileHandler;
use App\Framework\Queue\Queue;
/**
* Factory für Log-Handler-Erstellung basierend auf Umgebung und Konfiguration.
*/
final readonly class HandlerFactory
{
/**
* Erstellt alle Handler basierend auf der Umgebung
*
* @param TypedConfiguration $config
* @param Environment $env
* @param LogConfig $logConfig
* @param PathProvider $pathProvider
* @param LogLevel $minLevel
* @param Queue $queue
* @return array<LogHandler>
*/
public function createHandlers(
TypedConfiguration $config,
Environment $env,
LogConfig $logConfig,
PathProvider $pathProvider,
LogLevel $minLevel,
Queue $queue
): array {
$handlers = [];
// Console/Docker Logging Handler - für CLI und Web-Requests
if (PHP_SAPI === 'cli') {
// CLI: Docker JSON oder Console Handler
$handlers[] = $this->createCliHandler($config, $env, $minLevel);
} else {
// Web-Requests: Console Handler auf stderr mit intelligenter Formatter-Auswahl
$cliSapi = CliSapi::detect();
$colorOutput = $this->determineColorOutput($env, $cliSapi->stderr);
$handlers[] = $this->createWebHandler($minLevel, $colorOutput);
}
// MultiFileHandler für automatisches Channel-Routing
$multiFileFormatter = new LineFormatter();
$handlers[] = new MultiFileHandler(
$logConfig,
$pathProvider,
$multiFileFormatter,
$minLevel,
0644
);
// Fallback FileHandler für Kompatibilität (nur für 'app' Channel ohne Channel-Info)
$fileFormatter = new LineFormatter(
format: 'Line Formatter: [{timestamp}] [{level_name}] {request_id}{channel}{message}',
timestampFormat: 'Y-m-d H:i:s'
);
$handlers[] = new FileHandler(
$fileFormatter,
$logConfig->getLogPath('app'),
$minLevel,
0644,
null,
$pathProvider
);
return $handlers;
}
/**
* Erstellt den CLI-Handler (Docker JSON oder Console)
*/
private function createCliHandler(
TypedConfiguration $config,
Environment $env,
LogLevel $minLevel
): LogHandler {
// Prüfe ob wir in Docker laufen (für strukturierte JSON-Logs)
$inDocker = file_exists('/.dockerenv') || getenv('DOCKER_CONTAINER') === 'true';
if ($inDocker) {
if ($config->app->isProduction()) {
// Production Docker: Compact JSON für Log-Aggregatoren mit Redaction
return new DockerJsonHandler(
env: $env,
minLevel: $minLevel,
redactSensitiveData: true // Auto-redact in Production
);
} else {
// Development Docker: Pretty JSON für bessere Lesbarkeit
return new DockerJsonHandler(
env: $env,
serviceName: $config->app->name ?? 'app',
minLevel: $minLevel,
prettyPrint: true, // Pretty-print für Development
redactSensitiveData: false // Keine Redaction in Development für Debugging
);
}
} else {
// Lokale Entwicklung: Console Handler mit intelligenter Formatter-Auswahl
$cliSapi = CliSapi::detect();
$colorOutput = $this->determineColorOutput($env, $cliSapi->stdout);
return $this->createCliConsoleHandler($minLevel, $colorOutput);
}
}
/**
* Erstellt einen Web-Request ConsoleHandler mit intelligenter Formatter-Auswahl
*/
private function createWebHandler(LogLevel $minLevel, bool $colorOutput): ConsoleHandler
{
$developmentFormatter = new DevelopmentFormatter(
includeStackTrace: true,
colorOutput: $colorOutput
);
$lineFormatter = new LineFormatter(
colorOutput: $colorOutput
);
// Intelligente Formatter-Auswahl: Array mit beiden Formattern
return new ConsoleHandler(
[
'development' => $developmentFormatter,
'line' => $lineFormatter,
],
$minLevel,
debugOnly: false
);
}
/**
* Erstellt einen CLI ConsoleHandler mit intelligenter Formatter-Auswahl
*/
private function createCliConsoleHandler(LogLevel $minLevel, bool $colorOutput): ConsoleHandler
{
$developmentFormatter = new DevelopmentFormatter(
includeStackTrace: true,
colorOutput: $colorOutput
);
$lineFormatter = new LineFormatter(
colorOutput: $colorOutput
);
// Intelligente Formatter-Auswahl: Array mit beiden Formattern
return new ConsoleHandler(
[
'development' => $developmentFormatter,
'line' => $lineFormatter,
],
$minLevel
);
}
/**
* Bestimmt ob Farb-Output aktiviert werden soll
*
* @param Environment $env Environment für Konfiguration
* @param \App\Framework\Console\ValueObjects\TerminalStream $stream Terminal Stream für Terminal-Detection
* @return bool True wenn Farben aktiviert werden sollen
*/
private function determineColorOutput(Environment $env, \App\Framework\Console\ValueObjects\TerminalStream $stream): bool
{
$colorConfig = $env->get(EnvKey::LOG_COLOR_OUTPUT, 'auto');
return match (strtolower((string) $colorConfig)) {
'true', '1', 'yes', 'on' => true,
'false', '0', 'no', 'off' => false,
default => TerminalDetector::supportsColors($stream), // 'auto' oder default
};
}
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Framework\Logging\Handlers;
use App\Framework\Logging\LogHandler;
use App\Framework\Logging\Formatter\DevelopmentFormatter;
use App\Framework\Logging\Formatter\LineFormatter;
use App\Framework\Logging\Formatter\LogFormatter;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
@@ -12,6 +14,10 @@ use App\Framework\Logging\LogRecord;
/**
* Handler für die Ausgabe von Log-Einträgen in der Konsole.
*
* Unterstützt intelligente Formatter-Auswahl basierend auf LogLevel und Context:
* - ERROR/CRITICAL/ALERT/EMERGENCY oder Exception → DevelopmentFormatter (detailliert)
* - DEBUG/INFO/NOTICE/WARNING → LineFormatter (kompakt)
*
* Bei CLI: Nutzt stderr für WARNING+ und stdout für niedrigere Levels.
* Bei Web-Requests: Alle Logs gehen auf stderr (POSIX-konform, Docker-kompatibel).
*/
@@ -28,26 +34,53 @@ class ConsoleHandler implements LogHandler
private bool $debugOnly;
/**
* @var LogFormatter Formatter für die Log-Ausgabe
* @var LogFormatter|null Formatter für die Log-Ausgabe (bei einfachem Modus)
*/
private LogFormatter $formatter;
private ?LogFormatter $formatter = null;
/**
* @var DevelopmentFormatter|null Formatter für detaillierte Ausgabe (bei intelligentem Modus)
*/
private ?DevelopmentFormatter $developmentFormatter = null;
/**
* @var LineFormatter|null Formatter für kompakte Ausgabe (bei intelligentem Modus)
*/
private ?LineFormatter $lineFormatter = null;
/**
* @var bool Ob intelligente Formatter-Auswahl aktiviert ist
*/
private bool $intelligentSelection;
/**
* Erstellt einen neuen ConsoleHandler
*
* @param LogFormatter $formatter Formatter für die Log-Ausgabe
* @param LogFormatter|array{development: DevelopmentFormatter, line: LineFormatter}|null $formatter
* Einzelner Formatter (rückwärtskompatibel) oder Array mit 'development' und 'line' Formattern für intelligente Auswahl
* @param LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird
* @param bool $debugOnly Ob der Handler nur im Debug-Modus aktiv ist
* @param LogLevel $stderrLevel Level ab dem stderr verwendet wird (default: WARNING)
*/
public function __construct(
LogFormatter $formatter,
LogFormatter|array|null $formatter = null,
LogLevel|int $minLevel = LogLevel::DEBUG,
bool $debugOnly = true,
private readonly LogLevel $stderrLevel = LogLevel::WARNING,
) {
$this->formatter = $formatter;
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
$this->debugOnly = $debugOnly;
// Intelligente Formatter-Auswahl wenn Array übergeben wird
if (is_array($formatter)) {
$this->intelligentSelection = true;
$this->developmentFormatter = $formatter['development'] ?? null;
$this->lineFormatter = $formatter['line'] ?? null;
} else {
// Rückwärtskompatibel: Einzelner Formatter
$this->intelligentSelection = false;
$this->formatter = $formatter;
}
}
/**
@@ -68,8 +101,11 @@ class ConsoleHandler implements LogHandler
*/
public function handle(LogRecord $record): void
{
// Formatter auswählen (intelligent oder einfach)
$formatter = $this->selectFormatter($record);
// Formatter verwenden für Formatierung
$formatted = ($this->formatter)($record);
$formatted = $formatter($record);
// Formatter gibt immer string zurück für Console
$output = is_string($formatted)
@@ -96,9 +132,57 @@ class ConsoleHandler implements LogHandler
}
/**
* Gibt den Formatter zurück
* Wählt den passenden Formatter basierend auf LogLevel und Context
*/
public function getFormatter(): LogFormatter
private function selectFormatter(LogRecord $record): LogFormatter
{
// Wenn kein intelligenter Modus, verwende den einzelnen Formatter
if (!$this->intelligentSelection) {
if ($this->formatter === null) {
throw new \RuntimeException('ConsoleHandler: No formatter configured');
}
return $this->formatter;
}
// Prüfe ob Exception vorhanden ist
if ($this->hasException($record)) {
return $this->developmentFormatter ?? $this->lineFormatter ?? throw new \RuntimeException('ConsoleHandler: No formatter configured');
}
// Für ERROR, CRITICAL, ALERT, EMERGENCY: DevelopmentFormatter (detailliert)
if ($record->level->value >= LogLevel::ERROR->value) {
return $this->developmentFormatter ?? $this->lineFormatter ?? throw new \RuntimeException('ConsoleHandler: No formatter configured');
}
// Für DEBUG, INFO, NOTICE, WARNING: LineFormatter (kompakt)
return $this->lineFormatter ?? $this->developmentFormatter ?? throw new \RuntimeException('ConsoleHandler: No formatter configured');
}
/**
* Prüft ob der LogRecord eine Exception enthält
*/
private function hasException(LogRecord $record): bool
{
// Prüfe in extras
if ($record->hasExtra('exception_class') || $record->hasExtra('exception')) {
return true;
}
// Prüfe in context
$context = $record->getContext();
if (isset($context['exception']) || isset($context['exception_class'])) {
return true;
}
return false;
}
/**
* Gibt den Formatter zurück (rückwärtskompatibel)
*
* @deprecated Verwende selectFormatter() für intelligente Auswahl
*/
public function getFormatter(): ?LogFormatter
{
return $this->formatter;
}

View File

@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Logging;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
/**
* Factory für Logger-Instanzen.
*
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
* Diese Klasse wird nur noch für Legacy-Code verwendet und sollte nicht in neuem Code genutzt werden.
*/
final class LoggerFactory
{
private static ?DefaultLogger $defaultLogger = null;
/**
* Erzeugt einen neuen Logger mit optionalen Einstellungen.
*
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
*/
public static function create(
?Clock $clock = null,
LogLevel|int $minLevel = LogLevel::DEBUG,
array $handlers = []
): DefaultLogger {
$clock ??= new SystemClock();
return new DefaultLogger($clock, $minLevel, $handlers);
}
/**
* Gibt den Standard-Logger zurück oder erstellt ihn, falls er noch nicht existiert.
*
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
*/
public static function getDefaultLogger(): DefaultLogger
{
if (self::$defaultLogger === null) {
$debug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN);
$minLevel = $debug ? LogLevel::DEBUG : LogLevel::INFO;
self::$defaultLogger = self::create(null, $minLevel);
}
return self::$defaultLogger;
}
/**
* Setzt einen benutzerdefinierten Logger als Standard-Logger.
*
* @deprecated Verwende stattdessen Dependency Injection.
*/
public static function setDefaultLogger(DefaultLogger $logger): void
{
self::$defaultLogger = $logger;
}
}

View File

@@ -11,17 +11,7 @@ use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Logging\Formatter\DevelopmentFormatter;
use App\Framework\Logging\Formatter\LineFormatter;
use App\Framework\Logging\Handlers\ConsoleHandler;
use App\Framework\Logging\Handlers\DockerJsonHandler;
use App\Framework\Logging\Handlers\FileHandler;
use App\Framework\Logging\Handlers\MultiFileHandler;
use App\Framework\Logging\Handlers\NullHandler;
use App\Framework\Queue\Queue;
use App\Framework\Queue\FileQueue;
use App\Framework\Redis\RedisConfig;
use App\Framework\Redis\RedisConnection;
final readonly class LoggerInitializer
{
@@ -44,8 +34,13 @@ final readonly class LoggerInitializer
$processorManager = new ProcessorManager();
$minLevel = $this->determineMinLogLevel($config);
$logConfig = $this->initializeLogConfig($pathProvider);
$queue = $this->createQueue($pathProvider);
$handlers = $this->createHandlers($config, $env, $logConfig, $pathProvider, $minLevel, $queue);
$queueFactory = new QueueFactory();
$queue = $queueFactory->createQueue($pathProvider, $env);
$handlerFactory = new HandlerFactory();
$handlers = $handlerFactory->createHandlers($config, $env, $logConfig, $pathProvider, $minLevel, $queue);
$contextManager = $container->get(LogContextManager::class);
$clock = $container->get(Clock::class);
@@ -113,117 +108,4 @@ final readonly class LoggerInitializer
return $logConfig;
}
/**
* Erstellt die Queue für asynchrones Logging
*/
private function createQueue(PathProvider $pathProvider): Queue
{
#$redisConfig = RedisConfig::fromEnvironment($env);
#$redisConnection = new RedisConnection($redisConfig, 'queue');
#return new RedisQueue($redisConnection, 'commands');
$queuePath = $pathProvider->resolvePath('storage/queue');
return new FileQueue($queuePath);
}
/**
* Erstellt alle Handler basierend auf der Umgebung
*
* @param TypedConfiguration $config
* @param Environment $env
* @param LogConfig $logConfig
* @param PathProvider $pathProvider
* @param LogLevel $minLevel
* @param Queue $queue
* @return array<LogHandler>
*/
private function createHandlers(
TypedConfiguration $config,
Environment $env,
LogConfig $logConfig,
PathProvider $pathProvider,
LogLevel $minLevel,
Queue $queue
): array {
$handlers = [];
// Console/Docker Logging Handler - für CLI und Web-Requests
if (PHP_SAPI === 'cli') {
// CLI: Docker JSON oder Console Handler
$handlers[] = $this->createCliHandler($config, $env, $minLevel);
} else {
// Web-Requests: Console Handler auf stderr
$webFormatter = new LineFormatter();
$handlers[] = new ConsoleHandler($webFormatter, $minLevel, debugOnly: false);
}
//$handlers[] = new QueuedLogHandler($queue);
// MultiFileHandler für automatisches Channel-Routing
$multiFileFormatter = new LineFormatter();
$handlers[] = new MultiFileHandler(
$logConfig,
$pathProvider,
$multiFileFormatter,
$minLevel,
0644
);
// Fallback FileHandler für Kompatibilität (nur für 'app' Channel ohne Channel-Info)
$fileFormatter = new LineFormatter(
format: 'Line Formatter: [{timestamp}] [{level_name}] {request_id}{channel}{message}',
timestampFormat: 'Y-m-d H:i:s'
);
$handlers[] = new FileHandler(
$fileFormatter,
$logConfig->getLogPath('app'),
$minLevel,
0644,
null,
$pathProvider
);
return $handlers;
}
/**
* Erstellt den CLI-Handler (Docker JSON oder Console)
*/
private function createCliHandler(
TypedConfiguration $config,
Environment $env,
LogLevel $minLevel
): LogHandler {
// Prüfe ob wir in Docker laufen (für strukturierte JSON-Logs)
$inDocker = file_exists('/.dockerenv') || getenv('DOCKER_CONTAINER') === 'true';
if ($inDocker) {
if ($config->app->isProduction()) {
// Production Docker: Compact JSON für Log-Aggregatoren mit Redaction
return new DockerJsonHandler(
env: $env,
minLevel: $minLevel,
redactSensitiveData: true // Auto-redact in Production
);
} else {
// Development Docker: Pretty JSON für bessere Lesbarkeit
return new DockerJsonHandler(
env: $env,
serviceName: $config->app->name ?? 'app',
minLevel: $minLevel,
prettyPrint: true, // Pretty-print für Development
redactSensitiveData: false // Keine Redaction in Development für Debugging
);
}
} else {
// Lokale Entwicklung: Farbige Console-Logs
$consoleFormatter = new DevelopmentFormatter(
includeStackTrace: true,
colorOutput: true
);
return new ConsoleHandler($consoleFormatter, $minLevel);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\Logging;
use App\Framework\Config\Environment;
use App\Framework\Core\PathProvider;
use App\Framework\Queue\FileQueue;
use App\Framework\Queue\Queue;
/**
* Factory für Queue-Instanzen für asynchrones Logging.
*
* Unterstützt aktuell FileQueue, kann in Zukunft um RedisQueue erweitert werden.
*/
final readonly class QueueFactory
{
/**
* Erstellt eine Queue-Instanz basierend auf der Konfiguration
*
* @param PathProvider $pathProvider Path provider für File-basierte Queues
* @param Environment|null $env Optional: Environment für zukünftige Redis-Integration
* @return Queue
*/
public function createQueue(PathProvider $pathProvider, ?Environment $env = null): Queue
{
// TODO: Redis-Queue Integration wenn benötigt
// if ($env && $this->shouldUseRedisQueue($env)) {
// $redisConfig = RedisConfig::fromEnvironment($env);
// $redisConnection = new RedisConnection($redisConfig, 'queue');
// return new RedisQueue($redisConnection, 'commands');
// }
$queuePath = $pathProvider->resolvePath('storage/queue');
return new FileQueue($queuePath);
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Logging\ValueObjects;
use App\Framework\Http\ServerRequest;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
use Ramsey\Uuid\Uuid;
/**

View File

@@ -7,10 +7,9 @@ namespace App\Framework\MachineLearning\ModelManagement;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Version;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Ulid\UlidGenerator;
use App\Framework\Core\ValueObjects\Version;
/**
* Cache-based Performance Storage

View File

@@ -8,12 +8,12 @@ use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\Id\Ulid\UlidGenerator;
use App\Framework\MagicLinks\MagicLinkData;
use App\Framework\MagicLinks\MagicLinkToken;
use App\Framework\MagicLinks\TokenAction;
use App\Framework\MagicLinks\TokenConfig;
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
use App\Framework\Ulid\UlidGenerator;
use DateTimeImmutable;
final readonly class CacheMagicLinkService implements MagicLinkService

View File

@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace App\Framework\MagicLinks\Services;
use App\Framework\DateTime\Clock;
use App\Framework\Id\Ulid\UlidGenerator;
use App\Framework\MagicLinks\MagicLinkData;
use App\Framework\MagicLinks\MagicLinkToken;
use App\Framework\MagicLinks\TokenAction;
use App\Framework\MagicLinks\TokenConfig;
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
use App\Framework\Ulid\UlidGenerator;
final class InMemoryMagicLinkService implements MagicLinkService
{

View File

@@ -260,7 +260,7 @@ final class SmtpTransport implements TransportInterface
private function buildMultipartAlternativeMessage(Message $message, array $lines): string
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$boundary = 'alt_' . $generator->generate();
$lines[] = 'MIME-Version: 1.0';
@@ -292,7 +292,7 @@ final class SmtpTransport implements TransportInterface
private function buildMultipartMixedMessage(Message $message, array $lines): string
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$boundary = 'mixed_' . $generator->generate();
$lines[] = 'MIME-Version: 1.0';
@@ -377,7 +377,7 @@ final class SmtpTransport implements TransportInterface
private function generateMessageId(): string
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
return $generator->generate() . '.' . time() . '@' . gethostname();
}
@@ -415,7 +415,7 @@ final class SmtpTransport implements TransportInterface
}
// Fallback to generated ID
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
return $generator->generate() . '@' . gethostname();
}

View File

@@ -27,7 +27,7 @@ final class MockTransport implements TransportInterface
);
}
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$messageId = 'mock_' . $generator->generate();
$this->sentMessages[] = [
'message' => $message,

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Notification\Templates;
use App\Framework\Ulid\UlidGenerator;
use App\Framework\Id\Ulid\UlidGenerator;
/**
* Template Identifier Value Object

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Framework\Notification\ValueObjects;
use App\Framework\DateTime\SystemClock;
use App\Framework\Ulid\UlidGenerator;
use App\Framework\Ulid\UlidValidator;
use App\Framework\Id\Ulid\UlidGenerator;
use App\Framework\Id\Ulid\UlidValidator;
/**
* Unique identifier for notifications

View File

@@ -7,11 +7,11 @@ namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\FailureReason;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job that failed and was moved to the dead letter queue

View File

@@ -7,9 +7,9 @@ namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job chain entry in the database

View File

@@ -7,9 +7,9 @@ namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\Queue\ValueObjects\DependencyType;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job dependency entry in the database

View File

@@ -7,8 +7,8 @@ namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Ulid\Ulid;
#[Entity(table: 'job_metrics')]
final readonly class JobMetricsEntry

View File

@@ -8,8 +8,8 @@ use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job progress tracking entry

View File

@@ -339,7 +339,7 @@ final readonly class FileQueue implements Queue
private function generatePriorityFilename(float $score): string
{
$scoreStr = str_pad((string) (int) ($score * 1000000), 15, '0', STR_PAD_LEFT);
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
return "job_{$scoreStr}_" . $generator->generate() . '.json';
}
@@ -349,7 +349,7 @@ final readonly class FileQueue implements Queue
*/
private function generateDelayedFilename(int $availableTime): string
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
return "delayed_{$availableTime}_" . $generator->generate() . '.json';
}

View File

@@ -218,7 +218,7 @@ final readonly class QueueJobFeatureExtractor
foreach ($metricsHistory as $metrics) {
// Create minimal metadata from metrics
$metadata = new JobMetadata(
id: new \App\Framework\Ulid\Ulid(new \App\Framework\DateTime\SystemClock()),
id: new \App\Framework\Id\Ulid\Ulid(new \App\Framework\DateTime\SystemClock()),
class: \App\Framework\Core\ValueObjects\ClassName::create($metrics->queueName),
type: 'job',
queuedAt: \App\Framework\Core\ValueObjects\Timestamp::now(),

View File

@@ -245,7 +245,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
private function generateBatchId(): string
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
return 'batch_' . $generator->generate();
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Queue\ValueObjects;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
/**
* Value Object representing a unique Job identifier

View File

@@ -8,7 +8,7 @@ use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\SystemClock;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
/**
* Job Metadata Value Object

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Queue\ValueObjects;
use App\Framework\Ulid\UlidGenerator;
use App\Framework\Id\Ulid\UlidGenerator;
/**
* Value Object representing a unique Worker identifier

View File

@@ -57,7 +57,7 @@ final readonly class ConsoleTraceExporter implements TraceExporter
$spanMap = [];
$rootSpans = [];
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
// First, create a map of all spans
foreach ($spans as $span) {
$spanId = $span['spanId'] ?? $generator->generate();

View File

@@ -68,7 +68,7 @@ final readonly class DatabaseTraceExporter implements TraceExporter
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
";
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$traceId = $traceData['traceId'] ?? $generator->generate();
$startTime = $traceData['startTime'] ?? microtime(true);
$endTime = $traceData['endTime'] ?? ($startTime + ($traceData['duration'] ?? 0));
@@ -120,7 +120,7 @@ final readonly class DatabaseTraceExporter implements TraceExporter
// Remove trailing comma
$sql = rtrim($sql, ',');
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$values = [];
foreach ($spans as $span) {
$spanStartTime = $span['startTime'] ?? microtime(true);
@@ -210,8 +210,8 @@ final readonly class DatabaseTraceExporter implements TraceExporter
}
$spansSql = "
SELECT * FROM {$this->spansTable}
WHERE trace_id = ?
SELECT * FROM {$this->spansTable}
WHERE trace_id = ?
ORDER BY start_time ASC
";
$spans = $this->connection->fetchAll($spansSql, [$trace['id']]);
@@ -278,9 +278,9 @@ final readonly class DatabaseTraceExporter implements TraceExporter
$sql = "
SELECT trace_id, start_time, end_time, duration, span_count, error_count, status
FROM {$this->tracesTable}
FROM {$this->tracesTable}
{$whereClause}
ORDER BY start_time DESC
ORDER BY start_time DESC
LIMIT ? OFFSET ?
";

View File

@@ -46,7 +46,7 @@ final readonly class JaegerExporter implements TraceExporter
private function convertToJaegerFormat(array $traceData): array
{
$generator = new \App\Framework\Ulid\UlidGenerator();
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
$traceId = $traceData['traceId'] ?? $generator->generate();
$spans = [];

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Framework\TypeCaster\Casters;
use App\Framework\DateTime\SystemClock;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\TypeCaster\TypeCasterInterface;
use App\Framework\Ulid\Ulid;
use InvalidArgumentException;
final readonly class UlidCaster implements TypeCasterInterface

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Ulid\UlidValidator;
use App\Framework\Id\Ulid\UlidValidator;
use App\Framework\Validation\ValidationRule;
use Attribute;

View File

@@ -8,7 +8,7 @@ use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\DateTime\Clock;
use App\Framework\Http\IpAddress;
use App\Framework\Ulid\Ulid;
use App\Framework\Id\Ulid\Ulid;
use App\Framework\UserAgent\UserAgent;
use App\Framework\Vault\Exceptions\VaultException;
use App\Framework\Vault\Exceptions\VaultKeyNotFoundException;