chore: complete update
This commit is contained in:
89
src/Framework/Analytics/AnalyticsEvent.php
Normal file
89
src/Framework/Analytics/AnalyticsEvent.php
Normal 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)
|
||||
];
|
||||
}
|
||||
}
|
||||
20
src/Framework/Analytics/AnalyticsInterface.php
Normal file
20
src/Framework/Analytics/AnalyticsInterface.php
Normal 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;
|
||||
}
|
||||
111
src/Framework/Analytics/AnalyticsService.php
Normal file
111
src/Framework/Analytics/AnalyticsService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
62
src/Framework/Analytics/Middleware/AnalyticsMiddleware.php
Normal file
62
src/Framework/Analytics/Middleware/AnalyticsMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/Framework/Analytics/Queue/AnalyticsProcessor.php
Normal file
31
src/Framework/Analytics/Queue/AnalyticsProcessor.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
68
src/Framework/Analytics/Storage/FileAnalyticsStorage.php
Normal file
68
src/Framework/Analytics/Storage/FileAnalyticsStorage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Framework/Api/ApiException.php
Normal file
32
src/Framework/Api/ApiException.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/Framework/Api/ApiRequestTrait.php
Normal file
93
src/Framework/Api/ApiRequestTrait.php
Normal 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) ?: [];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
20
src/Framework/Attributes/StaticPage.php
Normal file
20
src/Framework/Attributes/StaticPage.php
Normal 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
|
||||
) {}
|
||||
}
|
||||
11
src/Framework/Auth/Auth.php
Normal file
11
src/Framework/Auth/Auth.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Auth;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
|
||||
class Auth
|
||||
{
|
||||
|
||||
}
|
||||
21
src/Framework/Auth/AuthMapper.php
Normal file
21
src/Framework/Auth/AuthMapper.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
18
src/Framework/Cache/Cache.php
Normal file
18
src/Framework/Cache/Cache.php
Normal 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;
|
||||
}
|
||||
32
src/Framework/Cache/CacheDecorator.php
Normal file
32
src/Framework/Cache/CacheDecorator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/Framework/Cache/CacheDriver.php
Normal file
12
src/Framework/Cache/CacheDriver.php
Normal 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;
|
||||
}
|
||||
70
src/Framework/Cache/CacheInitializer.php
Normal file
70
src/Framework/Cache/CacheInitializer.php
Normal 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();
|
||||
}
|
||||
}
|
||||
21
src/Framework/Cache/CacheItem.php
Normal file
21
src/Framework/Cache/CacheItem.php
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/Framework/Cache/CachePrefix.php
Normal file
12
src/Framework/Cache/CachePrefix.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
enum CachePrefix: string
|
||||
{
|
||||
case GENERAL = 'cache:';
|
||||
|
||||
case QUERY = 'query_cache:';
|
||||
|
||||
#case SESSION = 'session:';
|
||||
}
|
||||
15
src/Framework/Cache/Cacheable.php
Normal file
15
src/Framework/Cache/Cacheable.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
23
src/Framework/Cache/Commands/ClearCache.php
Normal file
23
src/Framework/Cache/Commands/ClearCache.php
Normal 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");
|
||||
}
|
||||
}
|
||||
46
src/Framework/Cache/Compression/GzipCompression.php
Normal file
46
src/Framework/Cache/Compression/GzipCompression.php
Normal 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;
|
||||
}
|
||||
}
|
||||
23
src/Framework/Cache/Compression/NullCompression.php
Normal file
23
src/Framework/Cache/Compression/NullCompression.php
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/Framework/Cache/CompressionAlgorithm.php
Normal file
22
src/Framework/Cache/CompressionAlgorithm.php
Normal 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;
|
||||
}
|
||||
168
src/Framework/Cache/CompressionCacheDecorator.php
Normal file
168
src/Framework/Cache/CompressionCacheDecorator.php
Normal 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;
|
||||
}
|
||||
*/
|
||||
}
|
||||
57
src/Framework/Cache/Driver/ApcuCache.php
Normal file
57
src/Framework/Cache/Driver/ApcuCache.php
Normal 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();
|
||||
}
|
||||
}
|
||||
154
src/Framework/Cache/Driver/FileCache.php
Normal file
154
src/Framework/Cache/Driver/FileCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
39
src/Framework/Cache/Driver/InMemoryCache.php
Normal file
39
src/Framework/Cache/Driver/InMemoryCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
35
src/Framework/Cache/Driver/NullCache.php
Normal file
35
src/Framework/Cache/Driver/NullCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
134
src/Framework/Cache/Driver/RedisCache.php
Normal file
134
src/Framework/Cache/Driver/RedisCache.php
Normal 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);
|
||||
}
|
||||
}
|
||||
38
src/Framework/Cache/FileCacheCleaner.php
Normal file
38
src/Framework/Cache/FileCacheCleaner.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
48
src/Framework/Cache/GeneralCache.php
Normal file
48
src/Framework/Cache/GeneralCache.php
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
103
src/Framework/Cache/LoggingCacheDecorator.php
Normal file
103
src/Framework/Cache/LoggingCacheDecorator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
124
src/Framework/Cache/MultiLevelCache.php
Normal file
124
src/Framework/Cache/MultiLevelCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
9
src/Framework/Cache/Serializer.php
Normal file
9
src/Framework/Cache/Serializer.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface Serializer
|
||||
{
|
||||
public function serialize(mixed $value): string;
|
||||
public function unserialize(string $value): mixed;
|
||||
}
|
||||
25
src/Framework/Cache/Serializer/JsonSerializer.php
Normal file
25
src/Framework/Cache/Serializer/JsonSerializer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/Framework/Cache/Serializer/PhpSerializer.php
Normal file
19
src/Framework/Cache/Serializer/PhpSerializer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
11
src/Framework/Cache/TaggedCache.php
Normal file
11
src/Framework/Cache/TaggedCache.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class TaggedCache
|
||||
{
|
||||
public function __construct(
|
||||
public Cache $cache
|
||||
) {}
|
||||
}
|
||||
8
src/Framework/CommandBus/CommandBus.php
Normal file
8
src/Framework/CommandBus/CommandBus.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\CommandBus;
|
||||
|
||||
interface CommandBus
|
||||
{
|
||||
public function dispatch(object $command): mixed;
|
||||
}
|
||||
32
src/Framework/CommandBus/CommandBusInitializer.php
Normal file
32
src/Framework/CommandBus/CommandBusInitializer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/Framework/CommandBus/CommandHandler.php
Normal file
12
src/Framework/CommandBus/CommandHandler.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\CommandBus;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(\Attribute::TARGET_METHOD)]
|
||||
final class CommandHandler
|
||||
{
|
||||
|
||||
}
|
||||
43
src/Framework/CommandBus/CommandHandlerCompiler.php
Normal file
43
src/Framework/CommandBus/CommandHandlerCompiler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
25
src/Framework/CommandBus/CommandHandlerDescriptor.php
Normal file
25
src/Framework/CommandBus/CommandHandlerDescriptor.php
Normal 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
33
src/Framework/CommandBus/CommandHandlerMapper.php
Normal file
33
src/Framework/CommandBus/CommandHandlerMapper.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
58
src/Framework/CommandBus/CommandHandlersCollection.php
Normal file
58
src/Framework/CommandBus/CommandHandlersCollection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
103
src/Framework/CommandBus/DefaultCommandBus.php
Normal file
103
src/Framework/CommandBus/DefaultCommandBus.php
Normal 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();
|
||||
}
|
||||
}
|
||||
16
src/Framework/CommandBus/Exceptions/NoHandlerFound.php
Normal file
16
src/Framework/CommandBus/Exceptions/NoHandlerFound.php
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
8
src/Framework/CommandBus/Middleware.php
Normal file
8
src/Framework/CommandBus/Middleware.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\CommandBus;
|
||||
|
||||
interface Middleware
|
||||
{
|
||||
public function handle(object $command, callable $next): mixed;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
26
src/Framework/CommandBus/Middleware/LoggingMiddleware.php
Normal file
26
src/Framework/CommandBus/Middleware/LoggingMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
9
src/Framework/CommandBus/ShouldQueue.php
Normal file
9
src/Framework/CommandBus/ShouldQueue.php
Normal 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 …)
|
||||
}
|
||||
18
src/Framework/Config/AppConfig.php
Normal file
18
src/Framework/Config/AppConfig.php
Normal 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
|
||||
) {}
|
||||
}
|
||||
117
src/Framework/Config/Configuration.php
Normal file
117
src/Framework/Config/Configuration.php
Normal 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;
|
||||
}
|
||||
}
|
||||
11
src/Framework/Config/EnvKey.php
Normal file
11
src/Framework/Config/EnvKey.php
Normal 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';
|
||||
}
|
||||
143
src/Framework/Config/Environment.php
Normal file
143
src/Framework/Config/Environment.php
Normal 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));
|
||||
}
|
||||
}
|
||||
9
src/Framework/Config/EnvironmentType.php
Normal file
9
src/Framework/Config/EnvironmentType.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
enum EnvironmentType: string
|
||||
{
|
||||
case DEV = 'development';
|
||||
case PROD = 'production';
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
47
src/Framework/Config/TypedConfigInitializer.php
Normal file
47
src/Framework/Config/TypedConfigInitializer.php
Normal 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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/Framework/Config/TypedConfiguration.php
Normal file
15
src/Framework/Config/TypedConfiguration.php
Normal 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,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
257
src/Framework/Console/Components/InteractiveMenu.php
Normal file
257
src/Framework/Console/Components/InteractiveMenu.php
Normal 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().
|
||||
}
|
||||
205
src/Framework/Console/Components/Table.php
Normal file
205
src/Framework/Console/Components/Table.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/Framework/Console/Components/TextBox.php
Normal file
124
src/Framework/Console/Components/TextBox.php
Normal 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 ?: [''];
|
||||
}
|
||||
}
|
||||
215
src/Framework/Console/Components/TreeHelper.php
Normal file
215
src/Framework/Console/Components/TreeHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
168
src/Framework/Console/ConsoleApplication.php
Normal file
168
src/Framework/Console/ConsoleApplication.php
Normal 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]");
|
||||
}
|
||||
}
|
||||
52
src/Framework/Console/ConsoleColor.php
Normal file
52
src/Framework/Console/ConsoleColor.php
Normal 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";
|
||||
}
|
||||
|
||||
}
|
||||
15
src/Framework/Console/ConsoleCommand.php
Normal file
15
src/Framework/Console/ConsoleCommand.php
Normal 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 = ''
|
||||
) {}
|
||||
}
|
||||
24
src/Framework/Console/ConsoleCommandMapper.php
Normal file
24
src/Framework/Console/ConsoleCommandMapper.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
20
src/Framework/Console/ConsoleFormat.php
Normal file
20
src/Framework/Console/ConsoleFormat.php
Normal 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";
|
||||
}
|
||||
}
|
||||
160
src/Framework/Console/ConsoleInput.php
Normal file
160
src/Framework/Console/ConsoleInput.php
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/Framework/Console/ConsoleInputInterface.php
Normal file
61
src/Framework/Console/ConsoleInputInterface.php
Normal 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;
|
||||
}
|
||||
166
src/Framework/Console/ConsoleOutput.php
Normal file
166
src/Framework/Console/ConsoleOutput.php
Normal 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);
|
||||
}
|
||||
}
|
||||
61
src/Framework/Console/ConsoleOutputInterface.php
Normal file
61
src/Framework/Console/ConsoleOutputInterface.php
Normal 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;
|
||||
}
|
||||
89
src/Framework/Console/ConsoleStyle.php
Normal file
89
src/Framework/Console/ConsoleStyle.php
Normal 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);
|
||||
}
|
||||
}
|
||||
193
src/Framework/Console/DemoCommand.php
Normal file
193
src/Framework/Console/DemoCommand.php
Normal 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';
|
||||
}
|
||||
}
|
||||
173
src/Framework/Console/DemoCommand/ScreenDemoCommand.php
Normal file
173
src/Framework/Console/DemoCommand/ScreenDemoCommand.php
Normal 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);
|
||||
}
|
||||
}
|
||||
50
src/Framework/Console/Examples/ProgressBarExample.php
Normal file
50
src/Framework/Console/Examples/ProgressBarExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
72
src/Framework/Console/Examples/SpinnerExample.php
Normal file
72
src/Framework/Console/Examples/SpinnerExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
60
src/Framework/Console/Examples/TableExample.php
Normal file
60
src/Framework/Console/Examples/TableExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
64
src/Framework/Console/Examples/TextBoxExample.php
Normal file
64
src/Framework/Console/Examples/TextBoxExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
89
src/Framework/Console/Examples/TreeExample.php
Normal file
89
src/Framework/Console/Examples/TreeExample.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
7
src/Framework/Console/Exceptions/ConsoleException.php
Normal file
7
src/Framework/Console/Exceptions/ConsoleException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Console\Exceptions;
|
||||
|
||||
class ConsoleException extends \Exception
|
||||
{
|
||||
}
|
||||
74
src/Framework/Console/ExitCode.php
Normal file
74
src/Framework/Console/ExitCode.php
Normal 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();
|
||||
}
|
||||
}
|
||||
185
src/Framework/Console/ProgressBar.php
Normal file
185
src/Framework/Console/ProgressBar.php
Normal 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);
|
||||
}
|
||||
}
|
||||
106
src/Framework/Console/README.md
Normal file
106
src/Framework/Console/README.md
Normal 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.
|
||||
15
src/Framework/Console/Screen/ClearStrategy.php
Normal file
15
src/Framework/Console/Screen/ClearStrategy.php
Normal 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
|
||||
}
|
||||
127
src/Framework/Console/Screen/Cursor.php
Normal file
127
src/Framework/Console/Screen/Cursor.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
src/Framework/Console/Screen/CursorControlCode.php
Normal file
48
src/Framework/Console/Screen/CursorControlCode.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
107
src/Framework/Console/Screen/Display.php
Normal file
107
src/Framework/Console/Screen/Display.php
Normal 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;
|
||||
}
|
||||
}
|
||||
38
src/Framework/Console/Screen/ScreenControlCode.php
Normal file
38
src/Framework/Console/Screen/ScreenControlCode.php
Normal 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}";
|
||||
}
|
||||
}
|
||||
145
src/Framework/Console/Screen/ScreenManager.php
Normal file
145
src/Framework/Console/Screen/ScreenManager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/Framework/Console/Screen/ScreenType.php
Normal file
17
src/Framework/Console/Screen/ScreenType.php
Normal 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
|
||||
}
|
||||
119
src/Framework/Console/Spinner.php
Normal file
119
src/Framework/Console/Spinner.php
Normal 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();
|
||||
}
|
||||
}
|
||||
24
src/Framework/Console/SpinnerStyle.php
Normal file
24
src/Framework/Console/SpinnerStyle.php
Normal 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 => ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙']
|
||||
};
|
||||
}
|
||||
}
|
||||
13
src/Framework/Context/ContextType.php
Normal file
13
src/Framework/Context/ContextType.php
Normal 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';
|
||||
}
|
||||
109
src/Framework/Context/ExecutionContext.php
Normal file
109
src/Framework/Context/ExecutionContext.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
133
src/Framework/Core/AppBootstrapper.php
Normal file
133
src/Framework/Core/AppBootstrapper.php
Normal 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
103
src/Framework/Core/Application.php
Normal file
103
src/Framework/Core/Application.php
Normal 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);
|
||||
}
|
||||
}
|
||||
16
src/Framework/Core/AttributeCompiler.php
Normal file
16
src/Framework/Core/AttributeCompiler.php
Normal 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
Reference in New Issue
Block a user