chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics;
final readonly class AnalyticsEvent
{
public function __construct(
public string $type,
public string $name,
public array $data = [],
public array $context = [],
public ?string $userId = null,
public ?string $sessionId = null,
public ?\DateTimeImmutable $timestamp = null
) {}
public static function pageView(string $path, array $context = []): self
{
return new self(
type: 'page_view',
name: $path,
context: $context,
timestamp: new \DateTimeImmutable()
);
}
public static function userAction(string $action, string $userId, array $data = []): self
{
return new self(
type: 'user_action',
name: $action,
data: $data,
userId: $userId,
timestamp: new \DateTimeImmutable()
);
}
public static function performance(string $metric, float $value, array $context = []): self
{
return new self(
type: 'performance',
name: $metric,
data: ['value' => $value],
context: $context,
timestamp: new \DateTimeImmutable()
);
}
public function withUserId(string $userId): self
{
return new self(
$this->type,
$this->name,
$this->data,
$this->context,
$userId,
$this->sessionId,
$this->timestamp
);
}
public function withSessionId(string $sessionId): self
{
return new self(
$this->type,
$this->name,
$this->data,
$this->context,
$this->userId,
$sessionId,
$this->timestamp
);
}
public function toArray(): array
{
return [
'type' => $this->type,
'name' => $this->name,
'data' => $this->data,
'context' => $this->context,
'user_id' => $this->userId,
'session_id' => $this->sessionId,
'timestamp' => $this->timestamp?->format(\DateTimeInterface::ATOM)
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics;
interface AnalyticsInterface
{
public function track(AnalyticsEvent $event): void;
public function trackPageView(string $path, array $context = []): void;
public function trackUserAction(string $action, string $userId, array $data = []): void;
public function trackPerformance(string $metric, float $value, array $context = []): void;
public function flush(): void;
public function isEnabled(): bool;
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics;
use App\Framework\Queue\Queue;
use App\Framework\Context\ExecutionContext;
use App\Framework\Config\Configuration;
final class AnalyticsService implements AnalyticsInterface
{
private bool $enabled;
private array $context = [];
public function __construct(
private readonly Queue $queue,
private readonly ExecutionContext $executionContext,
private readonly Configuration $config
) {
$this->enabled = $this->config->get('analytics.enabled', false);
$this->initializeContext();
}
public function track(AnalyticsEvent $event): void
{
#if (!$this->enabled) {
# return;
#}
$enrichedEvent = $this->enrichEvent($event);
// Queue für asynchrone Verarbeitung
$this->queue->push('analytics', $enrichedEvent->toArray());
}
public function trackPageView(string $path, array $context = []): void
{
$this->track(AnalyticsEvent::pageView($path, array_merge($this->context, $context)));
}
public function trackUserAction(string $action, string $userId, array $data = []): void
{
$this->track(AnalyticsEvent::userAction($action, $userId, $data));
}
public function trackPerformance(string $metric, float $value, array $context = []): void
{
$this->track(AnalyticsEvent::performance($metric, $value, array_merge($this->context, $context)));
}
public function flush(): void
{
// Queue-basiert - kein explizites Flush nötig
}
public function isEnabled(): bool
{
return $this->enabled;
}
private function enrichEvent(AnalyticsEvent $event): AnalyticsEvent
{
$sessionId = $this->executionContext->getSessionId();
$userId = $this->executionContext->getUserId();
$enrichedEvent = $event;
if ($sessionId && !$event->sessionId) {
$enrichedEvent = $enrichedEvent->withSessionId($sessionId);
}
if ($userId && !$event->userId) {
$enrichedEvent = $enrichedEvent->withUserId($userId);
}
return $enrichedEvent;
}
private function initializeContext(): void
{
$this->context = [
'environment' => $this->config->get('app.environment', 'production'),
'version' => $this->config->get('app.version', '1.0.0'),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'ip_address' => $this->getClientIp(),
];
}
private function getClientIp(): ?string
{
$headers = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$ips = explode(',', $_SERVER[$header]);
return trim($ips[0]);
}
}
return null;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics\Middleware;
use App\Framework\Analytics\AnalyticsInterface;
use App\Framework\Analytics\AnalyticsService;
use App\Framework\Config\Configuration;
use App\Framework\Context\ExecutionContext;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\PerformanceMeter;
use App\Framework\Queue\FileQueue;
#[MiddlewarePriorityAttribute(MiddlewarePriority::BUSINESS_LOGIC)]
final readonly class AnalyticsMiddleware implements HttpMiddleware
{
private AnalyticsService $analytics;
public function __construct(
#private AnalyticsInterface $analytics = new AnalyticsService($queue, $executionContext, $config),
private ExecutionContext $executionContext,
private Configuration $config,
private PerformanceMeter $performanceMeter
) {
$this->analytics = new AnalyticsService(new FileQueue(__DIR__.'/analytics-data'), $this->executionContext, $this->config);
}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$startTime = microtime(true);
$request = $context->request;
// Track page view
/*$this->analytics->trackPageView(
$context->request->path,
[
'method' => $request->method->value,
'query_params' => $request->queryParams,
'referer' => $request->headers->get('Referer')
]
);*/
$context = $next($context);
// Track response performance
$responseTime = (microtime(true) - $startTime) * 1000;
/*$this->analytics->trackPerformance('response_time', $responseTime, [
'path' => $request->path,
'method' => $request->method->value,
'status_code' => $context->response->status->value
]);*/
return $context;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics\Queue;
use App\Framework\Queue\QueueProcessor;
use App\Framework\Analytics\Storage\AnalyticsStorageInterface;
final readonly class AnalyticsProcessor implements QueueProcessor
{
public function __construct(
private AnalyticsStorageInterface $storage
) {}
public function canProcess(string $queue): bool
{
return $queue === 'analytics';
}
public function process(array $payload): void
{
$this->storage->store($payload);
}
public function failed(array $payload, \Throwable $exception): void
{
// Log failed analytics events
error_log("Analytics processing failed: " . $exception->getMessage());
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics\Storage;
interface AnalyticsStorageInterface
{
public function store(array $eventData): void;
public function query(array $filters = []): array;
public function aggregate(string $metric, array $filters = []): array;
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Framework\Analytics\Storage;
final class FileAnalyticsStorage implements AnalyticsStorageInterface
{
public function __construct(
private readonly string $storagePath
) {
if (!is_dir($this->storagePath)) {
mkdir($this->storagePath, 0755, true);
}
}
public function store(array $eventData): void
{
$date = date('Y-m-d');
$filename = $this->storagePath . "/analytics-{$date}.jsonl";
$line = json_encode($eventData) . "\n";
file_put_contents($filename, $line, FILE_APPEND | LOCK_EX);
}
public function query(array $filters = []): array
{
// Einfache Implementation - kann erweitert werden
$files = glob($this->storagePath . '/analytics-*.jsonl');
$events = [];
foreach ($files as $file) {
$lines = file($file, FILE_IGNORE_NEW_LINES);
foreach ($lines as $line) {
$event = json_decode($line, true);
if ($this->matchesFilters($event, $filters)) {
$events[] = $event;
}
}
}
return $events;
}
public function aggregate(string $metric, array $filters = []): array
{
$events = $this->query($filters);
// Einfache Aggregation - kann erweitert werden
$aggregated = [];
foreach ($events as $event) {
$key = $event['type'] ?? 'unknown';
$aggregated[$key] = ($aggregated[$key] ?? 0) + 1;
}
return $aggregated;
}
private function matchesFilters(array $event, array $filters): bool
{
foreach ($filters as $key => $value) {
if (!isset($event[$key]) || $event[$key] !== $value) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\Api;
use App\Framework\HttpClient\ClientResponse;
use RuntimeException;
class ApiException extends RuntimeException
{
public function __construct(
string $message,
int $code,
private readonly ClientResponse $response
) {
parent::__construct($message, $code);
}
public function getResponse(): ClientResponse
{
return $this->response;
}
public function getResponseData(): ?array
{
try {
return json_decode($this->response->body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Framework\Api;
use App\Framework\Http\Method;
use App\Framework\HttpClient\ClientOptions;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\CurlHttpClient;
use App\Framework\HttpClient\HttpClient;
trait ApiRequestTrait
{
private string $baseUrl;
private ClientOptions $defaultOptions;
private HttpClient $httpClient;
/**
* Sendet eine Anfrage an die API
*/
public function sendRequest(
Method $method,
string $endpoint,
array $data = [],
?ClientOptions $options = null
): ClientResponse {
$url = $this->baseUrl . '/' . ltrim($endpoint, '/');
$options = $options ?? $this->defaultOptions;
$request = ClientRequest::json(
method: $method,
url: $url,
data: $data,
options: $options
);
$response = $this->httpClient->send($request);
$this->handleResponseErrors($response);
return $response;
}
/**
* Verarbeitet Fehler in der API-Antwort
*/
private function handleResponseErrors(ClientResponse $response): void
{
if ($response->status->value < 400) {
return;
}
$responseData = json_decode($response->body, true) ?: [];
throw new ApiException(
$this->formatErrorMessage($responseData),
$response->status->value,
$response
);
}
/**
* Formatiert eine benutzerfreundliche Fehlermeldung
*/
private function formatErrorMessage(array $responseData): string
{
$errorMessage = 'API-Fehler';
if (isset($responseData['detail'])) {
$errorMessage .= ': ' . $responseData['detail'];
if (isset($responseData['validation_messages'])) {
$errorMessage .= ' - Validierungsfehler: ' . json_encode(
$responseData['validation_messages'],
JSON_UNESCAPED_UNICODE
);
}
} elseif (isset($responseData['error'])) {
$errorMessage .= ': ' . $responseData['error'];
}
return $errorMessage;
}
/**
* Dekodiert den Response-Body als JSON
*/
protected function decodeJson(ClientResponse $response): array
{
return json_decode($response->body, true) ?: [];
}
}

View File

@@ -4,15 +4,16 @@ declare(strict_types=1);
namespace App\Framework\Attributes;
use App\Framework\Http\Method;
use Attribute;
#[Attribute(\Attribute::TARGET_METHOD, \Attribute::IS_REPEATABLE)]
class Route
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final readonly class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
public Method $method = Method::GET,
public ?string $name = null,
) {
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\Attributes;
/**
* Kennzeichnet eine Controller-Methode, deren Route als statische Seite generiert werden soll.
*/
#[\Attribute(\Attribute::TARGET_METHOD, \Attribute::IS_REPEATABLE)]
final class StaticPage
{
/**
* @param string|null $outputPath Optionaler benutzerdefinierter Ausgabepfad für die statische Seite
* @param bool $prerender Ob die Seite beim Deployment vorgerendert werden soll
*/
public function __construct(
public ?string $outputPath = null,
public bool $prerender = true
) {}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Framework\Auth;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
class Auth
{
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Framework\Auth;
use App\Framework\Core\AttributeMapper;
final readonly class AuthMapper implements AttributeMapper
{
public function getAttributeClass(): string
{
return Auth::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
{
return [
'class' => $reflectionTarget->getDeclaringClass()->getName(),
'method' => $reflectionTarget->getName(),
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Framework\Cache;
interface Cache
{
public function get(string $key): CacheItem;
public function set(string $key, mixed $value, ?int $ttl = null): bool;
public function has(string $key): bool;
public function forget(string $key): bool;
public function clear(): bool;
/**
* Führt Callback aus, wenn Wert nicht im Cache ist ("Remember"-Pattern)
* und cached das Ergebnis für die gewünschte Zeit
*/
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem;
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Framework\Cache;
use ReflectionException;
use ReflectionMethod;
final readonly class CacheDecorator
{
public function __construct(
private object $service,
private Cache $cache
) {}
/**
* @throws ReflectionException
*/
public function __call(string $name, array $args)
{
$method = new ReflectionMethod($this->service, $name);
$attrs = $method->getAttributes(Cacheable::class);
if ($attrs) {
$attr = $attrs[0]->newInstance();
$key = $attr->key ?? $method->getName() . ':' . md5(serialize($args));
$ttl = $attr->ttl ?? 3600;
return $this->cache->remember($key, fn() => $method->invokeArgs($this->service, $args), $ttl);
}
return $method->invokeArgs($this->service, $args);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Framework\Cache;
interface CacheDriver
{
public function get(string $key): CacheItem;
public function set(string $key, string $value, ?int $ttl = null): bool;
public function has(string $key): bool;
public function forget(string $key): bool;
public function clear(): bool;
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Framework\Cache;
use App\Framework\Cache\Compression\GzipCompression;
use App\Framework\Cache\Compression\NullCompression;
use App\Framework\Cache\Driver\ApcuCache;
use App\Framework\Cache\Driver\FileCache;
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\Driver\NullCache;
use App\Framework\Cache\Driver\RedisCache;
use App\Framework\Cache\Serializer\JsonSerializer;
use App\Framework\Cache\Serializer\PhpSerializer;
use App\Framework\DI\Initializer;
final readonly class CacheInitializer
{
public function __construct(
private string $redisHost = 'redis',
private int $redisPort = 6379,
private int $compressionLevel = -1,
private int $minCompressionLength = 1024
) {}
#[Initializer]
public function __invoke(): Cache
{
$this->clear();
$serializer = new PhpSerializer();
$serializer = new JsonSerializer();
$compression = new GzipCompression($this->compressionLevel, $this->minCompressionLength);
// L1 Cache:
if(function_exists('apcu_clear_cache')) {
$apcuCache = new GeneralCache(new APCuCache);
}else {
$apcuCache = new GeneralCache(new InMemoryCache);
}
$compressedApcuCache = new CompressionCacheDecorator(
$apcuCache,
$compression,
$serializer
);
// L2 Cache:
$redisCache = new GeneralCache(new RedisCache(host: $this->redisHost, port: $this->redisPort));
$compressedRedisCache = new CompressionCacheDecorator(
$redisCache,
$compression,
$serializer
);
$multiLevelCache = new MultiLevelCache($compressedApcuCache, $compressedRedisCache);
#return $multiLevelCache;
return new LoggingCacheDecorator($multiLevelCache);
#return new GeneralCache(new NullCache());
}
private function clear(): void
{
apcu_clear_cache();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Framework\Cache;
final readonly class CacheItem
{
private function __construct(
public string $key,
public mixed $value,
public bool $isHit,
) {}
public static function miss(string $key): self
{
return new self($key, null, false);
}
public static function hit(string $key, mixed $value): self
{
return new self($key, $value, true);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Framework\Cache;
enum CachePrefix: string
{
case GENERAL = 'cache:';
case QUERY = 'query_cache:';
#case SESSION = 'session:';
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Framework\Cache;
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Cacheable
{
/**
* @param string|null $key Template z.B.: "user:profile:{id}"
*/
public function __construct(
public ?string $key = null,
public int $ttl = 3600,
) {}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Commands;
use App\Framework\Cache\Cache;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
final readonly class ClearCache
{
public function __construct(
private Cache $cache,
) {}
#[ConsoleCommand("cache:clear", "Clears the cache")]
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
{
$this->cache->clear();
$output->writeSuccess("Cache cleared");
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Framework\Cache\Compression;
use App\Framework\Cache\CompressionAlgorithm;
final class GzipCompression implements CompressionAlgorithm
{
private const string PREFIX = 'gz:';
private int $level;
private int $threshold;
public function __construct(int $compressionLevel = -1, int $minLengthToCompress = 1024)
{
$this->level = $compressionLevel;
$this->threshold = $minLengthToCompress;
}
public function compress(string $value, bool $forceCompression = false): string
{
if (!$forceCompression && strlen($value) < $this->threshold) {
return $value;
}
$compressed = gzcompress($value, $this->level);
if ($compressed === false) {
// Fallback auf Originalwert bei Fehler
return $value;
}
return self::PREFIX . $compressed;
}
public function decompress(string $value): string
{
if (!$this->isCompressed($value)) {
return $value;
}
$raw = substr($value, strlen(self::PREFIX));
$decompressed = @gzuncompress($raw);
return $decompressed !== false ? $decompressed : $value;
}
public function isCompressed(string $value): bool
{
return strncmp($value, self::PREFIX, strlen(self::PREFIX)) === 0;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Framework\Cache\Compression;
use App\Framework\Cache\CompressionAlgorithm;
final class NullCompression implements CompressionAlgorithm
{
public function compress(string $value, bool $forceCompression = false): string
{
return $value; // Keine Komprimierung
}
public function decompress(string $value): string
{
return $value; // Keine Dekomprimierung
}
public function isCompressed(string $value): bool
{
return false; // Nie komprimiert
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Framework\Cache;
interface CompressionAlgorithm
{
/**
* Komprimiert einen Wert. Rückgabe kann String oder Binärdaten sein.
* Gibt bei $forceCompression ein Flag an, ob immer komprimiert werden soll.
*/
public function compress(string $value, bool $forceCompression = false): string;
/**
* Dekodiert einen Wert, falls komprimiert; ansonsten unverändert zurückgeben.
*/
public function decompress(string $value): string;
/**
* Gibt true zurück, falls $value komprimiert ist (z.B. am Prefix erkennbar)
*/
public function isCompressed(string $value): bool;
}

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Framework\Cache;
final readonly class CompressionCacheDecorator implements Cache
{
public function __construct(
private Cache $innerCache,
private CompressionAlgorithm $algorithm,
private Serializer $serializer
) {}
public function get(string $key): CacheItem
{
$item = $this->innerCache->get($key);
if (!$item->isHit || !is_string($item->value)) {
return $item;
}
$value = $item->value;
if ($this->algorithm->isCompressed($value)) {
$value = $this->algorithm->decompress($value);
}
try {
// Versuche direkt zu deserialisieren.
// Schlägt dies fehl, wird der rohe (aber dekomprimierte) Wert zurückgegeben.
$unserialized = $this->serializer->unserialize($value);
return CacheItem::hit($key, $unserialized);
} catch (\Throwable $e) {
// Das ist ein erwartetes Verhalten, wenn der Wert ein einfacher String war.
// Optional: Loggen des Fehlers für Debugging-Zwecke.
// error_log("CompressionCacheDecorator: Deserialization failed for key: {$key}. Assuming raw value.");
return CacheItem::hit($key, $value);
}
//LEGACY:
/*if ($this->algorithm->isCompressed($item->value)) {
$decompressed = $this->algorithm->decompress($item->value);
// Prüfe ob der Inhalt serialisiert wurde
if ($this->isSerialized($decompressed)) {
try {
$unserialized = $this->serializer->unserialize($decompressed);
return CacheItem::hit($key, $unserialized);
} catch (\Throwable $e) {
// Fallback bei Deserialisierung-Fehler
error_log("CompressionCacheDecorator: Deserialization failed for key: {$key}, Error: " . $e->getMessage());
}
}
// Wenn nicht serialisiert oder Deserialisierung fehlgeschlagen, gib dekomprimierten String zurück
return CacheItem::hit($key, $decompressed);
}
if($this->isSerialized($item->value)) {
try {
$unserialized = $this->serializer->unserialize($item->value);
return CacheItem::hit($key, $unserialized);
} catch (\Throwable $e) {
error_log("CompressionCacheDecorator: Deserialization failed for key: {$key}, Error: " . $e->getMessage());
}
}
return $item;*/
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
if (!is_string($value)) {
$value = $this->serializer->serialize($value);
}
$compressed = $this->algorithm->compress($value);
return $this->innerCache->set($key, $compressed, $ttl);
}
public function has(string $key): bool
{
return $this->innerCache->has($key);
}
public function forget(string $key): bool
{
return $this->innerCache->forget($key);
}
public function clear(): bool
{
return $this->innerCache->clear();
}
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
{
$item = $this->get($key);
if ($item->isHit) {
return $item;
}
$value = $callback();
$this->set($key, $value, $ttl);
return CacheItem::hit($key, $value);
}
/**
* Prüft ob ein String serialisierte PHP-Daten enthält
*/
/*
private function isSerialized(string $data): bool
{
// Leere Strings oder sehr kurze Strings können nicht serialisiert sein
if (strlen($data) < 4) {
return false;
}
// Prüfe auf NULL-Wert (serialisiert als 'N;')
if ($data === 'N;') {
return true;
}
// Prüfe auf boolean false (serialisiert als 'b:0;')
if ($data === 'b:0;') {
return true;
}
// Prüfe auf boolean true (serialisiert als 'b:1;')
if ($data === 'b:1;') {
return true;
}
// Prüfe auf typische serialize() Patterns:
// a:N: (array mit N Elementen)
// s:N: (string mit N Zeichen)
// i:N; (integer mit Wert N)
// d:N; (double/float mit Wert N)
// O:N: (object mit Klassennamen der Länge N)
// b:N; (boolean mit Wert N)
// r:N; (reference)
// R:N; (reference)
if (preg_match('/^[asdObRr]:[0-9]+[:|;]/', $data)) {
return true;
}
// Prüfe auf integer Pattern: i:Zahl;
if (preg_match('/^i:-?[0-9]+;/', $data)) {
return true;
}
// Prüfe auf float Pattern: d:Zahl;
if (preg_match('/^d:-?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?;/', $data)) {
return true;
}
// Zusätzliche Validierung: Versuche tatsächlich zu deserialisieren
// aber nur bei verdächtigen Patterns, um Performance zu schonen
if (strlen($data) < 1000 && (
str_starts_with($data, 'a:') ||
str_starts_with($data, 'O:') ||
str_starts_with($data, 's:')
)) {
$test = @unserialize($data);
return $test !== false || $data === 'b:0;';
}
return false;
}
*/
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
final readonly class ApcuCache implements CacheDriver
{
public function __construct(
private string $prefix = 'cache:'
){}
private function prefixKey(string $key): string
{
return $this->prefix . $key;
}
public function get(string $key): CacheItem
{
$key = $this->prefixKey($key);
$success = false;
$value = apcu_fetch($key, $success);
if (!$success) {
return CacheItem::miss($key);
}
return CacheItem::hit($key, $value);
}
public function set(string $key, string $value, ?int $ttl = null): bool
{
$key = $this->prefixKey($key);
$ttl = $ttl ?? 0;
return apcu_store($key, $value, $ttl);
}
public function has(string $key): bool
{
$key = $this->prefixKey($key);
return apcu_exists($key);
}
public function forget(string $key): bool
{
$key = $this->prefixKey($key);
return apcu_delete($key);
}
public function clear(): bool
{
return apcu_clear_cache();
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\Storage;
final readonly class FileCache implements CacheDriver
{
private const string CACHE_PATH = __DIR__ . '/../storage/cache';
public function __construct(
private Storage $fileSystem = new FileStorage(),
) {
$this->fileSystem->createDirectory(self::CACHE_PATH);
}
private function getFileName(string $key, ?int $expiresAt): string
{
// Schütze vor Pfad/komischen Zeichen und Hash den Key
$safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $key);
$hash = md5($safeKey);
return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash .'_'. ($expiresAt ?? 0) . '.cache.php';
}
private function getFilesForKey(string $key): array
{
$hash = md5($key);
$pattern = self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '*.cache.php';
return glob($pattern) ?: [];
}
private function getLockFileName(string $key): string
{
$hash = md5($key);
return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '.lock';
}
private function withKeyLock(string $key, callable $callback)
{
$lockFile = fopen($this->getLockFileName($key), 'c');
if ($lockFile === false) {
// Optional: Fehler werfen oder Logging
return $callback(null);
}
try {
// Exklusiven Lock setzen, Wartezeit begrenzen wenn gewünscht
if (flock($lockFile, LOCK_EX)) {
return $callback($lockFile);
}
// Lock konnte nicht gesetzt werden
return $callback(null);
} finally {
flock($lockFile, LOCK_UN);
fclose($lockFile);
}
}
public function get(string $key): CacheItem
{
$bestFile = null;
$bestExpires = null;
foreach($this->getFilesForKey($key) as $file) {
if (!preg_match('/_(\d+)\.cache\.php$/', $file, $m)) {
continue;
}
$expiresAt = (int)$m[1];
if ($expiresAt > 0 && $expiresAt < time()) {
$this->fileSystem->delete($file);
continue;
}
if ($bestFile === null || $expiresAt > $bestExpires) {
$bestFile = $file;
$bestExpires = $expiresAt;
}
}
if ($bestFile === null) {
return CacheItem::miss($key);
}
$content = $this->fileSystem->get($bestFile);
$data = @unserialize($content) ?: [];
if (!isset($data['value'])) {
$this->fileSystem->delete($bestFile);
return CacheItem::miss($key);
}
return CacheItem::hit($key, $data['value']);
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
return $this->withKeyLock($key, function () use ($key, $value, $ttl) {
$expiresAt = $ttl ? (time() + $ttl) : null;
foreach ($this->getFilesForKey($key) as $file) {
$this->fileSystem->delete($file);
}
$file = $this->getFileName($key, $expiresAt);
$data = [
'value' => $value,
'expires_at' => $expiresAt,
];
$this->fileSystem->put($file, serialize($data));
return true;
});
}
public function has(string $key): bool
{
$item = $this->get($key);
return $item->isHit;
}
public function forget(string $key): bool
{
return $this->withKeyLock($key, function () use ($key) {
foreach ($this->getFilesForKey($key) as $file) {
$this->fileSystem->delete($file);
}
return true;
});
}
public function clear(): bool
{
$files = glob(self::CACHE_PATH . '*.cache.php') ?: [];
foreach ($files as $file) {
$this->fileSystem->delete($file);
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
final class InMemoryCache implements CacheDriver
{
private array $data = [];
public function get(string $key): CacheItem
{
return $this->data[$key] ?? CacheItem::miss($key);
}
public function set(string $key, string $value, ?int $ttl = null): bool
{
$this->data[$key] = $value;
return true;
}
public function has(string $key): bool
{
return isset($this->data[$key]);
}
public function forget(string $key): bool
{
unset($this->data[$key]);
return true;
}
public function clear(): bool
{
unset($this->data);
return true;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
class NullCache implements CacheDriver
{
public function get(string $key): CacheItem
{
return CacheItem::miss($key);
}
public function set(string $key, string $value, ?int $ttl = null): bool
{
return true;
}
public function has(string $key): bool
{
return false;
}
public function forget(string $key): bool
{
return true;
}
public function clear(): bool
{
return true;
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CachePrefix;
use App\Framework\Cache\Serializer;
use Predis\Client as Redis;
final readonly class RedisCache implements CacheDriver
{
private Redis $redis;
public function __construct(
string $host = '127.0.0.1',
int $port = 6379,
?string $password = null,
int $db = 0,
#private Serializer $serializer = new Serializer\PhpSerializer(),
private string $prefix = 'cache:'
)
{
$this->redis = new Redis(
parameters: [
'scheme' => 'tcp',
'timeout' => 1.0,
'read_write_timeout' => 1.0,
'host' => $host,
'port' => $port,
]
);
#$this->redis->connect();
if ($password) {
$this->redis->auth($password);
}
$this->redis->select($db);
}
private function prefixKey(string $key): string
{
return $this->prefix . $key;
}
public function get(string $key): CacheItem
{
$key = $this->prefixKey($key);
$data = $this->redis->get($key);
if ($data === null) {
return CacheItem::miss($key);
}
#$decoded = $this->serializer->unserialize($data); // oder json_decode($data, true)
$decoded = $data;
/*if (!is_array($decoded) || !array_key_exists('value', $decoded)) {
return CacheItem::miss($key);
}*/
// TODO: REMOVE TTL
$ttl = $this->redis->ttl($key);
$expiresAt = $ttl > 0 ? (time() + $ttl) : null;
return CacheItem::hit(
key : $key,
value: $decoded,
);
}
public function set(string $key, string $value, ?int $ttl = null): bool
{
$key = $this->prefixKey($key);
#$payload = $this->serializer->serialize($value); #war: ['value' => $value]
$payload = $value;
if ($ttl !== null) {
return $this->redis->setex($key, $ttl, $payload)->getPayload() === 'OK';
}
return $this->redis->set($key, $payload)->getPayload() === 'OK';
}
public function has(string $key): bool
{
$key = $this->prefixKey($key);
return $this->redis->exists($key) > 0;
}
public function forget(string $key): bool
{
$key = $this->prefixKey($key);
return $this->redis->del($key) > 0;
}
public function clear(): bool
{
try {
$patterns = array_map(
fn($prefix) => $prefix->value . '*',
CachePrefix::cases()
);
foreach($patterns as $pattern) {
$this->clearByPattern($pattern);
}
return true;
} catch (\Throwable $e) {
return false;
}
}
private function clearByPattern(string $pattern): void
{
$cursor = 0;
$batchSize = 1000; // Batch-Größe für bessere Performance
do {
$result = $this->redis->scan($cursor, [
'MATCH' => $pattern,
'COUNT' => $batchSize
]);
$cursor = $result[0];
$keys = $result[1];
if (!empty($keys)) {
$this->redis->del($keys);
}
} while ($cursor !== 0);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Framework\Cache;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\Storage;
final readonly class FileCacheCleaner
{
public function __construct(
private Storage $fileSystem = new FileStorage(),
private string $cacheFolder = __DIR__.'/storage/cache/'
) {}
/**
* Löscht alle abgelaufenen Cache-Dateien im Cache-Verzeichnis.
* Rückgabe: Anzahl gelöschter Dateien
*/
public function clean(): int
{
$now = time();
$deleted = 0;
// Alle passenden Cache-Files suchen (z.B. abcdef12345_1717000000.cache.php)
foreach ($this->fileSystem->listDirectory($this->cacheFolder, '*.cache.php') as $file) {
// Expires-At aus Dateiname extrahieren
if (preg_match('/_(\d+)\.cache\.php$/', $file, $match)) {
$expiresAt = (int)$match[1];
if ($expiresAt > 0 && $expiresAt < $now) {
if ($this->fileSystem->delete($file) === null) {
$deleted++;
}
}
}
}
return $deleted;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Framework\Cache;
final readonly class GeneralCache implements Cache
{
public function __construct(
private CacheDriver $adapter
) {}
public function get(string $key): CacheItem
{
return $this->adapter->get($key);
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
return $this->adapter->set($key, $value, $ttl);
}
public function has(string $key): bool
{
return $this->adapter->has($key);
}
public function forget(string $key): bool
{
return $this->adapter->forget($key);
}
public function clear(): bool
{
return $this->adapter->clear();
}
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
{
$item = $this->get($key);
if ($item->isHit) {
return $item;
}
$value = $callback();
$this->set($key, $value, $ttl);
return $this->get($key)->isHit ? $this->get($key) : CacheItem::hit($key, $value);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
final readonly class LoggingCacheDecorator implements Cache
{
public function __construct(
private Cache $innerCache,
) {}
public function get(string $key): CacheItem
{
$cacheItem = $this->innerCache->get($key);
$status = $cacheItem->isHit ? 'HIT' : 'MISS';
$valueType = $cacheItem->value === null ? 'NULL' : gettype($cacheItem->value);
error_log("Cache {$status}: {$key} (value: {$valueType})");
return $cacheItem;
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
$result = $this->innerCache->set($key, $value, $ttl);
$valueType = $value === null ? 'NULL' : gettype($value);
$ttlStr = $ttl === null ? 'default' : (string)$ttl;
$success = $result ? 'YES' : 'NO';
error_log("Cache SET: {$key} = {$valueType}, TTL: {$ttlStr}, Success: {$success}");
return $result;
}
public function has(string $key): bool
{
$exists = $this->innerCache->has($key);
$existsStr = $exists ? 'YES' : 'NO';
error_log("Cache HAS: {$key} = {$existsStr}");
return $exists;
}
public function forget(string $key): bool
{
$result = $this->innerCache->forget($key);
$success = $result ? 'YES' : 'NO';
error_log("Cache FORGET: {$key}, Success: {$success}");
return $result;
}
public function clear(): bool
{
$result = $this->innerCache->clear();
$success = $result ? 'YES' : 'NO';
error_log("Cache CLEAR: Success: {$success}");
return $result;
}
public function getMultiple(array $keys): array
{
$items = $this->innerCache->getMultiple($keys);
$hitCount = 0;
$missCount = 0;
foreach ($items as $item) {
if ($item->isHit) {
$hitCount++;
} else {
$missCount++;
}
}
$keyList = implode(', ', $keys);
error_log("Cache GET_MULTIPLE: [{$keyList}] - Hits: {$hitCount}, Misses: {$missCount}");
return $items;
}
public function setMultiple(array $items, ?int $ttl = null): bool
{
$result = $this->innerCache->setMultiple($items, $ttl);
$count = count($items);
$ttlStr = $ttl === null ? 'default' : (string)$ttl;
$success = $result ? 'YES' : 'NO';
error_log("Cache SET_MULTIPLE: {$count} items, TTL: {$ttlStr}, Success: {$success}");
return $result;
}
public function deleteMultiple(array $keys): bool
{
$result = $this->innerCache->deleteMultiple($keys);
$count = count($keys);
$success = $result ? 'YES' : 'NO';
error_log("Cache DELETE_MULTIPLE: {$count} keys, Success: {$success}");
return $result;
}
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
{
$result = $this->innerCache->remember($key, $callback, $ttl);
$success = $result->isHit ? 'YES' : 'NO';
error_log("Cache REMEMBER: {$key}, Success: {$success}");
return $result;
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace App\Framework\Cache;
final readonly class MultiLevelCache implements Cache
{
private const int MAX_FAST_CACHE_SIZE = 1024; //1KB
private const int FAST_CACHE_TTL = 300;
public function __construct(
private Cache $fastCache, // z.B. ArrayCache
private Cache $slowCache // z.B. RedisCache, FileCache
) {}
public function get(string $key): CacheItem
{
$item = $this->fastCache->get($key);
if ($item->isHit) {
return $item;
}
// Fallback auf "langsame" Ebene
$item = $this->slowCache->get($key);
if ($item->isHit) {
// In schnellen Cache zurücklegen (optional mit TTL aus Slow-Item)
if($this->shouldCacheInFast($item->value)) {
$this->fastCache->set($key, $item->value, self::FAST_CACHE_TTL);
}
}
return $item;
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
$slowSuccess = $this->slowCache->set($key, $value, $ttl);
$fastSuccess = true;
if($this->shouldCacheInFast($value)) {
$fastTtl = min($ttl ?? self::FAST_CACHE_TTL, self::FAST_CACHE_TTL);
$fastSuccess = $this->fastCache->set($key, $value, $fastTtl);
}
return $slowSuccess && $fastSuccess;
}
public function forget(string $key): bool
{
$s1 = $this->fastCache->forget($key);
$s2 = $this->slowCache->forget($key);
return $s1 && $s2;
}
public function clear(): bool
{
$s1 = $this->fastCache->clear();
$s2 = $this->slowCache->clear();
return $s1 && $s2;
}
/*
* - **has**: Versucht zuerst den schnelleren Layer. Im Falle eines Hits im "slowCache" wird der Wert aufgewärmt/gecached, damit zukünftige Zugriffe wieder schneller sind.
*/
public function has(string $key): bool
{
// Schneller Check im Fast-Cache
if ($this->fastCache->has($key)) {
return true;
}
// Ggf. im Slow-Cache prüfen (und dann in Fast-Cache "aufwärmen")
$slowHit = $this->slowCache->get($key);
if ($slowHit->isHit) {
if($this->shouldCacheInFast($slowHit->value)) {
$this->fastCache->set($key, $slowHit->value, self::FAST_CACHE_TTL);
}
return true;
}
return false;
}
/*
* - **remember**: Holt sich das Item per`get`(inkl. aller Multi-Level-Vorteile, wie bereits vorhanden). Wenn nicht im Cache, wird das Callback ausgeführt, gespeichert und als Treffer zurückgegeben.
*/
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
{
$item = $this->get($key);
if ($item->isHit) {
return $item;
}
// Wert generieren, speichern und zurückgeben
$value = $callback();
$this->set($key, $value, $ttl);
// Erstelle neuen CacheItem als Treffer
return CacheItem::hit($key, $value);
}
private function shouldCacheInFast(mixed $value): bool
{
// Wenn es bereits ein String ist (serialisiert), nutze dessen Länge
if (is_string($value)) {
return strlen($value) <= self::MAX_FAST_CACHE_SIZE;
}
// Für Arrays: Schnelle Heuristik ohne Serialisierung
if (is_array($value)) {
$elementCount = count($value, COUNT_RECURSIVE);
// Grobe Schätzung: 50 Bytes pro Element
$estimatedSize = $elementCount * 50;
return $estimatedSize <= self::MAX_FAST_CACHE_SIZE;
}
// Für Objekte: Konservativ - nicht in fast cache
if (is_object($value)) {
return false;
}
// Primitive Typen: Immer cachen
return true;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Framework\Cache;
interface Serializer
{
public function serialize(mixed $value): string;
public function unserialize(string $value): mixed;
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Framework\Cache\Serializer;
use App\Framework\Cache\Serializer;
use JsonException;
final readonly class JsonSerializer implements Serializer
{
/**
* @throws JsonException
*/
public function serialize(mixed $value): string
{
return json_encode($value, JSON_THROW_ON_ERROR);
}
/**
* @throws JsonException
*/
public function unserialize(string $value): mixed
{
return json_decode($value, true, 512, JSON_THROW_ON_ERROR);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Framework\Cache\Serializer;
use App\Framework\Cache\Serializer;
final readonly class PhpSerializer implements Serializer
{
public function serialize(mixed $value): string
{
return serialize($value);
}
public function unserialize(string $value): mixed
{
return unserialize($value);
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
final readonly class TaggedCache
{
public function __construct(
public Cache $cache
) {}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\CommandBus;
interface CommandBus
{
public function dispatch(object $command): mixed;
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Framework\CommandBus;
use App\Framework\Context\ExecutionContext;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Initializer;
use App\Framework\Discovery\Results\DiscoveryResults;
final readonly class CommandBusInitializer
{
public function __construct(
private DefaultContainer $container,
private DiscoveryResults $results,
private ExecutionContext $executionContext,
) {}
#[Initializer]
public function __invoke(): CommandBus
{
$this->results->get(CommandHandler::class);
$handlers = [];
foreach($this->results->get(CommandHandler::class) as $handler) {
$handlers[] = CommandHandlerDescriptor::fromHandlerArray($handler);
}
$handlersCollection = new CommandHandlersCollection(...$handlers);
return new DefaultCommandBus($handlersCollection, $this->container, $this->executionContext);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\CommandBus;
use Attribute;
#[Attribute(\Attribute::TARGET_METHOD)]
final class CommandHandler
{
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\CommandBus;
use App\Framework\Core\AttributeCompiler;
final readonly class CommandHandlerCompiler implements AttributeCompiler
{
public function getAttributeClass(): string
{
return CommandHandler::class;
}
public function compile(array $handlers): array
{
$compiledHandlers = [];
foreach ($handlers as $handler) {
$className = $handler['class'];
$methodName = $handler['method'];
// Ermittle den Command-Typ aus dem ersten Parameter der Methode
$methodInfo = new \ReflectionMethod($className, $methodName);
$parameters = $methodInfo->getParameters();
if (count($parameters) > 0) {
$paramType = $parameters[0]->getType();
if ($paramType instanceof \ReflectionNamedType) {
$commandClass = $paramType->getName();
// Speichere den Handler für diesen Command-Typ
$compiledHandlers[$commandClass] = [
'class' => $className,
'method' => $methodName,
];
}
}
}
return $compiledHandlers;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Framework\CommandBus;
/**
* Kapselt Handler-Information für einen Command
*/
final readonly class CommandHandlerDescriptor
{
public function __construct(
public string $class,
public string $method,
public string $command,
) {}
public static function fromHandlerArray(array $handler): self
{
return new self(
class: $handler['class'],
method: $handler['method'],
command: $handler['command'],
);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Framework\CommandBus;
use App\Framework\Core\AttributeMapper;
use ReflectionMethod;
use RuntimeException;
final class CommandHandlerMapper implements AttributeMapper
{
public function getAttributeClass(): string
{
return CommandHandler::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
{
if (!$reflectionTarget instanceof ReflectionMethod || !$attributeInstance instanceof CommandHandler) {
return null;
}
if(empty($reflectionTarget->getParameters()[0])) {
throw new RuntimeException('Missing Command-Type');
}
return [
'class' => $reflectionTarget->getDeclaringClass()->getName(),
'method' => $reflectionTarget->getName(),
'command' => $reflectionTarget->getParameters()[0]->getType()->getName(),
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Framework\CommandBus;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use Traversable;
final readonly class CommandHandlersCollection implements IteratorAggregate, Countable
{
private array $handlers;
public function __construct(
CommandHandlerDescriptor ...$handlers
#private array $handlers = []
){
$handlerArray = [];
foreach ($handlers as $handler) {
$handlerArray[$handler->command] = $handler;
}
$this->handlers = $handlerArray;
}
/*private function add(string $messageClass, object $handler): void
{
$this->handlers[$messageClass] = $handler;
}*/
/**
* @param class-string $commandClass
* @return CommandHandlerDescriptor|null
*/
public function get(string $commandClass): ?CommandHandlerDescriptor
{
return $this->handlers[$commandClass] ?? null;
}
public function all(): array
{
return $this->toArray();
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->handlers);
}
public function count(): int
{
return count($this->handlers);
}
public function toArray(): array
{
return $this->handlers;
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Framework\CommandBus;
use App\Application\Auth\LoginUser;
use App\Framework\CommandBus\Exceptions\NoHandlerFound;
use App\Framework\CommandBus\Middleware\DatabaseTransactionMiddleware;
use App\Framework\CommandBus\Middleware\PerformanceMonitoringMiddleware;
use App\Framework\Context\ExecutionContext;
use App\Framework\Context\ContextType;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\Queue\FileQueue;
use App\Framework\Queue\Queue;
use App\Framework\Queue\RedisQueue;
final readonly class DefaultCommandBus implements CommandBus
{
public function __construct(
private CommandHandlersCollection $commandHandlers,
private Container $container,
private ExecutionContext $executionContext,
private Queue $queue = new RedisQueue(host: 'redis'),
private array $middlewares = [
PerformanceMonitoringMiddleware::class,
DatabaseTransactionMiddleware::class,
],
) {}
public function dispatch(object $command): mixed
{
$contextType = $this->executionContext->getType();
error_log("CommandBus Debug: Execution context: {$contextType->value}");
error_log("CommandBus Debug: Context metadata: " . json_encode($this->executionContext->getMetadata()));
// Context-basierte Queue-Entscheidung
if ($this->shouldQueueInContext($command, $contextType)) {
error_log("CommandBus Debug: Job wird in Queue eingereiht: " . $command::class);
$this->queue->push($command);
return null;
}
error_log("CommandBus Debug: Job wird direkt ausgeführt: " . $command::class);
return $this->executeCommand($command);
}
private function executeCommand(object $command): mixed
{
$handlerExecutor = function (object $command): mixed {
$handler = $this->commandHandlers->get($command::class);
if($handler === null)
{
throw NoHandlerFound::forCommand($command::class);
}
// $handler = $this->commandHandlers[get_class($command)];
$handlerClass = $this->container->get($handler->class);
return $handlerClass->{$handler->method}($command);
};
$pipeline = array_reduce(
array_reverse($this->middlewares),
function (callable $next, string $middlewareClass): callable {
return function (object $command) use ($middlewareClass, $next): mixed {
/** @var Middleware $middleware */
$middleware = $this->container->get($middlewareClass);
return $middleware->handle($command, $next);
};
},
$handlerExecutor
);
return $pipeline($command);
}
private function shouldQueueInContext(object $command, ContextType $context): bool
{
$ref = new \ReflectionClass($command);
$hasQueueAttribute = !empty($ref->getAttributes(ShouldQueue::class));
if (!$hasQueueAttribute) {
return false; // Ohne #[ShouldQueue] Attribut nie queuen
}
// Context-basierte Entscheidung
return match($context) {
ContextType::WORKER => false, // Worker: NIEMALS queuen
ContextType::CONSOLE => false, // Artisan: meist direkt
ContextType::TEST => false, // Tests: immer direkt
ContextType::CLI_SCRIPT => true, // CLI Scripts: normal queuen
ContextType::WEB => true, // Web: Standard Queue-Verhalten
};
}
public function __debugInfo(): ?array
{
return $this->commandHandlers->toArray();
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\CommandBus\Exceptions;
use App\Framework\Exception\FrameworkException;
final class NoHandlerFound extends FrameworkException
{
public static function forCommand(string $commandClass): self
{
return new self(
"No handler found for command: $commandClass"
);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Framework\CommandBus;
interface Middleware
{
public function handle(object $command, callable $next): mixed;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Framework\CommandBus\Middleware;
use App\Framework\CommandBus\Middleware;
final readonly class DatabaseTransactionMiddleware implements Middleware
{
public function __construct() {}
public function handle(object $command, callable $next): mixed
{
#var_dump('DatabaseTransactionMiddleware');
// Wenn die Validierung erfolgreich ist, wird der nächste Schritt aufgerufen
return $next($command);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Framework\CommandBus\Middleware;
use App\Framework\CommandBus\Middleware;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\Logger;
final readonly class LoggingMiddleware implements Middleware
{
public function __construct(
private Logger $logger
) {}
public function handle(object $command, callable $next): mixed
{
$commandClass = get_class($command);
$this->logger->info("Starting command: $commandClass");
$result = $next($command);
$this->logger->info("Finished command: $commandClass");
return $result;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Framework\CommandBus\Middleware;
use App\Framework\CommandBus\Middleware;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceMeter;
final readonly class PerformanceMonitoringMiddleware implements Middleware
{
public function __construct(
private PerformanceMeter $meter
) {}
public function handle(object $command, callable $next): mixed
{
$this->meter->startMeasure($command::class, PerformanceCategory::SYSTEM);
$result = $next($command);
$this->meter->endMeasure($command::class);
return $result;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Framework\CommandBus;
#[\Attribute(\Attribute::TARGET_CLASS)]
class ShouldQueue
{
// Hier könnten noch Optionen rein (z.B. Queue-Name, Delay …)
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Framework\Config;
use App\Framework\DateTime\Timezone;
final readonly class AppConfig
{
public function __construct(
public string $name = 'Framework App',
public string $version = '1.0.0',
public string $environment = 'production',
public bool $debug = false,
public Timezone $timezone = Timezone::DEFAULT,
public string $locale = 'en',
public EnvironmentType $type = EnvironmentType::DEV
) {}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
class Configuration
{
private array $config = [];
/**
* Lädt Konfigurationen aus einer oder mehreren Quellen
*/
public function __construct(array $config = [])
{
$this->config = $config;
}
/**
* Lädt Konfigurationen aus PHP-Dateien
*/
public function loadFromFile(string $path): self
{
if (file_exists($path) && is_readable($path)) {
$fileConfig = require $path;
if (is_array($fileConfig)) {
$this->config = array_merge($this->config, $fileConfig);
}
}
return $this;
}
/**
* Lädt Konfigurationen aus einem Verzeichnis
*/
public function loadFromDirectory(string $directory): self
{
if (!is_dir($directory)) {
return $this;
}
foreach (glob($directory . '/*.php') as $file) {
$this->loadFromFile($file);
}
return $this;
}
/**
* Gibt einen Konfigurationswert zurück
*/
public function get(string $key, mixed $default = null): mixed
{
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $segment) {
if (!is_array($value) || !array_key_exists($segment, $value)) {
return $default;
}
$value = $value[$segment];
}
return $value;
}
/**
* Setzt einen Konfigurationswert
*/
public function set(string $key, mixed $value): self
{
$keys = explode('.', $key);
$config = &$this->config;
foreach ($keys as $i => $segment) {
if ($i === count($keys) - 1) {
$config[$segment] = $value;
break;
}
if (!isset($config[$segment]) || !is_array($config[$segment])) {
$config[$segment] = [];
}
$config = &$config[$segment];
}
return $this;
}
/**
* Prüft, ob ein Konfigurationswert existiert
*/
public function has(string $key): bool
{
$keys = explode('.', $key);
$config = $this->config;
foreach ($keys as $segment) {
if (!is_array($config) || !array_key_exists($segment, $config)) {
return false;
}
$config = $config[$segment];
}
return true;
}
/**
* Gibt alle Konfigurationswerte zurück
*/
public function all(): array
{
return $this->config;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Framework\Config;
enum EnvKey: string
{
case APP_DEBUG = 'APP_DEBUG';
case APP_ENV = 'APP_ENV';
case APP_TIMEZONE = 'APP_TIMEZONE';
case APP_LOCALE = 'APP_LOCALE';
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
final readonly class Environment
{
public function __construct(
private array $variables = []
){}
public function get(EnvKey|string $key, mixed $default = null): mixed
{
$key = $this->keyToString($key);
// Priorität: 1. System ENV, 2. Loaded variables, 3. Default
return $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?: $this->variables[$key] ?? $default;
}
public function getRequired(EnvKey|string $key): mixed
{
$key = $this->keyToString($key);
$value = $this->get($key);
if ($value === null) {
throw new RequiredEnvironmentVariableException($key);
}
return $value;
}
public function getInt(EnvKey|string $key, int $default = 0): int
{
$key = $this->keyToString($key);
return (int) $this->get($key, $default);
}
public function getBool(EnvKey|string $key, bool $default = false): bool
{
$key = $this->keyToString($key);
$value = $this->get($key, $default);
if (is_string($value)) {
return match (strtolower($value)) {
'true', '1', 'yes', 'on' => true,
'false', '0', 'no', 'off' => false,
default => $default
};
}
return (bool) $value;
}
public function getString(EnvKey|string $key, string $default = ''): string
{
$key = $this->keyToString($key);
return (string) $this->get($key, $default);
}
public function getEnum(EnvKey|string $key, string $enumClass, \BackedEnum $default):object
{
$key = $this->keyToString($key);
return forward_static_call([$enumClass, 'tryFrom'], $this->get($key, $default));
#$enumClass::tryFrom($this->get($key, $default));
}
public function has(EnvKey|string $key): bool
{
$key = $this->keyToString($key);
return $this->get($key) !== null;
}
public function all(): array
{
return array_merge(
$_ENV,
$_SERVER,
$this->variables
);
}
/**
* Factory method für .env file loading
*/
public static function fromFile(string $envPath): self
{
$variables = [];
if (file_exists($envPath) && is_readable($envPath)) {
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) {
continue;
}
[$name, $value] = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
// Remove quotes
if (str_starts_with($value, '"') && str_ends_with($value, '"')) {
$value = substr($value, 1, -1);
}
$variables[$name] = self::castValue($value);
}
}
return new self($variables);
}
private static function castValue(string $value): mixed
{
return match (strtolower($value)) {
'true' => true,
'false' => false,
'null' => null,
default => is_numeric($value) ? (str_contains($value, '.') ? (float) $value : (int) $value) : $value
};
}
public function keyToString(EnvKey|string $key):string
{
if(is_string($key)) {
return $key;
}
return $key->value;
}
/**
* Für Tests
*/
public function withVariable(string $key, mixed $value): self
{
$variables = $this->variables;
$variables[$key] = $value;
return new self($variables);
}
public function withVariables(array $variables): self
{
return new self(array_merge($this->variables, $variables));
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Framework\Config;
enum EnvironmentType: string
{
case DEV = 'development';
case PROD = 'production';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Framework\Config\Exceptions;
use App\Framework\Exception\FrameworkException;
class RequiredEnvironmentVariableException extends FrameworkException
{
/**
* @param string $key
*/
public function __construct(string $key)
{
parent::__construct("Required environment variable '$key' is not set.");
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Framework\Config;
use App\Framework\Context\ContextType;
use App\Framework\DateTime\Timezone;
use App\Framework\DI\Container;
final readonly class TypedConfigInitializer
{
public function __construct(
private Environment $env,
){
#debug($env->getEnum('APP_ENV', EnvironmentType::class, EnvironmentType::PROD));
}
public function __invoke(Container $container): TypedConfiguration
{
$appConfig = $this->createAppConfig();
$container->instance(AppConfig::class, $appConfig);
return new TypedConfiguration(
app: $appConfig,
);
}
private function createAppConfig(): AppConfig
{
return new AppConfig(
environment: $this->env->getString(
key: EnvKey::APP_ENV,
default: 'production'
),
debug: $this->env->getBool(EnvKey::APP_DEBUG),
timezone: $this->env->getEnum(
key: EnvKey::APP_TIMEZONE,
enumClass: Timezone::class,
default: Timezone::EuropeBerlin
),
locale: $this->env->getString(EnvKey::APP_LOCALE, 'de'),
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Framework\Config;
use App\Framework\Database\Config\DatabaseConfig;
final readonly class TypedConfiguration
{
public function __construct(
#public DatabaseConfig $database,
public AppConfig $app,
)
{
}
}

View File

@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutput;
/**
* Klasse für interaktive Menüs in der Konsole.
* Unterstützt sowohl einfache nummerierte Menüs als auch
* Menüs mit Pfeiltasten-Navigation.
*/
final class InteractiveMenu
{
private ConsoleOutput $output;
private array $menuItems = [];
private string $title = '';
private int $selectedIndex = 0;
private bool $showNumbers = true;
public function __construct(ConsoleOutput $output)
{
$this->output = $output;
}
/**
* Setzt den Titel des Menüs.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Menüpunkt hinzu.
*
* @param string $label Die Beschriftung des Menüpunkts
* @param callable|null $action Eine optionale Aktion, die bei Auswahl ausgeführt wird
* @param mixed $value Ein optionaler Wert, der zurückgegeben wird
*/
public function addItem(string $label, ?callable $action = null, mixed $value = null): self
{
$this->menuItems[] = [
'label' => $label,
'action' => $action,
'value' => $value ?? $label
];
return $this;
}
/**
* Fügt einen Separator zum Menü hinzu.
*/
public function addSeparator(): self
{
$this->menuItems[] = [
'label' => '---',
'action' => null,
'value' => null,
'separator' => true
];
return $this;
}
/**
* Steuert, ob Nummern angezeigt werden sollen.
*/
public function showNumbers(bool $show = true): self
{
$this->showNumbers = $show;
return $this;
}
/**
* Zeigt ein einfaches Menü mit Nummern-Auswahl.
*/
public function showSimple(): mixed
{
// Verwende den ScreenManager anstelle des direkten clearScreen()
$this->output->screen()->newMenu();
if ($this->title) {
$this->output->writeLine($this->title, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('=', strlen($this->title)));
$this->output->writeLine('');
}
foreach ($this->menuItems as $index => $item) {
if (isset($item['separator'])) {
$this->output->writeLine($item['label'], ConsoleColor::GRAY);
continue;
}
$number = $this->showNumbers ? ($index + 1) . '. ' : '';
$this->output->writeLine($number . $item['label']);
}
$this->output->writeLine('');
$this->output->write('Ihre Wahl: ', ConsoleColor::BRIGHT_CYAN);
$input = trim(fgets(STDIN));
if (is_numeric($input)) {
$selectedIndex = (int)$input - 1;
if (isset($this->menuItems[$selectedIndex]) && !isset($this->menuItems[$selectedIndex]['separator'])) {
$item = $this->menuItems[$selectedIndex];
if ($item['action']) {
return $item['action']();
}
return $item['value'];
}
}
$this->output->writeError('Ungültige Auswahl!');
return $this->showSimple();
}
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation.
*/
public function showInteractive(): mixed
{
$this->output->screen()->setInteractiveMode();
// Terminal in Raw-Modus setzen für Tastatur-Input
$this->setRawMode(true);
try {
while (true) {
$this->renderMenu();
$key = $this->readKey();
switch ($key) {
case "\033[A": // Pfeil hoch
$this->moveUp();
break;
case "\033[B": // Pfeil runter
$this->moveDown();
break;
case "\n": // Enter
case "\r":
return $this->selectCurrentItem();
case "\033": // ESC
return null;
case 'q':
case 'Q':
return null;
}
}
} finally {
$this->setRawMode(false);
}
}
/**
* Rendert das Menü auf dem Bildschirm.
*/
private function renderMenu(): void
{
// Verwende den ScreenManager anstelle des direkten clearScreen()
$this->output->screen()->newMenu();
if ($this->title) {
$this->output->writeLine($this->title, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine(str_repeat('=', strlen($this->title)));
$this->output->writeLine('');
}
foreach ($this->menuItems as $index => $item) {
if (isset($item['separator'])) {
$this->output->writeLine($item['label'], ConsoleColor::GRAY);
continue;
}
$isSelected = ($index === $this->selectedIndex);
$prefix = $isSelected ? '► ' : ' ';
$color = $isSelected ? ConsoleColor::BRIGHT_YELLOW : null;
$number = $this->showNumbers ? ($index + 1) . '. ' : '';
$this->output->writeLine($prefix . $number . $item['label'], $color);
}
$this->output->writeLine('');
$this->output->writeLine('Verwenden Sie ↑/↓ zum Navigieren, Enter zum Auswählen, ESC/Q zum Beenden', ConsoleColor::GRAY);
}
/**
* Bewegt die Auswahl nach oben.
*/
private function moveUp(): void
{
do {
$this->selectedIndex = ($this->selectedIndex - 1 + count($this->menuItems)) % count($this->menuItems);
} while (isset($this->menuItems[$this->selectedIndex]['separator']) && count($this->menuItems) > 1);
}
/**
* Bewegt die Auswahl nach unten.
*/
private function moveDown(): void
{
do {
$this->selectedIndex = ($this->selectedIndex + 1) % count($this->menuItems);
} while (isset($this->menuItems[$this->selectedIndex]['separator']) && count($this->menuItems) > 1);
}
/**
* Wählt den aktuellen Menüpunkt aus.
*/
private function selectCurrentItem(): mixed
{
$item = $this->menuItems[$this->selectedIndex];
if ($item['action']) {
return $item['action']();
}
return $item['value'];
}
/**
* Liest einen Tastendruck.
*/
private function readKey(): string
{
$key = fgetc(STDIN);
// Behandle Escape-Sequenzen
if ($key === "\033") {
$key .= fgetc(STDIN);
if ($key === "\033[") {
$key .= fgetc(STDIN);
}
}
return $key;
}
/**
* Setzt den Terminal-Modus.
*/
private function setRawMode(bool $enable): void
{
if ($enable) {
system('stty -echo -icanon min 1 time 0');
} else {
system('stty echo icanon');
}
}
// Die clearScreen-Methode wurde entfernt, da sie durch den ScreenManager ersetzt wurde.
// Verwende stattdessen $this->output->screen()->newScreen() oder $this->output->display()->clear().
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
/**
* Rendert eine Tabelle in der Konsole.
*/
final class Table
{
private array $headers = [];
private array $rows = [];
private array $columnWidths = [];
private int $padding = 1;
public function __construct(
private ?ConsoleStyle $headerStyle = null,
private ?ConsoleStyle $rowStyle = null,
private ?ConsoleStyle $borderStyle = null,
private bool $showBorders = true
) {
$this->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD);
$this->rowStyle ??= ConsoleStyle::create();
$this->borderStyle ??= ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt die Spaltenüberschriften der Tabelle.
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
$this->calculateColumnWidths();
return $this;
}
/**
* Fügt eine Zeile zur Tabelle hinzu.
*/
public function addRow(array $row): self
{
$this->rows[] = $row;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt alle Zeilen der Tabelle.
*/
public function setRows(array $rows): self
{
$this->rows = $rows;
$this->calculateColumnWidths();
return $this;
}
/**
* Setzt den Innenabstand der Zellen.
*/
public function setPadding(int $padding): self
{
$this->padding = max(0, $padding);
return $this;
}
/**
* Rendert die Tabelle und gibt den Text zurück.
*/
public function render(): string
{
if (empty($this->headers) && empty($this->rows)) {
return '';
}
$output = '';
if ($this->showBorders) {
$output .= $this->renderBorder('top') . "\n";
}
// Header
if (!empty($this->headers)) {
$output .= $this->renderRow($this->headers, $this->headerStyle) . "\n";
if ($this->showBorders) {
$output .= $this->renderBorder('middle') . "\n";
}
}
// Zeilen
foreach ($this->rows as $index => $row) {
$output .= $this->renderRow($row, $this->rowStyle) . "\n";
}
if ($this->showBorders) {
$output .= $this->renderBorder('bottom') . "\n";
}
return $output;
}
/**
* Rendert eine Zeile der Tabelle.
*/
private function renderRow(array $cells, ConsoleStyle $style): string
{
$output = '';
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
foreach ($cells as $index => $cell) {
$width = $this->columnWidths[$index] ?? 10;
$cellText = (string)$cell;
// Linksbündige Ausrichtung mit exakter Breite
$paddedCell = str_pad($cellText, $width, ' ', STR_PAD_RIGHT);
// Padding hinzufügen
$padding = str_repeat(' ', $this->padding);
$finalCell = $padding . $paddedCell . $padding;
$output .= $style->apply($finalCell);
if ($this->showBorders) {
$output .= $this->borderStyle->apply('│');
}
}
return $output;
}
/**
* Rendert eine Trennlinie der Tabelle.
*/
private function renderBorder(string $type): string
{
$left = match($type) {
'top' => '┌',
'middle' => '├',
'row' => '├',
'bottom' => '└',
default => '│'
};
$right = match($type) {
'top' => '┐',
'middle' => '┤',
'row' => '┤',
'bottom' => '┘',
default => '│'
};
$horizontal = '─';
$junction = match($type) {
'top' => '┬',
'middle' => '┼',
'row' => '┼',
'bottom' => '┴',
default => '│'
};
$border = $left;
foreach ($this->columnWidths as $index => $width) {
$cellWidth = $width + ($this->padding * 2);
$border .= str_repeat($horizontal, $cellWidth);
if ($index < count($this->columnWidths) - 1) {
$border .= $junction;
}
}
$border .= $right;
return $this->borderStyle->apply($border);
}
/**
* Berechnet die Breite jeder Spalte basierend auf dem Inhalt.
*/
private function calculateColumnWidths(): void
{
$this->columnWidths = [];
// Header-Breiten
foreach ($this->headers as $index => $header) {
$this->columnWidths[$index] = mb_strlen((string)$header);
}
// Zeilen-Breiten
foreach ($this->rows as $row) {
foreach ($row as $index => $cell) {
$length = mb_strlen((string)$cell);
$this->columnWidths[$index] = max($this->columnWidths[$index] ?? 0, $length);
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
/**
* Rendert eine Textbox in der Konsole.
*/
final readonly class TextBox
{
public function __construct(
private string $content,
private int $width = 80,
private int $padding = 1,
private ?ConsoleStyle $borderStyle = null,
private ?ConsoleStyle $contentStyle = null,
private ?string $title = null
) {}
public function render(): string
{
$borderStyle = $this->borderStyle ?? ConsoleStyle::create(color: ConsoleColor::GRAY);
$contentStyle = $this->contentStyle ?? ConsoleStyle::create();
$contentWidth = $this->width - ($this->padding * 2) - 2;
$lines = $this->wrapText($this->content, $contentWidth);
$output = '';
// Obere Linie
if ($this->title) {
$titleLength = mb_strlen($this->title);
$availableSpace = $this->width - $titleLength - 6; // Abzug für '[ ]' und Ränder
$leftPadding = max(0, (int)floor($availableSpace / 2));
$rightPadding = max(0, $availableSpace - $leftPadding);
$output .= $borderStyle->apply('┌' . str_repeat('─', $leftPadding) . '[ ' . $this->title . ' ]' . str_repeat('─', $rightPadding) . '┐') . "\n";
} else {
$output .= $borderStyle->apply('┌' . str_repeat('─', $this->width - 2) . '┐') . "\n";
}
// Padding oben
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Inhalt
foreach ($lines as $line) {
$lineLength = mb_strlen($line);
$spaces = $contentWidth - $lineLength;
$paddedLine = $line . str_repeat(' ', $spaces);
$output .= $borderStyle->apply('│') .
str_repeat(' ', $this->padding) .
$contentStyle->apply($paddedLine) .
str_repeat(' ', $this->padding) .
$borderStyle->apply('│') . "\n";
}
// Padding unten
for ($i = 0; $i < $this->padding; $i++) {
$output .= $borderStyle->apply('│') . str_repeat(' ', $this->width - 2) . $borderStyle->apply('│') . "\n";
}
// Untere Linie
$output .= $borderStyle->apply('└' . str_repeat('─', $this->width - 2) . '┘') . "\n";
return $output;
}
private function wrapText(string $text, int $width): array
{
$lines = explode("\n", $text);
$wrapped = [];
foreach ($lines as $line) {
if (mb_strlen($line) <= $width) {
$wrapped[] = $line;
} else {
$wrapped = array_merge($wrapped, $this->splitTextIntoLines($line, $width));
}
}
// Leere Zeile hinzufügen, falls keine Inhalte vorhanden
if (empty($wrapped)) {
$wrapped[] = '';
}
return $wrapped;
}
private function splitTextIntoLines(string $text, int $width): array
{
$lines = [];
$words = explode(' ', $text);
$currentLine = '';
foreach ($words as $word) {
$testLine = empty($currentLine) ? $word : $currentLine . ' ' . $word;
if (mb_strlen($testLine) <= $width) {
$currentLine = $testLine;
} else {
if (!empty($currentLine)) {
$lines[] = $currentLine;
$currentLine = $word;
} else {
// Wort ist länger als die Zeile - hart umbrechen
$lines[] = mb_substr($word, 0, $width);
$currentLine = mb_substr($word, $width);
}
}
}
if (!empty($currentLine)) {
$lines[] = $currentLine;
}
return $lines ?: [''];
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Components;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleOutput;
/**
* TreeHelper zum Anzeigen hierarchischer Baumstrukturen in der Konsole.
* Ähnlich dem Symfony TreeHelper, aber angepasst an unser Styling-System.
*/
final class TreeHelper
{
private string $prefix = '';
private bool $isLastElement = true;
private ?ConsoleStyle $nodeStyle = null;
private ?ConsoleStyle $leafStyle = null;
private ?ConsoleStyle $lineStyle = null;
/**
* @var array<array{title: string, node: ?self, isLeaf: bool}>
*/
private array $nodes = [];
public function __construct(
private string $title = '',
private ConsoleOutput $output = new ConsoleOutput(),
) {
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
$this->lineStyle = ConsoleStyle::create(color: ConsoleColor::GRAY);
}
/**
* Setzt den Stil für Knotentitel (Verzeichnisse/Kategorien).
*/
public function setNodeStyle(?ConsoleStyle $style): self
{
$this->nodeStyle = $style;
return $this;
}
/**
* Setzt den Stil für Blätter/Endpunkte.
*/
public function setLeafStyle(?ConsoleStyle $style): self
{
$this->leafStyle = $style;
return $this;
}
/**
* Setzt den Stil für Baumlinien.
*/
public function setLineStyle(?ConsoleStyle $style): self
{
$this->lineStyle = $style;
return $this;
}
/**
* Setzt den Haupttitel des Baums.
*/
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
/**
* Fügt einen Unterknoten (z.B. Unterverzeichnis) hinzu.
*/
public function addNode(string $title): self
{
$node = new self($title);
$node->nodeStyle = $this->nodeStyle;
$node->leafStyle = $this->leafStyle;
$node->lineStyle = $this->lineStyle;
$this->nodes[] = [
'title' => $title,
'node' => $node,
'isLeaf' => false
];
return $node;
}
/**
* Fügt einen Endpunkt (z.B. Datei) hinzu.
*/
public function addLeaf(string $title): self
{
$this->nodes[] = [
'title' => $title,
'node' => null,
'isLeaf' => true
];
return $this;
}
/**
* Zeigt die vollständige Baumstruktur an.
*/
public function display(): void
{
if (!empty($this->title)) {
$this->output->writeLine($this->title, $this->nodeStyle);
}
$this->displayTree();
}
/**
* Rendert die Baumstruktur und gibt den Text zurück.
*/
public function render(): string
{
$output = '';
if (!empty($this->title)) {
$output .= $this->nodeStyle->apply($this->title) . "\n";
}
$output .= $this->renderTree();
return $output;
}
/**
* Setzt den Präfix für die aktuelle Ebene.
* (Interne Methode für rekursives Rendern)
*/
private function setPrefix(string $prefix, bool $isLastElement): self
{
$this->prefix = $prefix;
$this->isLastElement = $isLastElement;
return $this;
}
/**
* Zeigt die Baumstruktur mit dem aktuellen Präfix an.
* (Interne Methode für rekursives Rendern)
*/
private function displayTree(): void
{
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel anzeigen
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $linePrefix . $item['title'];
$this->output->writeLine(
$this->lineStyle->apply($linePrefix) .
$style->apply($item['title'])
);
// Unterelemente rekursiv anzeigen
if (!$item['isLeaf'] && $item['node'] !== null) {
$item['node']
->setPrefix($nodePrefix, $isLast)
->displayTree();
}
}
}
/**
* Rendert die Baumstruktur mit dem aktuellen Präfix und gibt den Text zurück.
* (Interne Methode für rekursives Rendern)
*/
private function renderTree(): string
{
$output = '';
$count = count($this->nodes);
foreach ($this->nodes as $index => $item) {
$isLast = ($index === $count - 1);
$nodePrefix = $this->prefix . ($this->isLastElement ? ' ' : '│ ');
// Baumlinien und Verbindungen
$connector = $isLast ? '└── ' : '├── ';
$linePrefix = $this->prefix . $connector;
// Titel formatieren
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
$title = $item['title'];
$output .= $this->lineStyle->apply($linePrefix) .
$style->apply($title) . "\n";
// Unterelemente rekursiv rendern
if (!$item['isLeaf'] && $item['node'] !== null) {
$childOutput = $item['node']
->setPrefix($nodePrefix, $isLast)
->renderTree();
$output .= $childOutput;
}
}
return $output;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
use App\Framework\Console\Exceptions\CommandNotFoundException;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryResults;
use ReflectionClass;
use ReflectionMethod;
use Throwable;
final class ConsoleApplication
{
private array $commands = [];
private ConsoleOutputInterface $output;
public function __construct(
private readonly Container $container,
private readonly string $scriptName = 'console',
private readonly string $title = 'Console Application',
?ConsoleOutputInterface $output = null,
) {
$this->output = $output ?? new ConsoleOutput();
// Setze den Fenstertitel
$this->output->writeWindowTitle($this->title);
$results = $this->container->get(DiscoveryResults::class);
foreach($results->get(ConsoleCommand::class) as $command) {
$this->commands[$command['attribute_data']['name']] = [
'instance' => $this->container->get($command['class']),
'method' => $command['method'],
'description' => $command['attribute_data']['description'] ?? ['Keine Beschreibung verfügbar'],
'reflection' => new ReflectionMethod($command['class'], $command['method'])
];
}
}
/**
* Registriert alle Kommandos aus einer Klasse
*/
public function registerCommands(object $commandClass): void
{
$reflection = new ReflectionClass($commandClass);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$attributes = $method->getAttributes(ConsoleCommand::class);
foreach ($attributes as $attribute) {
/** @var ConsoleCommand $command */
$command = $attribute->newInstance();
$this->commands[$command->name] = [
'instance' => $commandClass,
'method' => $method->getName(),
'description' => $command->description,
'reflection' => $method
];
}
}
}
/**
* Führt ein Kommando aus
*/
public function run(array $argv): int
{
if (count($argv) < 2) {
$this->showHelp();
return ExitCode::SUCCESS->value;
}
$commandName = $argv[1];
$arguments = array_slice($argv, 2);
if (in_array($commandName, ['help', '--help', '-h'])) {
$this->showHelp();
return ExitCode::SUCCESS->value;
}
if (!isset($this->commands[$commandName])) {
$this->output->writeError("Kommando '{$commandName}' nicht gefunden.");
$this->showHelp();
return ExitCode::COMMAND_NOT_FOUND->value;
}
return $this->executeCommand($commandName, $arguments)->value;
}
private function executeCommand(string $commandName, array $arguments): ExitCode
{
$command = $this->commands[$commandName];
$instance = $command['instance'];
$method = $command['method'];
try {
// Setze den Fenstertitel für das aktuelle Kommando
$this->output->writeWindowTitle("{$this->scriptName} - {$commandName}");
// Erstelle ConsoleInput
$input = new ConsoleInput($arguments, $this->output);
// Führe das Kommando aus
$result = $instance->$method($input, $this->output);
// Behandle verschiedene Rückgabetypen
if ($result instanceof ExitCode) {
return $result;
}
if (is_int($result)) {
return ExitCode::from($result);
}
// Standardmäßig Erfolg, wenn nichts anderes zurückgegeben wird
return ExitCode::SUCCESS;
} catch (CommandNotFoundException $e) {
$this->output->writeError("Kommando nicht gefunden: " . $e->getMessage());
return ExitCode::COMMAND_NOT_FOUND;
} catch (Throwable $e) {
$this->output->writeError("Fehler beim Ausführen des Kommandos: " . $e->getMessage());
// Erweiterte Fehlerbehandlung basierend auf Exception-Typ
if ($e instanceof \InvalidArgumentException) {
return ExitCode::INVALID_INPUT;
}
if ($e instanceof \RuntimeException) {
return ExitCode::SOFTWARE_ERROR;
}
return ExitCode::GENERAL_ERROR;
}
}
private function showHelp(): void
{
$this->output->writeLine("Verfügbare Kommandos:", ConsoleColor::BRIGHT_CYAN);
$this->output->newLine();
$menu = new InteractiveMenu($this->output);
$menu->setTitle("Kommandos");
foreach ($this->commands as $name => $command) {
$description = $command['description'] ?: 'Keine Beschreibung verfügbar';
$menu->addItem($name, function () use ($name) {
return $this->executeCommand($name, [])->value;
}, $description);
#$this->output->writeLine(sprintf(" %-20s %s", $name, $description));
}
$this->output->writeLine(" " . $menu->showInteractive());
$this->output->newLine();
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente]");
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Enum für Konsolen-Farben mit ANSI-Escape-Codes.
*/
enum ConsoleColor: string
{
case RESET = "0";
// Einfache Textfarben
case BLACK = "30";
case RED = "31";
case GREEN = "32";
case YELLOW = "33";
case BLUE = "34";
case MAGENTA = "35";
case CYAN = "36";
case WHITE = "37";
case GRAY = "90";
// Helle Textfarben
case BRIGHT_RED = "91";
case BRIGHT_GREEN = "92";
case BRIGHT_YELLOW = "93";
case BRIGHT_BLUE = "94";
case BRIGHT_MAGENTA = "95";
case BRIGHT_CYAN = "96";
case BRIGHT_WHITE = "97";
// Hintergrundfarben
case BG_BLACK = "40";
case BG_RED = "41";
case BG_GREEN = "42";
case BG_YELLOW = "43";
case BG_BLUE = "44";
case BG_MAGENTA = "45";
case BG_CYAN = "46";
case BG_WHITE = "47";
// Kombinierte Farben
case WHITE_ON_RED = "97;41";
case BLACK_ON_YELLOW = "30;43";
public function toAnsi(): string
{
return "\033[{$this->value}m";
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use Attribute;
#[Attribute(\Attribute::TARGET_METHOD)]
final readonly class ConsoleCommand
{
public function __construct(
public string $name,
public string $description = ''
) {}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Core\AttributeMapper;
final readonly class ConsoleCommandMapper implements AttributeMapper
{
public function getAttributeClass(): string
{
return ConsoleCommand::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
{
return [
'name' => $attributeInstance->name,
'description' => $attributeInstance->description,
'class' => $reflectionTarget->getDeclaringClass()->getName(),
'method' => $reflectionTarget->getName(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Framework\Console;
enum ConsoleFormat: string
{
case RESET = "0";
case BOLD = "1";
case DIM = "2";
case ITALIC = "3";
case UNDERLINE = "4";
case BLINK = "5";
case REVERSE = "7";
case STRIKETHROUGH = "9";
public function toAnsi(): string
{
return "\033[{$this->value}m";
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
final class ConsoleInput implements ConsoleInputInterface
{
private array $arguments;
private array $options = [];
private ?ConsoleOutputInterface $output = null;
public function __construct(array $arguments, ?ConsoleOutputInterface $output = null)
{
$this->parseArguments($arguments);
$this->output = $output ?? new ConsoleOutput();
}
private function parseArguments(array $arguments): void
{
$this->arguments = [];
foreach ($arguments as $arg) {
if (str_starts_with($arg, '--')) {
// Long option: --key=value oder --key
$parts = explode('=', substr($arg, 2), 2);
$this->options[$parts[0]] = $parts[1] ?? true;
} elseif (str_starts_with($arg, '-')) {
// Short option: -k value oder -k
$key = substr($arg, 1);
$this->options[$key] = true;
} else {
// Argument
$this->arguments[] = $arg;
}
}
}
public function getArgument(int $index, ?string $default = null): ?string
{
return $this->arguments[$index] ?? $default;
}
public function getArguments(): array
{
return $this->arguments;
}
public function getOption(string $name, mixed $default = null): mixed
{
return $this->options[$name] ?? $default;
}
public function hasOption(string $name): bool
{
return isset($this->options[$name]);
}
public function getOptions(): array
{
return $this->options;
}
/**
* Fragt nach einer Benutzereingabe.
*/
public function ask(string $question, string $default = ''): string
{
return $this->output->askQuestion($question, $default);
}
/**
* Fragt nach einem Passwort (versteckte Eingabe).
*/
public function askPassword(string $question): string
{
echo $question . ": ";
// Verstecke Eingabe
system('stty -echo');
$password = trim(fgets(STDIN));
system('stty echo');
echo PHP_EOL;
return $password;
}
/**
* Fragt nach einer Bestätigung (Ja/Nein).
*/
public function confirm(string $question, bool $default = false): bool
{
return $this->output->confirm($question, $default);
}
/**
* Zeigt ein einfaches Auswahlmenü.
*/
public function choice(string $question, array $choices, mixed $default = null): mixed
{
$menu = new InteractiveMenu($this->output);
$menu->setTitle($question);
foreach ($choices as $key => $choice) {
$menu->addItem($choice, null, $key);
}
return $menu->showSimple();
}
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation.
*/
public function menu(string $title, array $items): mixed
{
$menu = new InteractiveMenu($this->output);
$menu->setTitle($title);
foreach ($items as $key => $item) {
if ($item === '---') {
$menu->addSeparator();
} else {
$menu->addItem($item, null, $key);
}
}
return $menu->showInteractive();
}
/**
* Ermöglicht die Mehrfachauswahl aus einer Liste von Optionen.
*/
public function multiSelect(string $question, array $options): array
{
$this->output->writeLine($question, ConsoleColor::BRIGHT_CYAN);
$this->output->writeLine("Mehrfachauswahl mit Komma getrennt (z.B. 1,3,5):");
foreach ($options as $key => $option) {
$this->output->writeLine(" " . ($key + 1) . ": {$option}");
}
$this->output->write("Ihre Auswahl: ", ConsoleColor::BRIGHT_CYAN);
$input = trim(fgets(STDIN));
$selected = [];
$indices = explode(',', $input);
foreach ($indices as $index) {
$index = (int)trim($index) - 1;
if (isset($options[$index])) {
$selected[] = $options[$index];
}
}
return $selected;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Framework\Console;
interface ConsoleInputInterface
{
/**
* Gibt ein Argument an der angegebenen Position zurück
*/
public function getArgument(int $index, ?string $default = null): ?string;
/**
* Gibt alle Argumente zurück
*/
public function getArguments(): array;
/**
* Gibt eine Option zurück
*/
public function getOption(string $name, mixed $default = null): mixed;
/**
* Prüft, ob eine Option vorhanden ist
*/
public function hasOption(string $name): bool;
/**
* Gibt alle Optionen zurück
*/
public function getOptions(): array;
/**
* Fragt nach einer Benutzereingabe
*/
public function ask(string $question, string $default = ''): string;
/**
* Fragt nach einem Passwort (versteckte Eingabe)
*/
public function askPassword(string $question): string;
/**
* Fragt nach einer Bestätigung (Ja/Nein)
*/
public function confirm(string $question, bool $default = false): bool;
/**
* Zeigt ein einfaches Auswahlmenü
*/
public function choice(string $question, array $choices, mixed $default = null): mixed;
/**
* Zeigt ein interaktives Menü mit Pfeiltasten-Navigation
*/
public function menu(string $title, array $items): mixed;
/**
* Ermöglicht die Mehrfachauswahl aus einer Liste von Optionen
*/
public function multiSelect(string $question, array $options): array;
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Screen\Cursor;
use App\Framework\Console\Screen\Display;
use App\Framework\Console\Screen\ScreenManager;
final readonly class ConsoleOutput implements ConsoleOutputInterface
{
public Cursor $cursor;
public Display $display;
public ScreenManager $screen;
public function __construct()
{
$this->cursor = new Cursor($this);
$this->display = new Display($this);
$this->screen = new ScreenManager($this);
}
/**
* Gibt den Cursor-Controller zurück.
*/
public function cursor(): Cursor
{
return $this->cursor;
}
/**
* Gibt den Display-Controller zurück.
*/
public function display(): Display
{
return $this->display;
}
/**
* Gibt den ScreenManager zurück.
*/
public function screen(): ScreenManager
{
return $this->screen;
}
/**
* Schreibt Text mit optionalem Stil.
*/
public function write(string $message, ConsoleStyle|ConsoleColor|null $style = null): void
{
if ($style instanceof ConsoleColor) {
// Abwärtskompatibilität
echo $style->toAnsi() . $message . ConsoleColor::RESET->toAnsi();
} elseif ($style instanceof ConsoleStyle) {
echo $style->apply($message);
} else {
echo $message;
}
}
/**
* Schreibt rohe ANSI-Sequenzen ohne Verarbeitung.
*/
public function writeRaw(string $raw): void
{
echo $raw;
}
/**
* Setzt den Fenstertitel.
*/
public function writeWindowTitle(string $title, int $mode = 0): void
{
$this->writeRaw("\033]$mode;{$title}\007");
}
/**
* Schreibt eine Zeile mit optionalem Stil.
*/
public function writeLine(string $message = '', ConsoleStyle|ConsoleColor|null $color = null): void
{
$this->write($message . PHP_EOL, $color);
}
/**
* Schreibt eine Erfolgsmeldung.
*/
public function writeSuccess(string $message): void
{
$this->writeLine('✓ ' . $message, ConsoleStyle::success());
}
/**
* Schreibt eine Fehlermeldung.
*/
public function writeError(string $message): void
{
$this->writeLine('✗ ' . $message, ConsoleStyle::error());
}
/**
* Schreibt eine Warnmeldung.
*/
public function writeWarning(string $message): void
{
$this->writeLine('⚠ ' . $message, ConsoleStyle::warning());
}
/**
* Schreibt eine Infomeldung.
*/
public function writeInfo(string $message): void
{
$this->writeLine(' ' . $message, ConsoleStyle::info());
}
/**
* Fügt eine oder mehrere Leerzeilen ein.
*/
public function newLine(int $count = 1): void
{
echo str_repeat(PHP_EOL, $count);
}
/**
* Stellt eine Frage und gibt die Antwort zurück.
*/
public function askQuestion(string $question, ?string $default = null): string
{
$prompt = $question;
if ($default !== null) {
$prompt .= " [{$default}]";
}
$prompt .= ': ';
$this->write($prompt, ConsoleColor::BRIGHT_CYAN);
$answer = trim(fgets(STDIN));
return $answer === '' && $default !== null ? $default : $answer;
}
/**
* Stellt eine Ja/Nein-Frage.
*/
public function confirm(string $question, bool $default = false): bool
{
$defaultText = $default ? 'Y/n' : 'y/N';
$answer = $this->askQuestion("{$question} [{$defaultText}]");
if ($answer === '') {
return $default;
}
return in_array(strtolower($answer), ['y', 'yes', 'ja', '1', 'true']);
}
/**
* Prüft, ob der Output zu einem Terminal geht.
*/
public function isTerminal(): bool
{
return function_exists('posix_isatty') && posix_isatty(STDOUT);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Interface für ConsoleOutput-Klassen.
*/
interface ConsoleOutputInterface
{
/**
* Schreibt Text in die Konsole
*/
public function write(string $message, ?ConsoleColor $color = null): void;
/**
* Schreibt Text mit Zeilenumbruch in die Konsole
*/
public function writeLine(string $message = '', ?ConsoleColor $color = null): void;
/**
* Schreibt eine Erfolgsmeldung
*/
public function writeSuccess(string $message): void;
/**
* Schreibt eine Fehlermeldung
*/
public function writeError(string $message): void;
/**
* Schreibt eine Warnmeldung
*/
public function writeWarning(string $message): void;
/**
* Schreibt eine Informationsmeldung
*/
public function writeInfo(string $message): void;
/**
* Fügt leere Zeilen hinzu
*/
public function newLine(int $count = 1): void;
/**
* Stellt eine Frage und wartet auf Benutzereingabe
*/
public function askQuestion(string $question, ?string $default = null): string;
/**
* Fragt nach einer Bestätigung (ja/nein)
*/
public function confirm(string $question, bool $default = false): bool;
/**
* Setzt den Fenstertitel des Terminals
*/
public function writeWindowTitle(string $title, int $mode = 0): void;
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Framework\Console;
final readonly class ConsoleStyle
{
public function __construct(
public ?ConsoleColor $color = null,
public ?ConsoleFormat $format = null,
public ?ConsoleColor $background = null,
){}
/**
* Erstellt einen Style mit den gewünschten Eigenschaften.
*/
public static function create(
?ConsoleColor $color = null,
?ConsoleFormat $format = null,
?ConsoleColor $backgroundColor = null
): self {
return new self($color, $format, $backgroundColor);
}
public function toAnsi(): string
{
$codes = [];
if ($this->format !== null) {
$codes[] = $this->format->value;
}
if ($this->color !== null) {
$codes[] = $this->color->value;
}
if ($this->background !== null) {
$codes[] = $this->background->value;
}
if (empty($codes)) {
return '';
}
return "\033[" . implode(';', $codes) . 'm';
}
/**
* Wendet den Style auf einen Text an.
*/
public function apply(string $text): string
{
$ansi = $this->toAnsi();
if ($ansi === '') {
return $text;
}
return $ansi . $text . ConsoleColor::RESET->toAnsi();
}
// Vordefinierte Styles für häufige Anwendungen
public static function success(): self
{
return new self(ConsoleColor::BRIGHT_GREEN, ConsoleFormat::BOLD);
}
public static function error(): self
{
return new self(ConsoleColor::BRIGHT_RED, ConsoleFormat::BOLD);
}
public static function warning(): self
{
return new self(ConsoleColor::BRIGHT_YELLOW, ConsoleFormat::BOLD);
}
public static function info(): self
{
return new self(ConsoleColor::BRIGHT_BLUE);
}
public static function highlight(): self
{
return new self(format: ConsoleFormat::BOLD);
}
public static function dim(): self
{
return new self(format: ConsoleFormat::DIM);
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
use App\Framework\Console\Components\InteractiveMenu;
final readonly class DemoCommand
{
#[ConsoleCommand('demo:hello', 'Zeigt eine einfache Hallo-Welt-Nachricht')]
public function hello(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeWindowTitle('Help Title', 2);
$name = $input->getArgument(0, 'Welt');
$output->writeLine("Hallo, {$name}!", ConsoleColor::BRIGHT_GREEN);
if ($input->hasOption('time')) {
$output->writeInfo('Aktuelle Zeit: ' . date('Y-m-d H:i:s'));
}
return 0;
}
#[ConsoleCommand('demo:colors', 'Zeigt alle verfügbaren Farben')]
public function colors(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Verfügbare Farben:', ConsoleColor::BRIGHT_WHITE);
$output->newLine();
$colors = [
'BLACK' => ConsoleColor::BLACK,
'RED' => ConsoleColor::RED,
'GREEN' => ConsoleColor::GREEN,
'YELLOW' => ConsoleColor::YELLOW,
'BLUE' => ConsoleColor::BLUE,
'MAGENTA' => ConsoleColor::MAGENTA,
'CYAN' => ConsoleColor::CYAN,
'WHITE' => ConsoleColor::WHITE,
'GRAY' => ConsoleColor::GRAY,
];
foreach ($colors as $name => $color) {
$output->writeLine(" {$name}: Dies ist ein Test", $color);
}
$output->newLine();
$output->writeLine('Spezielle Ausgaben:');
$output->writeSuccess('Erfolg-Nachricht');
$output->writeError('Fehler-Nachricht');
$output->writeWarning('Warnung-Nachricht');
$output->writeInfo('Info-Nachricht');
return 0;
}
#[ConsoleCommand('demo:interactive', 'Interaktive Demo mit Benutzereingaben')]
public function interactive(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Interaktive Demo', ConsoleColor::BRIGHT_CYAN);
$output->newLine();
$name = $output->askQuestion('Wie ist Ihr Name?', 'Unbekannt');
$age = $output->askQuestion('Wie alt sind Sie?');
$confirmed = $output->confirm('Sind die Angaben korrekt?', true);
$output->newLine();
if ($confirmed) {
$output->writeSuccess("Hallo {$name}, Sie sind {$age} Jahre alt!");
} else {
$output->writeWarning('Vorgang abgebrochen.');
}
return 0;
}
#[ConsoleCommand('demo:menu', 'Zeigt ein interaktives Menü')]
public function menu(ConsoleInput $input, ConsoleOutput $output): int
{
$menu = new InteractiveMenu($output);
$result = $menu
->setTitle('Hauptmenü - Demo Application')
->addItem('Benutzer verwalten', function() use ($output) {
return $this->userMenu($output);
})
->addItem('Einstellungen', function() use ($output) {
$output->writeInfo('Einstellungen werden geöffnet...');
return 'settings';
})
->addSeparator()
->addItem('Hilfe anzeigen', function() use ($output) {
$output->writeInfo('Hilfe wird angezeigt...');
return 'help';
})
->addItem('Beenden', function() use ($output) {
$output->writeSuccess('Auf Wiedersehen!');
return 'exit';
})
->showInteractive();
if ($result) {
$output->writeInfo("Menü-Rückgabe: {$result}");
}
return 0;
}
#[ConsoleCommand('demo:simple-menu', 'Zeigt ein einfaches Nummern-Menü')]
public function simpleMenu(ConsoleInput $input, ConsoleOutput $output): int
{
$menu = new InteractiveMenu($output);
$result = $menu
->setTitle('Einfaches Menü')
->addItem('Option 1')
->addItem('Option 2')
->addItem('Option 3')
->showSimple();
$output->writeSuccess("Sie haben gewählt: {$result}");
return 0;
}
#[ConsoleCommand('demo:wizard', 'Zeigt einen Setup-Wizard')]
public function wizard(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeInfo('🧙 Setup-Wizard gestartet');
$output->newLine();
// Schritt 1: Grundeinstellungen
$output->writeLine('Schritt 1: Grundeinstellungen', ConsoleColor::BRIGHT_CYAN);
$appName = $input->ask('Name der Anwendung', 'Meine App');
$version = $input->ask('Version', '1.0.0');
// Schritt 2: Datenbank
$output->newLine();
$output->writeLine('Schritt 2: Datenbank-Konfiguration', ConsoleColor::BRIGHT_CYAN);
$dbHost = $input->ask('Datenbank-Host', 'localhost');
$dbName = $input->ask('Datenbank-Name', 'myapp');
$dbUser = $input->ask('Datenbank-Benutzer', 'root');
$dbPass = $input->askPassword('Datenbank-Passwort');
// Schritt 3: Features
$output->newLine();
$output->writeLine('Schritt 3: Features auswählen', ConsoleColor::BRIGHT_CYAN);
$features = $input->multiSelect('Welche Features möchten Sie aktivieren?', [
'Caching',
'Logging',
'Email-Versand',
'API-Schnittstelle',
'Admin-Panel'
]);
// Zusammenfassung
$output->newLine();
$output->writeLine('🎯 Konfiguration abgeschlossen!', ConsoleColor::BRIGHT_GREEN);
$output->newLine();
$output->writeLine('Ihre Einstellungen:');
$output->writeLine("- App-Name: {$appName}");
$output->writeLine("- Version: {$version}");
$output->writeLine("- DB-Host: {$dbHost}");
$output->writeLine("- DB-Name: {$dbName}");
$output->writeLine("- Features: " . implode(', ', $features));
if ($input->confirm('Konfiguration speichern?', true)) {
$output->writeSuccess('✅ Konfiguration wurde gespeichert!');
} else {
$output->writeWarning('❌ Konfiguration wurde nicht gespeichert.');
}
return 0;
}
/**
* Hilfsmethode für das Benutzer-Untermenü.
*/
private function userMenu(ConsoleOutput $output): string
{
$menu = new InteractiveMenu($output);
return $menu
->setTitle('Benutzer-Verwaltung')
->addItem('Benutzer auflisten', fn() => 'list_users')
->addItem('Neuen Benutzer erstellen', fn() => 'create_user')
->addItem('Benutzer bearbeiten', fn() => 'edit_user')
->addItem('Benutzer löschen', fn() => 'delete_user')
->addSeparator()
->addItem('← Zurück zum Hauptmenü', fn() => 'back')
->showInteractive() ?? 'back';
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\DemoCommand;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
use App\Framework\Console\Screen\ScreenType;
final class ScreenDemoCommand
{
#[ConsoleCommand('demo:screen', 'Zeigt die verschiedenen Screen-Management-Funktionen')]
public function __invoke(ConsoleInput $input, ConsoleOutput $output): int
{
// Aktiviere interaktiven Modus
$output->screen()->setInteractiveMode(true);
// Zeige ein Menü
$output->screen()->newMenu();
$output->writeLine('=== Screen-Management Demo ===', ConsoleStyle::success());
$output->writeLine('');
$output->writeLine('1. Cursor-Bewegungen');
$output->writeLine('2. Bildschirmlöschung');
$output->writeLine('3. Fortschrittsanzeige');
$output->writeLine('4. Typensatz-Beispiel');
$output->writeLine('');
$choice = $output->askQuestion('Wählen Sie eine Option (1-4)');
switch ($choice) {
case '1':
$this->demoCursor($input, $output);
break;
case '2':
$this->demoDisplay($input, $output);
break;
case '3':
$this->demoProgress($input, $output);
break;
case '4':
$this->demoTypeset($input, $output);
break;
default:
$output->writeError('Ungültige Auswahl!');
}
return 0;
}
private function demoCursor(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::CONTENT);
$output->writeLine('=== Cursor-Bewegungs-Demo ===', ConsoleStyle::success());
$output->writeLine('');
// Cursor-Bewegungen
$output->writeLine('Der Cursor wird jetzt bewegt...');
sleep(1);
$output->cursor()->down(2)->right(5);
$output->write('Hallo!', ConsoleStyle::success());
sleep(1);
$output->cursor()->down(1)->left(5);
$output->write('Welt!', ConsoleStyle::info());
sleep(1);
$output->cursor()->moveTo(10, 20);
$output->write('Position 10,20', ConsoleStyle::warning());
sleep(1);
$output->cursor()->home();
$output->writeLine("\nZurück zum Anfang!", ConsoleStyle::error());
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um fortzufahren...");
$output->screen()->waitForInput();
}
private function demoDisplay(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::CONTENT);
$output->writeLine('=== Bildschirmlöschungs-Demo ===', ConsoleStyle::success());
$output->writeLine('');
// Bildschirm füllen
for ($i = 1; $i <= 10; $i++) {
$output->writeLine("Zeile $i: Dies ist ein Test");
}
sleep(1);
$output->writeLine("\nLösche in 3 Sekunden den Bildschirm...");
sleep(3);
// Bildschirm löschen
$output->display()->clear();
$output->writeLine('Bildschirm wurde gelöscht!');
sleep(1);
// Zeilen hinzufügen
for ($i = 1; $i <= 5; $i++) {
$output->writeLine("Neue Zeile $i");
}
sleep(1);
$output->writeLine("\nLösche nur die aktuelle Zeile in 2 Sekunden...");
sleep(2);
// Zeile löschen
$output->display()->clearLine();
$output->writeLine('Die Zeile wurde gelöscht und durch diese ersetzt!');
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um fortzufahren...");
$output->screen()->waitForInput();
}
private function demoProgress(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::PROGRESS);
$output->writeLine('=== Fortschrittsanzeige-Demo ===', ConsoleStyle::success());
$output->writeLine('');
$total = 20;
for ($i = 0; $i <= $total; $i++) {
$percent = floor(($i / $total) * 100);
$bar = str_repeat('█', $i) . str_repeat('░', $total - $i);
// Zeile löschen und neue Fortschrittsanzeige
$output->display()->clearLine();
$output->write("Fortschritt: [{$bar}] {$percent}%");
usleep(200000); // 200ms pause
}
$output->writeLine("\n\nFortschritt abgeschlossen!");
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um fortzufahren...");
$output->screen()->waitForInput();
}
private function demoTypeset(ConsoleInput $input, ConsoleOutput $output): void
{
$output->screen()->newScreen(ScreenType::CONTENT);
$output->writeLine('=== Typensatz-Demo ===', ConsoleStyle::success());
$output->writeLine('');
$text = 'Dies ist eine Demonstration des Typensatz-Effekts. Der Text wird Zeichen für Zeichen angezeigt, ' .
'als ob er gerade getippt würde. Diese Technik kann für Intros, Tutorials oder ' .
'dramatische Effekte verwendet werden.';
// Typensatz-Effekt
foreach (str_split($text) as $char) {
$output->write($char);
usleep(rand(50000, 150000)); // 50-150ms zufällige Pause
}
$output->writeLine("\n\nTypensatz abgeschlossen!");
sleep(1);
$output->writeLine("\nDrücken Sie eine Taste, um zurückzukehren...");
$output->screen()->waitForInput();
// Zurück zum Hauptmenü
$output->screen()->newMenu();
$this->__invoke($input, $output);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Framework\Console\Examples;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ProgressBar;
class ProgressBarExample
{
#[ConsoleCommand(name: 'demo:progressbar', description: 'Zeigt eine Demonstration der Fortschrittsanzeige')]
public function showProgressBarDemo(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeInfo('Demonstration der Fortschrittsanzeige');
$output->newLine();
// Einfache Fortschrittsanzeige
$output->writeLine('Einfache Fortschrittsanzeige:');
$progress = new ProgressBar($output, 10);
$progress->start();
for ($i = 0; $i < 10; $i++) {
// Simuliere Arbeit
usleep(200000);
$progress->advance();
}
$progress->finish();
// Fortschrittsanzeige mit angepasstem Format
$output->writeLine('Fortschrittsanzeige mit angepasstem Format:');
$progress = new ProgressBar($output, 5);
$progress->setFormat('%bar% %current%/%total% [%percent%%] - %elapsed%s vergangen, %remaining%s verbleibend');
$progress->setBarCharacters('█', '░', '█');
$progress->start();
for ($i = 0; $i < 5; $i++) {
// Simuliere Arbeit
usleep(500000);
$progress->advance();
}
$progress->finish();
$output->writeSuccess('Demonstration abgeschlossen!');
return 0;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Framework\Console\Examples;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\Spinner;
use App\Framework\Console\SpinnerStyle;
class SpinnerExample
{
#[ConsoleCommand(name: 'demo:spinner', description: 'Zeigt eine Demonstration der Spinner-Komponente')]
public function showSpinnerDemo(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeInfo('Demonstration der Spinner-Komponente');
$output->newLine();
// Einfacher Spinner
$spinner = new Spinner($output, 'Lade Daten...');
$spinner->start();
// Simuliere Arbeit
for ($i = 0; $i < 10; $i++) {
usleep(100000);
$spinner->update();
}
$spinner->success('Daten erfolgreich geladen!');
// Spinner mit verschiedenen Stilen
$styles = [
SpinnerStyle::DOTS,
SpinnerStyle::LINE,
SpinnerStyle::BOUNCE,
SpinnerStyle::ARROW
];
foreach ($styles as $style) {
$output->writeLine("Spinner-Stil: {$style->name}");
$spinner = new Spinner($output, 'Verarbeite...', $style);
$spinner->start();
for ($i = 0; $i < 15; $i++) {
usleep(100000);
if ($i === 5) {
$spinner->setMessage('Fast fertig...');
}
$spinner->update();
}
$spinner->success('Fertig!');
}
// Fehlerhafter Prozess
$spinner = new Spinner($output, 'Verbinde mit Server...');
$spinner->start();
for ($i = 0; $i < 8; $i++) {
usleep(150000);
$spinner->update();
}
$spinner->error('Verbindung fehlgeschlagen!');
$output->writeSuccess('Demonstration abgeschlossen!');
return 0;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Examples;
use App\Framework\Console\Components\Table;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
final class TableExample
{
#[ConsoleCommand('demo:table', 'Zeigt eine Beispiel-Tabelle')]
public function showTable(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Beispiel für die Table-Komponente', ConsoleStyle::create(
color: ConsoleColor::BRIGHT_WHITE,
format: ConsoleFormat::BOLD
));
$output->newLine();
// Einfache Tabelle
$table = new Table(
headerStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD),
borderStyle: ConsoleStyle::create(color: ConsoleColor::CYAN)
);
$table->setHeaders(['Name', 'Alter', 'Stadt'])
->addRow(['Max Mustermann', '30', 'Berlin'])
->addRow(['Anna Schmidt', '25', 'München'])
->addRow(['Peter Weber', '35', 'Hamburg']);
$output->write($table->render());
$output->newLine();
// Komplexere Tabelle mit Produktdaten
$output->writeLine('Produktübersicht:', ConsoleStyle::create(color: ConsoleColor::BRIGHT_GREEN));
$productTable = new Table(
headerStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD),
rowStyle: ConsoleStyle::create(color: ConsoleColor::WHITE),
borderStyle: ConsoleStyle::create(color: ConsoleColor::GRAY)
);
$productTable->setHeaders(['Artikel-Nr.', 'Bezeichnung', 'Preis', 'Lagerbestand', 'Status'])
->setPadding(2)
->addRow(['A-1001', 'Tastatur Deluxe', '89,99 €', '45', 'Verfügbar'])
->addRow(['A-1002', 'Gaming Maus Pro', '69,99 €', '12', 'Knapp'])
->addRow(['A-1003', '4K Monitor 27"', '349,99 €', '0', 'Ausverkauft'])
->addRow(['A-1004', 'USB-C Kabel 2m', '19,99 €', '156', 'Verfügbar'])
->addRow(['A-1005', 'Wireless Headset', '129,99 €', '8', 'Knapp']);
$output->write($productTable->render());
return 0;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Examples;
use App\Framework\Console\Components\TextBox;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
final class TextBoxExample
{
#[ConsoleCommand('demo:textbox', 'Zeigt verschiedene TextBox-Beispiele')]
public function showTextBox(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Beispiele für die TextBox-Komponente', ConsoleStyle::create(
color: ConsoleColor::BRIGHT_WHITE,
format: ConsoleFormat::BOLD
));
$output->newLine();
// Einfache TextBox
$infoBox = new TextBox(
content: "Dies ist eine einfache Informationsbox.\nSie kann mehrere Zeilen enthalten und passt den Text automatisch an die Breite an.",
width: 60,
borderStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_BLUE),
contentStyle: ConsoleStyle::create(color: ConsoleColor::WHITE),
title: "Information"
);
$output->write($infoBox->render());
$output->newLine();
// Warnung TextBox
$warningBox = new TextBox(
content: "Warnung! Diese Aktion kann nicht rückgängig gemacht werden. Stellen Sie sicher, dass Sie ein Backup Ihrer Daten haben, bevor Sie fortfahren.",
width: 70,
padding: 2,
borderStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW),
contentStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE),
title: "⚠ Warnung"
);
$output->write($warningBox->render());
$output->newLine();
// Fehler TextBox
$errorBox = new TextBox(
content: "Fehler: Die Verbindung zur Datenbank konnte nicht hergestellt werden. Überprüfen Sie Ihre Zugangsdaten und stellen Sie sicher, dass der Datenbankserver läuft.",
width: 80,
padding: 1,
borderStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_RED),
contentStyle: ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE),
title: "✗ Fehler"
);
$output->write($errorBox->render());
return 0;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Examples;
use App\Framework\Console\Components\TreeHelper;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleFormat;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ConsoleStyle;
final class TreeExample
{
#[ConsoleCommand('demo:tree', 'Zeigt ein Beispiel für die TreeHelper-Komponente')]
public function showTreeExample(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine('Beispiel für den TreeHelper', ConsoleStyle::create(
color: ConsoleColor::BRIGHT_WHITE,
format: ConsoleFormat::BOLD
));
$output->newLine();
// Einfaches Verzeichnisbeispiel
$tree = new TreeHelper('Projektstruktur');
$tree->setNodeStyle(ConsoleStyle::create(color: ConsoleColor::BRIGHT_CYAN, format: ConsoleFormat::BOLD));
$tree->setLeafStyle(ConsoleStyle::create(color: ConsoleColor::WHITE));
$srcNode = $tree->addNode('src/');
$domainNode = $srcNode->addNode('Domain/');
$domainNode->addNode('User/')->addLeaf('User.php')->addLeaf('UserRepository.php');
$domainNode->addNode('Product/')->addLeaf('Product.php')->addLeaf('ProductRepository.php');
$frameworkNode = $srcNode->addNode('Framework/');
$consoleNode = $frameworkNode->addNode('Console/');
$consoleNode->addNode('Components/')
->addLeaf('Table.php')
->addLeaf('TextBox.php')
->addLeaf('TreeHelper.php');
$consoleNode->addLeaf('ConsoleColor.php');
$consoleNode->addLeaf('ConsoleFormat.php');
$consoleNode->addLeaf('ConsoleOutput.php');
$testNode = $tree->addNode('tests/');
$testNode->addLeaf('UserTest.php');
$testNode->addLeaf('ProductTest.php');
$tree->addLeaf('composer.json');
$tree->addLeaf('README.md');
$output->write($tree->render());
$output->newLine(2);
// Zweites Beispiel: Kategorien
$output->writeLine('Kategorie-Baum:', ConsoleStyle::create(color: ConsoleColor::BRIGHT_GREEN));
$categories = new TreeHelper();
$categories->setNodeStyle(ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD));
$categories->setLeafStyle(ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE));
$categories->setLineStyle(ConsoleStyle::create(color: ConsoleColor::GRAY));
$electronics = $categories->addNode('Elektronik');
$computers = $electronics->addNode('Computer & Zubehör');
$computers->addLeaf('Laptops');
$computers->addLeaf('Desktop-PCs');
$peripherie = $computers->addNode('Peripheriegeräte');
$peripherie->addLeaf('Monitore');
$peripherie->addLeaf('Tastaturen');
$peripherie->addLeaf('Mäuse');
$smartphones = $electronics->addNode('Smartphones & Zubehör');
$smartphones->addLeaf('Handys');
$smartphones->addLeaf('Hüllen');
$smartphones->addLeaf('Ladegeräte');
$kleidung = $categories->addNode('Kleidung');
$kleidung->addLeaf('Herren');
$kleidung->addLeaf('Damen');
$kleidung->addLeaf('Kinder');
$output->write($categories->render());
return 0;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Framework\Console\Exceptions;
class CommandNotFoundException extends ConsoleException
{
public function __construct(string $commandName)
{
parent::__construct("Kommando '{$commandName}' nicht gefunden.");
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Framework\Console\Exceptions;
class ConsoleException extends \Exception
{
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console;
/**
* Standard Exit-Codes für Console-Anwendungen
*
* Basiert auf den POSIX-Standards und bewährten Praktiken:
* - 0: Erfolg
* - 1: Allgemeiner Fehler
* - 2: Falsche Verwendung (ungültige Argumente)
* - 64-78: Spezifische Fehler-Codes (sysexits.h Standard)
*/
enum ExitCode: int
{
case SUCCESS = 0;
case GENERAL_ERROR = 1;
case USAGE_ERROR = 2;
case COMMAND_NOT_FOUND = 64;
case INVALID_INPUT = 65;
case NO_INPUT = 66;
case UNAVAILABLE = 69;
case SOFTWARE_ERROR = 70;
case OS_ERROR = 71;
case OS_FILE_ERROR = 72;
case CANT_CREATE = 73;
case IO_ERROR = 74;
case TEMP_FAIL = 75;
case PROTOCOL_ERROR = 76;
case NO_PERMISSION = 77;
case CONFIG_ERROR = 78;
/**
* Gibt eine menschenlesbare Beschreibung des Exit-Codes zurück
*/
public function getDescription(): string
{
return match($this) {
self::SUCCESS => 'Erfolgreich abgeschlossen',
self::GENERAL_ERROR => 'Allgemeiner Fehler',
self::USAGE_ERROR => 'Falsche Verwendung oder ungültige Argumente',
self::COMMAND_NOT_FOUND => 'Kommando nicht gefunden',
self::INVALID_INPUT => 'Ungültige Eingabedaten',
self::NO_INPUT => 'Keine Eingabe vorhanden',
self::UNAVAILABLE => 'Service nicht verfügbar',
self::SOFTWARE_ERROR => 'Interner Software-Fehler',
self::OS_ERROR => 'Betriebssystem-Fehler',
self::OS_FILE_ERROR => 'Datei-/Verzeichnis-Fehler',
self::CANT_CREATE => 'Kann Datei/Verzeichnis nicht erstellen',
self::IO_ERROR => 'Ein-/Ausgabe-Fehler',
self::TEMP_FAIL => 'Temporärer Fehler',
self::PROTOCOL_ERROR => 'Protokoll-Fehler',
self::NO_PERMISSION => 'Keine Berechtigung',
self::CONFIG_ERROR => 'Konfigurationsfehler',
};
}
/**
* Prüft, ob der Exit-Code einen Erfolg darstellt
*/
public function isSuccess(): bool
{
return $this === self::SUCCESS;
}
/**
* Prüft, ob der Exit-Code einen Fehler darstellt
*/
public function isError(): bool
{
return !$this->isSuccess();
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace App\Framework\Console;
class ProgressBar
{
private ConsoleOutputInterface $output;
private int $total;
private int $current = 0;
private int $width;
private float $startTime;
private string $format = '%bar% %percent%%';
private string $barChar = '=';
private string $emptyBarChar = '-';
private string $progressChar = '>';
private int $redrawFrequency = 1;
private int $writeCount = 0;
private bool $firstRun = true;
public function __construct(ConsoleOutputInterface $output, int $total = 100, int $width = 50)
{
$this->output = $output;
$this->total = max(1, $total);
$this->width = $width;
$this->startTime = microtime(true);
}
/**
* Setzt das Format der Fortschrittsanzeige
* Verfügbare Platzhalter:
* - %bar%: Die Fortschrittsanzeige
* - %current%: Aktueller Fortschritt
* - %total%: Gesamtwert
* - %percent%: Prozentsatz des Fortschritts
* - %elapsed%: Verstrichene Zeit in Sekunden
* - %remaining%: Geschätzte verbleibende Zeit in Sekunden
*/
public function setFormat(string $format): self
{
$this->format = $format;
return $this;
}
/**
* Setzt die Zeichen für die Fortschrittsanzeige
*/
public function setBarCharacters(string $barChar = '=', string $emptyBarChar = '-', string $progressChar = '>'): self
{
$this->barChar = $barChar;
$this->emptyBarChar = $emptyBarChar;
$this->progressChar = $progressChar;
return $this;
}
/**
* Setzt die Häufigkeit der Aktualisierung
*/
public function setRedrawFrequency(int $frequency): self
{
$this->redrawFrequency = max(1, $frequency);
return $this;
}
/**
* Erhöht den Fortschritt um den angegebenen Schritt
*/
public function advance(int $step = 1): self
{
$this->setCurrent($this->current + $step);
return $this;
}
/**
* Setzt den aktuellen Fortschritt
*/
public function setCurrent(int $current): self
{
$this->current = min($this->total, max(0, $current));
$this->writeCount++;
if ($this->writeCount >= $this->redrawFrequency || $this->firstRun || $this->current >= $this->total) {
$this->display();
$this->writeCount = 0;
}
return $this;
}
/**
* Startet die Fortschrittsanzeige
*/
public function start(): self
{
$this->startTime = microtime(true);
$this->current = 0;
$this->firstRun = true;
$this->display();
return $this;
}
/**
* Beendet die Fortschrittsanzeige
*/
public function finish(): self
{
if ($this->current < $this->total) {
$this->current = $this->total;
}
$this->display();
$this->output->newLine(2);
return $this;
}
/**
* Zeigt die Fortschrittsanzeige an
*/
private function display(): void
{
if (!$this->firstRun) {
// Bewege den Cursor eine Zeile nach oben
$this->output->write("\033[1A");
// Lösche die aktuelle Zeile
$this->output->write("\033[2K");
}
$this->firstRun = false;
$percent = $this->current / $this->total;
$bar = $this->getProgressBar($percent);
$replacements = [
'%bar%' => $bar,
'%current%' => (string) $this->current,
'%total%' => (string) $this->total,
'%percent%' => number_format($percent * 100, 0),
'%elapsed%' => $this->getElapsedTime(),
'%remaining%' => $this->getRemaining($percent),
];
$line = str_replace(array_keys($replacements), array_values($replacements), $this->format);
$this->output->writeLine($line);
}
/**
* Generiert die Fortschrittsanzeige
*/
private function getProgressBar(float $percent): string
{
$completedWidth = (int) floor($percent * $this->width);
$emptyWidth = $this->width - $completedWidth - ($completedWidth < $this->width ? 1 : 0);
$bar = str_repeat($this->barChar, $completedWidth);
if ($completedWidth < $this->width) {
$bar .= $this->progressChar . str_repeat($this->emptyBarChar, $emptyWidth);
}
return $bar;
}
/**
* Berechnet die verstrichene Zeit
*/
private function getElapsedTime(): string
{
return number_format(microtime(true) - $this->startTime, 1);
}
/**
* Berechnet die verbleibende Zeit
*/
private function getRemaining(float $percent): string
{
if ($percent === 0) {
return '--';
}
$elapsed = microtime(true) - $this->startTime;
$remaining = $elapsed / $percent - $elapsed;
return number_format($remaining, 1);
}
}

View File

@@ -0,0 +1,106 @@
# Console-Modul
Dieses Modul bietet eine flexible und benutzerfreundliche Konsolen-Schnittstelle für Ihre PHP-Anwendung. Es ermöglicht die Erstellung von CLI-Befehlen mit einfacher Eingabe- und Ausgabehandlung.
## Hauptkomponenten
### ConsoleApplication
Die zentrale Klasse zur Verwaltung und Ausführung von Konsolen-Befehlen.
```php
$app = new ConsoleApplication('app', 'Meine Anwendung');
$app->registerCommands(new MyCommands());
$app->run($argv);
```
### ConsoleCommand-Attribut
Verwenden Sie das `ConsoleCommand`-Attribut, um Methoden als Konsolenbefehle zu kennzeichnen:
```php
class MyCommands
{
#[ConsoleCommand(name: 'hello', description: 'Gibt eine Begrüßung aus')]
public function sayHello(ConsoleInput $input, ConsoleOutput $output): int
{
$name = $input->getArgument(0, 'Welt');
$output->writeLine("Hallo, {$name}!", ConsoleColor::BRIGHT_GREEN);
return 0;
}
}
```
### ConsoleInput und ConsoleOutput
Diese Klassen bieten Methoden für die Ein- und Ausgabe in der Konsole:
```php
// Eingabe
$name = $input->ask('Wie heißen Sie?');
$confirm = $input->confirm('Fortfahren?', true);
$option = $input->getOption('verbose');
// Ausgabe
$output->writeSuccess('Operation erfolgreich!');
$output->writeError('Fehler aufgetreten!');
$output->writeInfo('Wussten Sie schon...');
```
## Fortschrittsanzeigen
### ProgressBar
Zeigt eine Fortschrittsanzeige für Operationen mit bekannter Länge:
```php
$total = count($items);
$progress = new ProgressBar($output, $total);
$progress->start();
foreach ($items as $item) {
// Verarbeite $item
$progress->advance();
}
$progress->finish();
```
### Spinner
Zeigt einen animierten Spinner für Operationen unbekannter Länge:
```php
$spinner = new Spinner($output, 'Lade Daten...');
$spinner->start();
// Ausführung der Operation
do {
// Arbeit ausführen
$spinner->update();
} while (!$finished);
$spinner->success('Daten erfolgreich geladen!');
```
## Beispiele
Sehen Sie sich die Beispielklassen im `Examples`-Verzeichnis an, um mehr über die Verwendung der Komponenten zu erfahren:
- `ProgressBarExample`: Zeigt verschiedene Konfigurationen der Fortschrittsanzeige
- `SpinnerExample`: Demonstriert die Verwendung von Spinnern mit verschiedenen Stilen
## Anpassung
Sie können die Anzeige anpassen, indem Sie benutzerdefinierte Formatierungen und Farben verwenden:
```php
$progress->setFormat('%bar% %percent%%');
$progress->setBarCharacters('█', '░', '█');
$spinner = new Spinner($output, 'Lade...', SpinnerStyle::BOUNCE);
```
## Fehlerbehebung
Wenn Probleme mit der Anzeige auftreten, stellen Sie sicher, dass Ihr Terminal ANSI-Escape-Sequenzen unterstützt. Die meisten modernen Terminals tun dies, aber Windows-Terminals können Einschränkungen haben.

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Strategien für das Löschen des Bildschirms.
*/
enum ClearStrategy
{
case NEVER; // Niemals löschen
case ALWAYS; // Immer löschen
case ON_NEW_SCREEN; // Nur bei neuen Bildschirmen
case SMART; // Intelligente Entscheidung
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
use App\Framework\Console\ConsoleOutput;
/**
* Verantwortlich für Cursor-Positionierung.
*/
final class Cursor
{
public function __construct(
private ConsoleOutput $output
) {}
/**
* Bewegt den Cursor zu einer bestimmten Position.
*/
public function moveTo(int $row, int $col): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::POSITION->format($row, $col));
}
return $this;
}
/**
* Bewegt den Cursor zum Anfang (Home-Position).
*/
public function home(): self
{
if ($this->output->isTerminal()) {
// Die Home-Position ist 1,1 (obere linke Ecke)
$this->output->writeRaw(CursorControlCode::POSITION->format(1, 1));
}
return $this;
}
/**
* Bewegt den Cursor um X Zeilen nach oben.
*/
public function up(int $lines = 1): self
{
if ($this->output->isTerminal() && $lines > 0) {
$this->output->writeRaw(CursorControlCode::UP->format($lines));
}
return $this;
}
/**
* Bewegt den Cursor um X Zeilen nach unten.
*/
public function down(int $lines = 1): self
{
if ($this->output->isTerminal() && $lines > 0) {
$this->output->writeRaw(CursorControlCode::DOWN->format($lines));
}
return $this;
}
/**
* Bewegt den Cursor um X Spalten nach links.
*/
public function left(int $columns = 1): self
{
if ($this->output->isTerminal() && $columns > 0) {
$this->output->writeRaw(CursorControlCode::LEFT->format($columns));
}
return $this;
}
/**
* Bewegt den Cursor um X Spalten nach rechts.
*/
public function right(int $columns = 1): self
{
if ($this->output->isTerminal() && $columns > 0) {
$this->output->writeRaw(CursorControlCode::RIGHT->format($columns));
}
return $this;
}
/**
* Versteckt den Cursor.
*/
public function hide(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::HIDE->format());
}
return $this;
}
/**
* Zeigt den Cursor wieder an.
*/
public function show(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::SHOW->format());
}
return $this;
}
/**
* Speichert die aktuelle Cursorposition.
*/
public function save(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::SAVE->format());
}
return $this;
}
/**
* Stellt die zuvor gespeicherte Cursorposition wieder her.
*/
public function restore(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(CursorControlCode::RESTORE->format());
}
return $this;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Cursor-Steuerungscodes nach ANSI-Standard.
*/
enum CursorControlCode: string
{
// Bewegung
case UP = "A"; // n Zeilen nach oben
case DOWN = "B"; // n Zeilen nach unten
case RIGHT = "C"; // n Spalten nach rechts
case LEFT = "D"; // n Spalten nach links
case NEXT_LINE = "E"; // n Zeilen nach unten und an den Anfang
case PREV_LINE = "F"; // n Zeilen nach oben und an den Anfang
case COLUMN = "G"; // Zur absoluten Spalte
case POSITION = "H"; // Zur Position (Zeile;Spalte)
// Sichtbarkeit
case HIDE = "?25l"; // Cursor verstecken
case SHOW = "?25h"; // Cursor anzeigen
// Andere
case SAVE = "s"; // Cursorposition speichern
case RESTORE = "u"; // Cursorposition wiederherstellen
/**
* Formatiert den ANSI-Steuerungscode korrekt.
*/
public function format(int ...$params): string
{
// Wenn keine Parameter gegeben sind, aber wir einen speziellen Escape-Code haben
if (empty($params) && in_array($this, [self::HIDE, self::SHOW])) {
return "\033[{$this->value}";
}
// Wenn keine Parameter, dann ohne formatieren
if (empty($params)) {
return "\033[{$this->value}";
}
// Wenn Parameter vorhanden sind, formatieren
$paramStr = implode(';', $params);
return "\033[{$paramStr}{$this->value}";
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
use App\Framework\Console\ConsoleOutput;
/**
* Verantwortlich für Bildschirm-Management (Löschen, etc.)
*/
final readonly class Display
{
public function __construct(
private ConsoleOutput $output
) {}
/**
* Löscht den gesamten Bildschirm und setzt den Cursor an den Anfang.
*/
public function clear(): self
{
if ($this->output->isTerminal()) {
// Bildschirm löschen und Cursor an den Anfang setzen
$this->output->writeRaw(ScreenControlCode::CLEAR_ALL->format());
$this->output->writeRaw(CursorControlCode::POSITION->format(1, 1));
}
return $this;
}
/**
* Löscht die aktuelle Zeile.
*/
public function clearLine(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_LINE->format());
$this->output->writeRaw("\r"); // Cursor an den Zeilenanfang
}
return $this;
}
/**
* Löscht vom Cursor bis zum Ende des Bildschirms.
*/
public function clearToEnd(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_BELOW->format());
}
return $this;
}
/**
* Löscht vom Cursor bis zum Anfang des Bildschirms.
*/
public function clearToBeginning(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_ABOVE->format());
}
return $this;
}
/**
* Löscht vom Cursor bis zum Ende der Zeile.
*/
public function clearLineToEnd(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_LINE_RIGHT->format());
}
return $this;
}
/**
* Löscht vom Cursor bis zum Anfang der Zeile.
*/
public function clearLineToBeginning(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::CLEAR_LINE_LEFT->format());
}
return $this;
}
/**
* Aktiviert den alternativen Buffer (für Vollbild-Anwendungen).
*/
public function useAlternateBuffer(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::ALTERNATE_BUFFER->format());
}
return $this;
}
/**
* Kehrt zum Hauptbuffer zurück.
*/
public function useMainBuffer(): self
{
if ($this->output->isTerminal()) {
$this->output->writeRaw(ScreenControlCode::MAIN_BUFFER->format());
}
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Bildschirm-Steuerungscodes nach ANSI-Standard.
*/
enum ScreenControlCode: string
{
case CLEAR_ALL = "2J"; // Gesamten Bildschirm löschen
case CLEAR_ABOVE = "1J"; // Bildschirm vom Cursor nach oben löschen
case CLEAR_BELOW = "0J"; // Bildschirm vom Cursor nach unten löschen
case CLEAR_LINE = "2K"; // Komplette Zeile löschen
case CLEAR_LINE_LEFT = "1K"; // Zeile vom Cursor nach links löschen
case CLEAR_LINE_RIGHT = "0K"; // Zeile vom Cursor nach rechts löschen
case SCROLL_UP = "S"; // Bildschirm nach oben scrollen
case SCROLL_DOWN = "T"; // Bildschirm nach unten scrollen
case SAVE_SCREEN = "?47h"; // Bildschirm speichern
case RESTORE_SCREEN = "?47l"; // Bildschirm wiederherstellen
case ALTERNATE_BUFFER = "?1049h"; // Alternativen Puffer aktivieren
case MAIN_BUFFER = "?1049l"; // Hauptpuffer wiederherstellen
/**
* Formatiert den ANSI-Steuerungscode korrekt.
*/
public function format(int ...$params): string
{
// Wenn keine Parameter, dann ohne formatieren
if (empty($params)) {
return "\033[{$this->value}";
}
// Wenn Parameter vorhanden sind, formatieren
$paramStr = implode(';', $params);
return "\033[{$paramStr}{$this->value}";
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
use App\Framework\Console\ConsoleOutput;
/**
* Verwaltet intelligentes Bildschirm-Management.
*/
final class ScreenManager
{
private ClearStrategy $strategy = ClearStrategy::SMART;
private bool $interactiveMode = false;
private ?ScreenType $lastScreenType = null;
private int $screenCount = 0;
public function __construct(
private ConsoleOutput $output
) {}
/**
* Setzt die Löschstrategie.
*/
public function setStrategy(ClearStrategy $strategy): self
{
$this->strategy = $strategy;
return $this;
}
/**
* Aktiviert/deaktiviert den interaktiven Modus.
*/
public function setInteractiveMode(bool $interactive = true): self
{
$this->interactiveMode = $interactive;
return $this;
}
/**
* Markiert den Beginn eines neuen Bildschirms.
* Diese Methode rufst du VOR der Ausgabe auf.
*/
public function newScreen(ScreenType $type = ScreenType::CONTENT): self
{
if ($this->shouldClear($type)) {
$this->output->display()->clear();
}
$this->lastScreenType = $type;
$this->screenCount++;
return $this;
}
/**
* Convenience-Methoden für häufige Screen-Typen.
*/
public function newMenu(): self
{
return $this->newScreen(ScreenType::MENU);
}
public function newDialog(): self
{
return $this->newScreen(ScreenType::DIALOG);
}
public function newContent(): self
{
return $this->newScreen(ScreenType::CONTENT);
}
public function newLog(): self
{
return $this->newScreen(ScreenType::LOG);
}
public function newProgress(): self
{
return $this->newScreen(ScreenType::PROGRESS);
}
/**
* Zeigt eine temporäre Nachricht für eine bestimmte Zeit an.
*/
public function temporary(string $message, int $seconds = 2): self
{
$this->output->writeLine($message);
sleep($seconds);
$this->output->display()->clearLine();
return $this;
}
/**
* Wartet auf Benutzereingabe.
*/
public function waitForInput(): self
{
if ($this->output->isTerminal()) {
fread(STDIN, 1);
}
return $this;
}
/**
* Entscheidet, ob der Bildschirm gelöscht werden soll.
*/
private function shouldClear(ScreenType $type): bool
{
if (!$this->output->isTerminal()) {
return false;
}
return match($this->strategy) {
ClearStrategy::NEVER => false,
ClearStrategy::ALWAYS => true,
ClearStrategy::ON_NEW_SCREEN => $type === ScreenType::MENU || $type === ScreenType::DIALOG,
ClearStrategy::SMART => $this->shouldClearSmart($type),
};
}
/**
* Intelligente Entscheidung für das Löschen.
*/
private function shouldClearSmart(ScreenType $type): bool
{
// Nie löschen für Logs und Progress
if ($type === ScreenType::LOG || $type === ScreenType::PROGRESS) {
return false;
}
// Immer löschen für Menüs und Dialoge im interaktiven Modus
if ($this->interactiveMode && ($type === ScreenType::MENU || $type === ScreenType::DIALOG)) {
return true;
}
// Für Content: nur löschen wenn der letzte Screen ein anderer Typ war
if ($type === ScreenType::CONTENT) {
return $this->lastScreenType !== null && $this->lastScreenType !== ScreenType::CONTENT;
}
return false;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Console\Screen;
/**
* Typen von Bildschirminhalten.
*/
enum ScreenType
{
case MENU; // Menüs und Navigation
case DIALOG; // Dialoge und Formulare
case CONTENT; // Normale Inhaltsanzeige
case LOG; // Log-Ausgaben
case PROGRESS; // Fortschrittsanzeigen
case INFO; // Informationsmeldungen
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Framework\Console;
class Spinner
{
private ConsoleOutputInterface $output;
private string $message;
private array $frames;
private int $currentFrame = 0;
private float $startTime;
private bool $active = false;
private float $interval;
private int $updateCount = 0;
public function __construct(
ConsoleOutputInterface $output,
string $message = 'Loading...',
SpinnerStyle $style = SpinnerStyle::DOTS,
float $interval = 0.1
) {
$this->output = $output;
$this->message = $message;
$this->frames = $style->getFrames();
$this->interval = $interval;
}
/**
* Startet den Spinner
*/
public function start(): self
{
$this->startTime = microtime(true);
$this->active = true;
$this->update();
return $this;
}
/**
* Stoppt den Spinner mit einer Erfolgs-, Fehler- oder neutralen Nachricht
*/
public function stop(?string $message = null, ?ConsoleColor $color = null): self
{
$this->active = false;
// Lösche die aktuelle Zeile
$this->output->write("\r\033[2K");
if ($message !== null) {
$this->output->writeLine($message, $color);
}
return $this;
}
/**
* Stoppt den Spinner mit einer Erfolgsmeldung
*/
public function success(string $message): self
{
return $this->stop("" . $message, ConsoleColor::BRIGHT_GREEN);
}
/**
* Stoppt den Spinner mit einer Fehlermeldung
*/
public function error(string $message): self
{
return $this->stop("" . $message, ConsoleColor::BRIGHT_RED);
}
/**
* Ändert die Nachricht des Spinners während er läuft
*/
public function setMessage(string $message): self
{
$this->message = $message;
if ($this->active) {
$this->update();
}
return $this;
}
/**
* Aktualisiert die Anzeige des Spinners
*/
public function update(): self
{
if (!$this->active) {
return $this;
}
$this->updateCount++;
// Aktualisiere nur in festgelegten Intervallen
$elapsed = microtime(true) - $this->startTime;
$expectedUpdates = floor($elapsed / $this->interval);
if ($this->updateCount < $expectedUpdates) {
$this->currentFrame = ($this->currentFrame + 1) % count($this->frames);
// Lösche die aktuelle Zeile und schreibe den aktualisierten Spinner
$frame = $this->frames[$this->currentFrame];
$this->output->write("\r\033[2K{$frame} {$this->message}");
$this->updateCount = $expectedUpdates;
}
return $this;
}
/**
* Beendet den Spinner ohne Ausgabe
*/
public function clear(): self
{
return $this->stop();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Framework\Console;
enum SpinnerStyle: string
{
case DOTS = 'dots';
case LINE = 'line';
case BOUNCE = 'bounce';
case ARROW = 'arrow';
/**
* Gibt die Frames für den gewählten Stil zurück
*/
public function getFrames(): array
{
return match($this) {
self::DOTS => ['. ', '.. ', '...', ' ..', ' .', ' '],
self::LINE => ['|', '/', '-', '\\'],
self::BOUNCE => ['⠈', '⠐', '⠠', '⢀', '⡀', '⠄', '⠂', '⠁'],
self::ARROW => ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙']
};
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\Context;
enum ContextType: string
{
case WEB = 'web';
case CONSOLE = 'console';
case WORKER = 'worker';
case CLI_SCRIPT = 'cli-script';
case TEST = 'test';
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace App\Framework\Context;
final readonly class ExecutionContext
{
public function __construct(
private ContextType $type,
private array $metadata = []
) {}
public function getType(): ContextType
{
return $this->type;
}
public function isWeb(): bool
{
return $this->type === ContextType::WEB;
}
public function isWorker(): bool
{
return $this->type === ContextType::WORKER;
}
public function isConsole(): bool
{
return $this->type === ContextType::CONSOLE;
}
public function isCli(): bool
{
return in_array($this->type, [
ContextType::CONSOLE,
ContextType::WORKER,
ContextType::CLI_SCRIPT
]);
}
public function isTest(): bool
{
return $this->type === ContextType::TEST;
}
public function getMetadata(): array
{
return array_merge([
'type' => $this->type->value,
'sapi' => php_sapi_name(),
'script' => $_SERVER['argv'][0] ?? null,
'command_line' => implode(' ', $_SERVER['argv'] ?? []),
'pid' => getmypid(),
], $this->metadata);
}
public static function detect(): self
{
// Test Environment
if (defined('PHPUNIT_COMPOSER_INSTALL') ||
isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') {
return new self(ContextType::TEST, ['detected_by' => 'phpunit_or_env']);
}
// Web Request
if (php_sapi_name() !== 'cli') {
return new self(ContextType::WEB, ['detected_by' => 'sapi']);
}
// CLI Detection
$scriptName = $_SERVER['argv'][0] ?? '';
$commandLine = implode(' ', $_SERVER['argv'] ?? []);
$type = match(true) {
str_contains($scriptName, 'worker') => ContextType::WORKER,
str_contains($scriptName, 'artisan') => ContextType::CONSOLE,
str_contains($commandLine, 'pest') => ContextType::TEST,
str_contains($commandLine, 'phpunit') => ContextType::TEST,
default => ContextType::CLI_SCRIPT
};
return new self($type, [
'detected_by' => 'cli_analysis',
'script_name' => $scriptName,
'command_line' => $commandLine
]);
}
public static function forWorker(): self
{
return new self(ContextType::WORKER, ['forced_by' => 'worker']);
}
public static function forTest(): self
{
return new self(ContextType::TEST, ['forced_by' => 'test']);
}
public static function forConsole(): self
{
return new self(ContextType::CONSOLE, ['forced_by' => 'console']);
}
public static function forWeb(): self
{
return new self(ContextType::WEB, ['forced_by' => 'web']);
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheInitializer;
use App\Framework\Config\Configuration;
use App\Framework\Config\Environment;
use App\Framework\Config\TypedConfigInitializer;
use App\Framework\Config\TypedConfiguration;
use App\Framework\Console\ConsoleApplication;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Context\ExecutionContext;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Container;
use App\Framework\ErrorHandling\ErrorHandler;
use App\Framework\Http\ResponseEmitter;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceMeter;
/**
* Verantwortlich für die grundlegende Initialisierung der Anwendung
*/
final readonly class AppBootstrapper
{
private DefaultContainer $container;
private ContainerBootstrapper $bootstrapper;
public function __construct(
private string $basePath,
private PerformanceMeter $meter,
private array $config = [],
){
$this->container = new DefaultContainer();
$this->bootstrapper = new ContainerBootstrapper($this->container);
$env = Environment::fromFile($this->basePath . '/.env');
$this->container->instance(TypedConfiguration::class, new TypedConfigInitializer($env)($this->container));
// ExecutionContext detection sollte das erste sein, das nach dem Instanzieren des containers passiert. noch bevor dem bootstrap des containers.
$executionContext = ExecutionContext::detect();
$this->container->instance(ExecutionContext::class, $executionContext);
error_log("AppBootstrapper: Context detected as {$executionContext->getType()->value}");
error_log('AppBootstrapper: Context metadata: ' . json_encode($executionContext->getMetadata()));
}
public function bootstrapWeb(): Application
{
$this->bootstrap();
$this->registerWebErrorHandler();
$this->registerApplication();
return $this->container->get(Application::class);
}
public function bootstrapConsole(): ConsoleApplication
{
$this->bootstrap();
$this->registerCliErrorHandler();
$this->registerConsoleApplication();
return $this->container->get(ConsoleApplication::class);
}
public function bootstrapWorker(): Container
{
$this->bootstrap();
$this->registerCliErrorHandler();
$consoleOutput = new ConsoleOutput();
$this->container->instance(ConsoleOutput::class, $consoleOutput);
return $this->container;
}
private function bootstrap(): void
{
$this->meter->startMeasure('bootstrap:start', PerformanceCategory::SYSTEM);
$this->bootstrapper->bootstrap($this->basePath, $this->meter, $this->config);
// ErrorHandler wird jetzt kontextabhängig registriert
// $this->container->get(ErrorHandler::class)->register();
$this->meter->endMeasure('bootstrap:end');
}
private function registerWebErrorHandler(): void
{
$this->container->get(ErrorHandler::class)->register();
}
private function registerCliErrorHandler(): void
{
$output = $this->container->has(ConsoleOutput::class)
? $this->container->get(ConsoleOutput::class)
: new ConsoleOutput();
$cliErrorHandler = new \App\Framework\ErrorHandling\CliErrorHandler($output);
$cliErrorHandler->register();
}
private function registerApplication(): void
{
$this->container->singleton(Application::class, function (Container $c) {
return new Application(
$c,
$c->get(PathProvider::class),
$c->get(ResponseEmitter::class),
$c->get(Configuration::class)
);
});
}
private function registerConsoleApplication(): void
{
$this->container->singleton(ConsoleApplication::class, function (Container $c) {
return new ConsoleApplication(
$c,
'console',
'My Console App',
null,
);
});
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
use App\Framework\Cache\Cache;
use App\Framework\Config\Configuration;
use App\Framework\Core\Events\AfterEmitResponse;
use App\Framework\Core\Events\AfterHandleRequest;
use App\Framework\Core\Events\ApplicationBooted;
use App\Framework\Core\Events\BeforeEmitResponse;
use App\Framework\Core\Events\BeforeHandleRequest;
use App\Framework\Core\Events\EventCompilerPass;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\Events\OnEvent;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Http\MiddlewareManager;
use App\Framework\Http\Request;
use App\Framework\Http\ResponseEmitter;
use App\Framework\Router\HttpRouter;
use DateTimeImmutable;
final readonly class Application
{
private MiddlewareManager $middlewareManager;
private AttributeDiscoveryService $discoveryService;
private EventDispatcher $eventDispatcher;
public function __construct(
private Container $container,
private PathProvider $pathProvider,
private ResponseEmitter $responseEmitter,
private Configuration $config
) {
// Middleware-Manager initialisieren
$this->middlewareManager = $this->container->get(MiddlewareManager::class);
$this->eventDispatcher = $container->get(EventDispatcher::class);
// Discovery-Service initialisieren
#$this->discoveryService = new AttributeDiscoveryService($container, $pathProvider, $config);
}
/**
* Führt die Anwendung aus
*/
public function run(): void
{
// ApplicationBooted-Event dispatchen
$environment = $this->config->get('environment', 'dev');
$version = $this->config->get('app.version', 'dev');
$bootEvent = new ApplicationBooted(
new DateTimeImmutable(),
$environment,
$version
);
$this->event($bootEvent);
// Attribute verarbeiten und Komponenten einrichten
#$this->setupApplicationComponents();
// Sicherstellen, dass ein Router registriert wurde
if (!$this->container->has(HttpRouter::class)) {
throw new \RuntimeException('Kritischer Fehler: Router wurde nicht initialisiert');
}
$this->event(new BeforeHandleRequest);
$this->event(new AfterHandleRequest);
$request = $this->container->get(Request::class);
$response = $this->middlewareManager->chain->handle($request);
$this->event(new BeforeEmitResponse);
// Response ausgeben
$this->responseEmitter->emit($response);
$this->event(new AfterEmitResponse);
}
private function event(object $event): void
{
$this->eventDispatcher->dispatch($event);
}
/**
* Gibt einen Konfigurationswert zurück
*/
public function config(string $key, mixed $default = null): mixed
{
return $this->config->get($key, $default);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Framework\Core;
interface AttributeCompiler
{
/**
* Gibt die Attributklasse zurück, für die dieser Compiler zuständig ist
*/
public function getAttributeClass(): string;
/**
* Kompiliert die gemappten Attributdaten in eine optimierte Form
*/
public function compile(array $data): mixed;
}

Some files were not shown because too many files have changed in this diff Show More