Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -4,21 +4,30 @@ 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\EncryptedEnvLoader;
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\EnvironmentType;
|
||||
use App\Framework\Config\SecretManager;
|
||||
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\Core\Events\EventDispatcher;
|
||||
use App\Framework\Core\Events\EventDispatcherInterface;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\ErrorHandling\CliErrorHandler;
|
||||
use App\Framework\ErrorHandling\ErrorHandler;
|
||||
use App\Framework\Http\MiddlewareManager;
|
||||
use App\Framework\Http\MiddlewareManagerInterface;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Http\ServerEnvironment;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\MemoryMonitor;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Performance\PerformanceMeter;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
|
||||
/**
|
||||
* Verantwortlich für die grundlegende Initialisierung der Anwendung
|
||||
@@ -30,34 +39,49 @@ final readonly class AppBootstrapper
|
||||
private ContainerBootstrapper $bootstrapper;
|
||||
|
||||
public function __construct(
|
||||
private string $basePath,
|
||||
private PerformanceMeter $meter,
|
||||
private array $config = [],
|
||||
){
|
||||
private string $basePath,
|
||||
private PerformanceCollectorInterface $collector,
|
||||
private MemoryMonitor $memoryMonitor,
|
||||
#private array $config = [],
|
||||
) {
|
||||
$this->container = new DefaultContainer();
|
||||
$this->bootstrapper = new ContainerBootstrapper($this->container);
|
||||
|
||||
// Initialize environment with encryption support
|
||||
$env = $this->initializeEnvironment();
|
||||
|
||||
$env = Environment::fromFile($this->basePath . '/.env');
|
||||
// Make Environment available throughout the application
|
||||
$this->container->instance(Environment::class, $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();
|
||||
// ExecutionContext detection sollte das erste sein, das nach dem Instanziieren des containers passiert. noch bevor dem bootstrap des containers.
|
||||
$executionContext = ExecutionContext::detect($env);
|
||||
$this->container->instance(ExecutionContext::class, $executionContext);
|
||||
|
||||
error_log("AppBootstrapper: Context detected as {$executionContext->getType()->value}");
|
||||
error_log('AppBootstrapper: Context metadata: ' . json_encode($executionContext->getMetadata()));
|
||||
// Register MemoryMonitor as singleton
|
||||
$this->container->singleton(MemoryMonitor::class, $this->memoryMonitor);
|
||||
|
||||
// Only log context in development - production doesn't need this noise
|
||||
$envType = EnvironmentType::fromEnvironment($env);
|
||||
#error_log("AppBootstrapper: 🚀 Context detected as {$executionContext->getType()->value}");
|
||||
#error_log("AppBootstrapper: Debug - isProduction: " . ($envType->isProduction() ? 'true' : 'false'));
|
||||
}
|
||||
|
||||
public function bootstrapWeb(): Application
|
||||
public function bootstrapWeb(): ApplicationInterface
|
||||
{
|
||||
$this->bootstrap();
|
||||
$this->registerWebErrorHandler();
|
||||
$this->registerApplication();
|
||||
|
||||
return $this->container->get(Application::class);
|
||||
$mm = $this->container->get(MiddlewareManager::class);
|
||||
$this->container->instance(MiddlewareManagerInterface::class, $mm);
|
||||
|
||||
$ed = $this->container->get(EventDispatcher::class);
|
||||
$this->container->instance(EventDispatcherInterface::class, $ed);
|
||||
|
||||
return $this->container->get(ApplicationInterface::class);
|
||||
}
|
||||
|
||||
public function bootstrapConsole(): ConsoleApplication
|
||||
@@ -80,16 +104,31 @@ final readonly class AppBootstrapper
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
public function bootstrapWebSocket(): 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->collector->startTiming('bootstrap', PerformanceCategory::SYSTEM);
|
||||
|
||||
$this->bootstrapper->bootstrap($this->basePath, $this->meter, $this->config);
|
||||
$this->bootstrapper->bootstrap($this->basePath, $this->collector);
|
||||
|
||||
$this->collector->endTiming('bootstrap');
|
||||
|
||||
// Initialize secrets management after container is bootstrapped
|
||||
$env = $this->container->get(Environment::class);
|
||||
$this->initializeSecretsManagement($env);
|
||||
|
||||
// ErrorHandler wird jetzt kontextabhängig registriert
|
||||
// $this->container->get(ErrorHandler::class)->register();
|
||||
|
||||
$this->meter->endMeasure('bootstrap:end');
|
||||
}
|
||||
|
||||
private function registerWebErrorHandler(): void
|
||||
@@ -103,18 +142,17 @@ final readonly class AppBootstrapper
|
||||
? $this->container->get(ConsoleOutput::class)
|
||||
: new ConsoleOutput();
|
||||
|
||||
$cliErrorHandler = new \App\Framework\ErrorHandling\CliErrorHandler($output);
|
||||
$cliErrorHandler = new CliErrorHandler($output);
|
||||
$cliErrorHandler->register();
|
||||
}
|
||||
|
||||
private function registerApplication(): void
|
||||
{
|
||||
$this->container->singleton(Application::class, function (Container $c) {
|
||||
$this->container->singleton(ApplicationInterface::class, function (Container $c) {
|
||||
return new Application(
|
||||
$c,
|
||||
$c->get(PathProvider::class),
|
||||
$c->get(ResponseEmitter::class),
|
||||
$c->get(Configuration::class)
|
||||
$c->get(TypedConfiguration::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -130,4 +168,64 @@ final readonly class AppBootstrapper
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize environment with encryption support
|
||||
*/
|
||||
private function initializeEnvironment(): Environment
|
||||
{
|
||||
// First, try to load basic environment to get encryption key
|
||||
$basicEnv = Environment::fromFile($this->basePath . '/.env');
|
||||
$encryptionKey = $basicEnv->get('ENCRYPTION_KEY');
|
||||
|
||||
// If we have an encryption key, use the encrypted loader
|
||||
if ($encryptionKey !== null) {
|
||||
try {
|
||||
// These dependencies will be resolved later through the container
|
||||
$randomGenerator = $this->container->get(RandomGenerator::class);
|
||||
$encryptionFactory = new EncryptionFactory($randomGenerator);
|
||||
$encryptedLoader = new EncryptedEnvLoader($encryptionFactory, $randomGenerator);
|
||||
|
||||
return $encryptedLoader->loadEnvironment($this->basePath, $encryptionKey);
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback to basic environment if encryption fails
|
||||
error_log("Failed to load encrypted environment: " . $e->getMessage());
|
||||
|
||||
return $basicEnv;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to basic environment loading
|
||||
return $basicEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize secrets management after container is bootstrapped
|
||||
*/
|
||||
private function initializeSecretsManagement(Environment $env): void
|
||||
{
|
||||
$encryptionKey = $env->get('ENCRYPTION_KEY');
|
||||
|
||||
if ($encryptionKey === null) {
|
||||
return; // No secrets management without encryption key
|
||||
}
|
||||
|
||||
try {
|
||||
$randomGenerator = $this->container->get(RandomGenerator::class);
|
||||
$serverEnvironment = $this->container->get(ServerEnvironment::class);
|
||||
$encryptionFactory = new EncryptionFactory($randomGenerator);
|
||||
$encryption = $encryptionFactory->createBest($encryptionKey);
|
||||
|
||||
$secretManager = new SecretManager(
|
||||
$env,
|
||||
$encryption,
|
||||
$serverEnvironment,
|
||||
$randomGenerator
|
||||
);
|
||||
|
||||
$this->container->instance(SecretManager::class, $secretManager);
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Failed to initialize secrets management: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,44 +4,43 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\Config\TypedConfiguration;
|
||||
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\Core\Events\EventDispatcherInterface;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Http\MiddlewareManager;
|
||||
use App\Framework\Http\MiddlewareManagerInterface;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
use App\Framework\Router\HttpRouter;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class Application
|
||||
final readonly class Application implements ApplicationInterface
|
||||
{
|
||||
private MiddlewareManager $middlewareManager;
|
||||
private AttributeDiscoveryService $discoveryService;
|
||||
private EventDispatcher $eventDispatcher;
|
||||
private MiddlewareManagerInterface $middlewareManager;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
private PerformanceCollectorInterface $performanceCollector;
|
||||
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private PathProvider $pathProvider,
|
||||
private ResponseEmitter $responseEmitter,
|
||||
private Configuration $config
|
||||
private TypedConfiguration $config,
|
||||
?MiddlewareManagerInterface $middlewareManager = null,
|
||||
?EventDispatcherInterface $eventDispatcher = null,
|
||||
?PerformanceCollectorInterface $performanceCollector = null,
|
||||
) {
|
||||
// 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);
|
||||
// Dependencies optional injizieren oder aus Container holen
|
||||
$this->middlewareManager = $middlewareManager ?? $this->container->get(MiddlewareManagerInterface::class);
|
||||
$this->eventDispatcher = $eventDispatcher ?? $this->container->get(EventDispatcherInterface::class);
|
||||
$this->performanceCollector = $performanceCollector ?? $this->container->get(PerformanceCollectorInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,55 +48,70 @@ final readonly class Application
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// ApplicationBooted-Event dispatchen
|
||||
$environment = $this->config->get('environment', 'dev');
|
||||
$version = $this->config->get('app.version', 'dev');
|
||||
$this->boot();
|
||||
$response = $this->handleRequest($this->container->get(Request::class));
|
||||
$this->emitResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap der Anwendung
|
||||
*/
|
||||
private function boot(): void
|
||||
{
|
||||
// ApplicationBooted-Event dispatchen
|
||||
$environment = $this->config->app->environment;
|
||||
$version = $this->config->app->version;
|
||||
|
||||
$bootEvent = new ApplicationBooted(
|
||||
new DateTimeImmutable(),
|
||||
$environment,
|
||||
$version
|
||||
bootTime: new DateTimeImmutable(),
|
||||
environment: $environment,
|
||||
version: $version
|
||||
);
|
||||
|
||||
$this->event($bootEvent);
|
||||
|
||||
// Attribute verarbeiten und Komponenten einrichten
|
||||
#$this->setupApplicationComponents();
|
||||
|
||||
// Sicherstellen, dass ein Router registriert wurde
|
||||
if (!$this->container->has(HttpRouter::class)) {
|
||||
if (! $this->container->has(HttpRouter::class)) {
|
||||
throw new \RuntimeException('Kritischer Fehler: Router wurde nicht initialisiert');
|
||||
}
|
||||
}
|
||||
|
||||
$this->event(new BeforeHandleRequest);
|
||||
/**
|
||||
* Verarbeitet den HTTP-Request
|
||||
*/
|
||||
private function handleRequest(Request $request): Response
|
||||
{
|
||||
$this->event(new BeforeHandleRequest());
|
||||
|
||||
$response = $this->performanceCollector->measure(
|
||||
'handle_request',
|
||||
PerformanceCategory::SYSTEM,
|
||||
function () use ($request) {
|
||||
return $this->middlewareManager->chain->handle($request);
|
||||
}
|
||||
);
|
||||
|
||||
$this->event(new AfterHandleRequest);
|
||||
#$response = $this->middlewareManager->chain->handle($request);
|
||||
|
||||
$request = $this->container->get(Request::class);
|
||||
$this->event(new AfterHandleRequest());
|
||||
|
||||
$response = $this->middlewareManager->chain->handle($request);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->event(new BeforeEmitResponse);
|
||||
/**
|
||||
* Gibt die HTTP-Response aus
|
||||
*/
|
||||
private function emitResponse(Response $response): void
|
||||
{
|
||||
$this->event(new BeforeEmitResponse());
|
||||
|
||||
// Response ausgeben
|
||||
$this->responseEmitter->emit($response);
|
||||
|
||||
$this->event(new AfterEmitResponse);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
10
src/Framework/Core/ApplicationInterface.php
Normal file
10
src/Framework/Core/ApplicationInterface.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
interface ApplicationInterface
|
||||
{
|
||||
public function run(): void;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
interface AttributeCompiler
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Auth\AuthMapper;
|
||||
use App\Framework\CommandBus\CommandHandlerCompiler;
|
||||
use App\Framework\CommandBus\CommandHandlerMapper;
|
||||
use App\Framework\Config\Configuration;
|
||||
use App\Framework\Console\ConsoleCommandMapper;
|
||||
use App\Framework\DI\AttributeProcessorRegistry;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\InitializerMapper;
|
||||
use App\Framework\QueryBus\QueryHandlerCompiler;
|
||||
use App\Framework\QueryBus\QueryHandlerMapper;
|
||||
use Archive\Async1\DiscoveryFactory;
|
||||
|
||||
/**
|
||||
* Verwaltet die Entdeckung und Verarbeitung von Attributen
|
||||
*/
|
||||
readonly class AttributeDiscoveryService
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private PathProvider $pathProvider,
|
||||
private Configuration $config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Führt die Attribut-Discovery durch und verarbeitet die gefundenen Attribute
|
||||
*/
|
||||
public function discoverAndProcess(): array
|
||||
{
|
||||
$useAsyncDiscovery = $this->config->get('async_discovery', true);
|
||||
$srcDir = $this->pathProvider->getSourcePath();
|
||||
|
||||
// Discovery erstellen
|
||||
$discoveryFactory = new DiscoveryFactory();
|
||||
$discovery = $discoveryFactory->create($useAsyncDiscovery, $srcDir);
|
||||
|
||||
// Attribute-Processor-Registry initialisieren
|
||||
$processorRegistry = $this->createProcessorRegistry();
|
||||
|
||||
// Attribute entdecken
|
||||
$results = $discovery->discover($srcDir);
|
||||
|
||||
// Alle Attribute verarbeiten
|
||||
return $processorRegistry->processAll($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt und konfiguriert die AttributeProcessorRegistry
|
||||
*/
|
||||
private function createProcessorRegistry(): AttributeProcessorRegistry
|
||||
{
|
||||
$processorRegistry = new AttributeProcessorRegistry($this->container);
|
||||
|
||||
// Mapper registrieren
|
||||
$this->registerMappers($processorRegistry);
|
||||
|
||||
// Compiler registrieren
|
||||
$this->registerCompilers($processorRegistry);
|
||||
|
||||
return $processorRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert alle AttributeMapper
|
||||
*/
|
||||
private function registerMappers(AttributeProcessorRegistry $registry): void
|
||||
{
|
||||
$registry
|
||||
->registerMapper(RouteMapper::class)
|
||||
->registerMapper(CommandHandlerMapper::class)
|
||||
->registerMapper(\App\Framework\Core\Events\EventHandlerMapper::class)
|
||||
->registerMapper(\App\Framework\EventBus\EventHandlerMapper::class)
|
||||
->registerMapper(QueryHandlerMapper::class)
|
||||
->registerMapper(ConsoleCommandMapper::class)
|
||||
->registerMapper(AuthMapper::class)
|
||||
|
||||
->registerMapper(InitializerMapper::class);
|
||||
|
||||
// Hier können weitere Mapper registriert werden
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert alle AttributeCompiler
|
||||
*/
|
||||
private function registerCompilers(AttributeProcessorRegistry $registry): void
|
||||
{
|
||||
$registry
|
||||
->registerCompiler(RouteCompiler::class)
|
||||
->registerCompiler(CommandHandlerCompiler::class)
|
||||
->registerCompiler(\App\Framework\Core\Events\EventHandlerCompiler::class)
|
||||
->registerCompiler(\App\Framework\EventBus\EventHandlerCompiler::class)
|
||||
->registerCompiler(QueryHandlerCompiler::class);
|
||||
|
||||
// Hier können weitere Compiler registriert werden
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Reflection\WrappedReflectionClass;
|
||||
use App\Framework\Reflection\WrappedReflectionMethod;
|
||||
|
||||
/**
|
||||
* Interface für AttributeMapper mit optimierter Performance
|
||||
* Interface für AttributeMapper mit Framework Reflection Module
|
||||
*/
|
||||
interface AttributeMapper
|
||||
{
|
||||
@@ -14,20 +18,10 @@ interface AttributeMapper
|
||||
public function getAttributeClass(): string;
|
||||
|
||||
/**
|
||||
* @param object $reflectionTarget ReflectionClass|ReflectionMethod
|
||||
* Maps attributes using Framework Reflection Module
|
||||
* @param WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget
|
||||
* @param object $attributeInstance
|
||||
* @return array|null
|
||||
*/
|
||||
public function map(object $reflectionTarget, object $attributeInstance): ?array;
|
||||
|
||||
/**
|
||||
* Prüft, ob dieses Attribut auf dem Reflector verarbeitet werden kann
|
||||
* (Schnelle Prüfung ohne vollständige Verarbeitung)
|
||||
*/
|
||||
#public function canProcess(\Reflector $reflector): bool;
|
||||
|
||||
/**
|
||||
* Gibt Metadaten über das Attribut zurück (für Debugging und Caching)
|
||||
*/
|
||||
#public function getAttributeMetadata(): array;
|
||||
public function map(WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget, object $attributeInstance): ?array;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
|
||||
final readonly class AttributeMapperLocator
|
||||
{
|
||||
private InterfaceImplementationLocator $locator;
|
||||
@@ -26,6 +23,7 @@ final readonly class AttributeMapperLocator
|
||||
{
|
||||
/** @var AttributeMapper[] $mappers */
|
||||
$mappers = $this->locator->locate($directory, AttributeMapper::class);
|
||||
|
||||
return $mappers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
use App\Framework\Discovery\ReflectionAwareVisitor;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
use App\Framework\Reflection\WrappedReflectionClass;
|
||||
|
||||
/**
|
||||
* Visitor zum Verarbeiten von Attributen in Klassen
|
||||
*/
|
||||
final class AttributeMappingVisitor implements FileVisitor
|
||||
final class AttributeMappingVisitor implements FileVisitor, ReflectionAwareVisitor
|
||||
{
|
||||
private array $attributeMappers = [];
|
||||
|
||||
private array $mappedAttributes = [];
|
||||
|
||||
private array $mapperByClass = [];
|
||||
|
||||
private array $processingStats = [];
|
||||
|
||||
/**
|
||||
@@ -45,7 +54,7 @@ final class AttributeMappingVisitor implements FileVisitor
|
||||
'classes_with_attributes' => 0,
|
||||
'total_attributes' => 0,
|
||||
'processed_attributes' => 0,
|
||||
'skipped_reflectors' => 0
|
||||
'skipped_reflectors' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -57,31 +66,32 @@ final class AttributeMappingVisitor implements FileVisitor
|
||||
'classes_with_attributes' => 0,
|
||||
'total_attributes' => 0,
|
||||
'processed_attributes' => 0,
|
||||
'skipped_reflectors' => 0
|
||||
'skipped_reflectors' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function onIncrementalScanComplete(): void
|
||||
{
|
||||
// Logging der Verarbeitungsstatistiken für inkrementellen Scan
|
||||
error_log("Inkrementelle Attribut-Verarbeitung abgeschlossen: " . json_encode($this->processingStats));
|
||||
// Processing stats removed for production
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
public function visitClass(ClassName $className, FilePath $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
// This method is kept for backward compatibility but should not be used
|
||||
// when ReflectionProvider is available. The UnifiedDiscoveryService will
|
||||
// prefer visitClassWithReflection() when possible.
|
||||
}
|
||||
|
||||
public function visitClassWithReflection(ClassName $className, FilePath $filePath, WrappedReflectionClass $reflection): void
|
||||
{
|
||||
$this->processingStats['total_classes']++;
|
||||
$hasAttributes = false;
|
||||
|
||||
try {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Schnelle Vorprüfung: Hat die Klasse überhaupt relevante Attribute?
|
||||
$relevantAttributes = $this->hasRelevantAttributes($reflection);
|
||||
if (!$relevantAttributes) {
|
||||
$relevantAttributes = $this->hasRelevantAttributesWrapped($reflection);
|
||||
if (! $relevantAttributes) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,26 +99,256 @@ final class AttributeMappingVisitor implements FileVisitor
|
||||
$this->processingStats['classes_with_attributes']++;
|
||||
|
||||
// Verarbeite alle Attribute auf Klassenebene
|
||||
$this->processAttributes($reflection->getAttributes(), $reflection);
|
||||
$this->processAttributesWrapped($reflection->getAttributes(), $reflection);
|
||||
|
||||
// Verarbeite alle Methoden
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
$methods = $reflection->getMethods();
|
||||
foreach ($methods as $method) {
|
||||
// Schnelle Vorprüfung für jede Methode
|
||||
if ($this->hasRelevantAttributes($method)) {
|
||||
$this->processAttributes($method->getAttributes(), $method);
|
||||
if ($this->hasRelevantAttributesWrappedMethod($method)) {
|
||||
$this->processAttributesWrappedMethod($method->getAttributes(), $method);
|
||||
}
|
||||
}
|
||||
|
||||
// Verarbeite alle Eigenschaften
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
$properties = $reflection->getProperties();
|
||||
foreach ($properties as $property) {
|
||||
// Schnelle Vorprüfung für jede Eigenschaft
|
||||
if ($this->hasRelevantAttributes($property)) {
|
||||
$this->processAttributes($property->getAttributes(), $property);
|
||||
if ($this->hasRelevantAttributesWrappedProperty($property)) {
|
||||
$this->processAttributesWrappedProperty($property->getAttributes(), $property);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fehler beim Verarbeiten der Klasse protokollieren
|
||||
error_log("Fehler bei der Attributverarbeitung von {$className}: " . $e->getMessage());
|
||||
// Silent failure - skip this class and continue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schnelle Vorprüfung, ob eine WrappedReflectionClass relevante Attribute hat
|
||||
*/
|
||||
private function hasRelevantAttributesWrapped(WrappedReflectionClass $reflection): bool
|
||||
{
|
||||
$attributes = $reflection->getAttributes();
|
||||
$this->processingStats['total_attributes'] += $attributes->count();
|
||||
|
||||
if ($attributes->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
// For wrapped reflection, we need to create native reflection for mapper compatibility
|
||||
try {
|
||||
$nativeReflection = new \ReflectionClass($reflection->getName());
|
||||
if ($mapper->canProcess($nativeReflection)) {
|
||||
return true;
|
||||
} else {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schnelle Vorprüfung für WrappedReflectionMethod
|
||||
*/
|
||||
private function hasRelevantAttributesWrappedMethod($method): bool
|
||||
{
|
||||
$attributes = $method->getAttributes();
|
||||
$this->processingStats['total_attributes'] += $attributes->count();
|
||||
|
||||
if ($attributes->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
// For wrapped reflection, we need to create native reflection for mapper compatibility
|
||||
try {
|
||||
$nativeClass = new \ReflectionClass($method->getDeclaringClass()->getName());
|
||||
$nativeMethod = $nativeClass->getMethod($method->getName());
|
||||
if ($mapper->canProcess($nativeMethod)) {
|
||||
return true;
|
||||
} else {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schnelle Vorprüfung für WrappedReflectionProperty
|
||||
*/
|
||||
private function hasRelevantAttributesWrappedProperty($property): bool
|
||||
{
|
||||
$attributes = $property->getAttributes();
|
||||
$this->processingStats['total_attributes'] += $attributes->count();
|
||||
|
||||
if ($attributes->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
// For wrapped reflection, we need to create native reflection for mapper compatibility
|
||||
try {
|
||||
$nativeClass = new \ReflectionClass($property->getDeclaringClass()->getName());
|
||||
$nativeProperty = $nativeClass->getProperty($property->getName());
|
||||
if ($mapper->canProcess($nativeProperty)) {
|
||||
return true;
|
||||
} else {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$this->processingStats['skipped_reflectors']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Attribute einer WrappedReflectionClass
|
||||
*/
|
||||
private function processAttributesWrapped($attributes, WrappedReflectionClass $reflection): void
|
||||
{
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
try {
|
||||
$nativeReflection = new \ReflectionClass($reflection->getName());
|
||||
if ($mapper->canProcess($nativeReflection)) {
|
||||
$attributeInstance = $attribute->newInstance();
|
||||
$mapper->map($attributeInstance, $nativeReflection);
|
||||
$this->processingStats['processed_attributes']++;
|
||||
|
||||
if (! isset($this->mappedAttributes[$attributeClass])) {
|
||||
$this->mappedAttributes[$attributeClass] = [];
|
||||
}
|
||||
|
||||
$reflectorKey = [
|
||||
'type' => 'class',
|
||||
'name' => $reflection->getName(),
|
||||
];
|
||||
|
||||
$this->mappedAttributes[$attributeClass][] = [
|
||||
'attribute' => $attributeInstance,
|
||||
'reflector' => $reflectorKey,
|
||||
'metadata' => $mapper->getAttributeMetadata(),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silent failure - skip this attribute and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Attribute einer WrappedReflectionMethod
|
||||
*/
|
||||
private function processAttributesWrappedMethod($attributes, $method): void
|
||||
{
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
try {
|
||||
$nativeClass = new \ReflectionClass($method->getDeclaringClass()->getName());
|
||||
$nativeMethod = $nativeClass->getMethod($method->getName());
|
||||
if ($mapper->canProcess($nativeMethod)) {
|
||||
$attributeInstance = $attribute->newInstance();
|
||||
$mapper->map($attributeInstance, $nativeMethod);
|
||||
$this->processingStats['processed_attributes']++;
|
||||
|
||||
if (! isset($this->mappedAttributes[$attributeClass])) {
|
||||
$this->mappedAttributes[$attributeClass] = [];
|
||||
}
|
||||
|
||||
$reflectorKey = [
|
||||
'type' => 'method',
|
||||
'class' => $method->getDeclaringClass()->getName(),
|
||||
'name' => $method->getName(),
|
||||
];
|
||||
|
||||
$this->mappedAttributes[$attributeClass][] = [
|
||||
'attribute' => $attributeInstance,
|
||||
'reflector' => $reflectorKey,
|
||||
'metadata' => $mapper->getAttributeMetadata(),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silent failure - skip this attribute and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Attribute einer WrappedReflectionProperty
|
||||
*/
|
||||
private function processAttributesWrappedProperty($attributes, $property): void
|
||||
{
|
||||
foreach ($attributes as $attribute) {
|
||||
$attributeClass = $attribute->getName();
|
||||
|
||||
if (isset($this->mapperByClass[$attributeClass])) {
|
||||
$mapper = $this->mapperByClass[$attributeClass];
|
||||
|
||||
try {
|
||||
$nativeClass = new \ReflectionClass($property->getDeclaringClass()->getName());
|
||||
$nativeProperty = $nativeClass->getProperty($property->getName());
|
||||
if ($mapper->canProcess($nativeProperty)) {
|
||||
$attributeInstance = $attribute->newInstance();
|
||||
$mapper->map($attributeInstance, $nativeProperty);
|
||||
$this->processingStats['processed_attributes']++;
|
||||
|
||||
if (! isset($this->mappedAttributes[$attributeClass])) {
|
||||
$this->mappedAttributes[$attributeClass] = [];
|
||||
}
|
||||
|
||||
$reflectorKey = [
|
||||
'type' => 'property',
|
||||
'class' => $property->getDeclaringClass()->getName(),
|
||||
'name' => $property->getName(),
|
||||
];
|
||||
|
||||
$this->mappedAttributes[$attributeClass][] = [
|
||||
'attribute' => $attributeInstance,
|
||||
'reflector' => $reflectorKey,
|
||||
'metadata' => $mapper->getAttributeMetadata(),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silent failure - skip this attribute and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +398,7 @@ final class AttributeMappingVisitor implements FileVisitor
|
||||
$this->processingStats['processed_attributes']++;
|
||||
|
||||
// Speichere für Caching
|
||||
if (!isset($this->mappedAttributes[$attributeClass])) {
|
||||
if (! isset($this->mappedAttributes[$attributeClass])) {
|
||||
$this->mappedAttributes[$attributeClass] = [];
|
||||
}
|
||||
|
||||
@@ -168,10 +408,10 @@ final class AttributeMappingVisitor implements FileVisitor
|
||||
$this->mappedAttributes[$attributeClass][] = [
|
||||
'attribute' => $attributeInstance,
|
||||
'reflector' => $reflectorKey,
|
||||
'metadata' => $mapper->getAttributeMetadata()
|
||||
'metadata' => $mapper->getAttributeMetadata(),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Verarbeiten des Attributs {$attributeClass}: " . $e->getMessage());
|
||||
// Silent failure - skip this attribute and continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,45 +426,45 @@ final class AttributeMappingVisitor implements FileVisitor
|
||||
if ($reflector instanceof \ReflectionClass) {
|
||||
return [
|
||||
'type' => 'class',
|
||||
'name' => $reflector->getName()
|
||||
'name' => $reflector->getName(),
|
||||
];
|
||||
} elseif ($reflector instanceof \ReflectionMethod) {
|
||||
return [
|
||||
'type' => 'method',
|
||||
'class' => $reflector->getDeclaringClass()->getName(),
|
||||
'name' => $reflector->getName()
|
||||
'name' => $reflector->getName(),
|
||||
];
|
||||
} elseif ($reflector instanceof \ReflectionProperty) {
|
||||
return [
|
||||
'type' => 'property',
|
||||
'class' => $reflector->getDeclaringClass()->getName(),
|
||||
'name' => $reflector->getName()
|
||||
'name' => $reflector->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'unknown',
|
||||
'name' => method_exists($reflector, 'getName') ? $reflector->getName() : 'unknown'
|
||||
'name' => method_exists($reflector, 'getName') ? $reflector->getName() : 'unknown',
|
||||
];
|
||||
}
|
||||
|
||||
public function onScanComplete(): void
|
||||
{
|
||||
// Logging der Verarbeitungsstatistiken
|
||||
error_log("Attribut-Verarbeitung abgeschlossen: " . json_encode($this->processingStats));
|
||||
// Processing completed - stats removed for production
|
||||
}
|
||||
|
||||
public function loadFromCache(Cache $cache): void
|
||||
{
|
||||
$cacheItem = $cache->get($this->getCacheKey());
|
||||
$cacheItem = $cache->get(CacheKey::fromString($this->getCacheKey()));
|
||||
if ($cacheItem->isHit) {
|
||||
$this->mappedAttributes = $cacheItem->value;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
public function getCacheKey(): CacheKey
|
||||
{
|
||||
return 'attribute_mappings';
|
||||
return CacheKey::fromString('attribute_mappings');
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
@@ -37,12 +38,14 @@ final readonly class AttributeProcessor
|
||||
|
||||
/**
|
||||
* Verarbeitet die Attribute eines Elements (Klasse oder Methode)
|
||||
* @param ReflectionClass|ReflectionMethod $ref
|
||||
* @param array $results
|
||||
*/
|
||||
private function processAttributes(ReflectionClass|ReflectionMethod $ref, array &$results): void
|
||||
{
|
||||
foreach ($ref->getAttributes() as $attribute) {
|
||||
$attrName = $attribute->getName();
|
||||
if (!isset($this->mapperMap[$attrName])) {
|
||||
if (! isset($this->mapperMap[$attrName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -81,5 +84,4 @@ final readonly class AttributeProcessor
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
@@ -9,6 +10,7 @@ namespace App\Framework\Core;
|
||||
final class ClassParser
|
||||
{
|
||||
private static array $classCache = [];
|
||||
|
||||
private static array $tokenCache = [];
|
||||
|
||||
/**
|
||||
@@ -54,7 +56,7 @@ final class ClassParser
|
||||
#debug("Fallback namespace from content: '$namespace'");
|
||||
}
|
||||
|
||||
if (!empty($namespace)) {
|
||||
if (! empty($namespace)) {
|
||||
$fullClassName = '\\' . trim($namespace, '\\') . '\\' . $className;
|
||||
} else {
|
||||
$fullClassName = '\\' . $className;
|
||||
@@ -115,6 +117,7 @@ final class ClassParser
|
||||
}
|
||||
|
||||
$result = trim($namespace, '\\');
|
||||
|
||||
#debug("Final parsed namespace: '$result'");
|
||||
return $result;
|
||||
}
|
||||
@@ -146,7 +149,7 @@ final class ClassParser
|
||||
*/
|
||||
private static function getTokens(string $file): ?array
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
if (! file_exists($file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -199,7 +202,6 @@ final class ClassParser
|
||||
self::$tokenCache = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extrahiert den Klassennamen (mit Namespace) aus einer PHP-Datei
|
||||
*/
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Core\Commands;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Debug\OutputInterface;
|
||||
|
||||
final readonly class ClearDiscoveryCache
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache,
|
||||
private PathProvider $pathProvider)
|
||||
{
|
||||
}
|
||||
|
||||
#[ConsoleCommand('discovery:clear', 'Clears the discovery cache')]
|
||||
public function __invoke(): void
|
||||
{
|
||||
$this->cache->forget('discovery_service');
|
||||
}
|
||||
|
||||
#[ConsoleCommand('clear:routes', 'Clears the routes cache')]
|
||||
public function clearRoutes(ConsoleInput $input, ConsoleOutput $output): void
|
||||
{
|
||||
if(file_exists($this->pathProvider->resolvePath('/cache/routes.cache.php')) === false) {
|
||||
$output->writeError('Routes cache not found');
|
||||
return;
|
||||
}
|
||||
|
||||
unlink($this->pathProvider->resolvePath('/cache/routes.cache.php'));
|
||||
|
||||
$output->writeSuccess('Routes cache cleared');;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,209 @@
|
||||
<?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\DateTime\Clock;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\DI\ContainerCompiler;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\DependencyResolver;
|
||||
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Performance\PerformanceMeter;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
|
||||
final readonly class ContainerBootstrapper
|
||||
{
|
||||
public function __construct(
|
||||
private Container $container)
|
||||
{}
|
||||
private Container $container,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap container with intelligent compilation strategy
|
||||
*/
|
||||
public function bootstrap(
|
||||
string $basePath,
|
||||
PerformanceMeter $meter,
|
||||
array $config = []): void
|
||||
{
|
||||
// Kern-Dienste im Container registrieren:
|
||||
$this->container->instance(PerformanceMeter::class, $meter);
|
||||
$this->container->instance(Cache::class, new CacheInitializer()());
|
||||
$this->container->instance(Configuration::class, new Configuration($config));
|
||||
$this->container->instance(PathProvider::class, new PathProvider($basePath));
|
||||
$this->container->instance(ResponseEmitter::class, new ResponseEmitter());
|
||||
PerformanceCollectorInterface $collector,
|
||||
): Container {
|
||||
// Temporarily disable compiled container to fix bootstrap issues
|
||||
// TODO: Re-enable once container interface compatibility is fixed
|
||||
// $optimizedContainer = $this->tryLoadCompiledContainer($basePath, $collector);
|
||||
// if ($optimizedContainer !== null) {
|
||||
// return $optimizedContainer;
|
||||
// }
|
||||
|
||||
$this->autowire($this->container->get(PathProvider::class), $this->container->get(Configuration::class));
|
||||
// Fallback to fresh container bootstrap
|
||||
return $this->bootstrapFreshContainer($basePath, $collector);
|
||||
}
|
||||
|
||||
private function autowire(PathProvider $pathProvider, Configuration $configuration):void
|
||||
/**
|
||||
* Try to load compiled container if valid
|
||||
*/
|
||||
private function tryLoadCompiledContainer(
|
||||
string $basePath,
|
||||
PerformanceCollectorInterface $collector
|
||||
): ?Container {
|
||||
try {
|
||||
$cacheDir = sys_get_temp_dir() . '/framework-cache';
|
||||
$compiledPath = ContainerCompiler::getCompiledContainerPath($cacheDir);
|
||||
|
||||
// Quick existence check first
|
||||
if (! file_exists($compiledPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create temporary fresh container to validate against
|
||||
$tempContainer = $this->createFreshContainer($basePath, $collector);
|
||||
|
||||
// Create compiler for validation
|
||||
$reflectionProvider = new CachedReflectionProvider();
|
||||
$dependencyResolver = new DependencyResolver($reflectionProvider, $tempContainer);
|
||||
$compiler = new ContainerCompiler($reflectionProvider, $dependencyResolver);
|
||||
|
||||
// Validate compiled container
|
||||
if (! $compiler->isCompiledContainerValid($tempContainer, $compiledPath)) {
|
||||
return null; // Invalid, will trigger fresh bootstrap
|
||||
}
|
||||
|
||||
// Load and return compiled container
|
||||
$compiledContainer = ContainerCompiler::load($compiledPath);
|
||||
|
||||
// Add runtime instances that can't be compiled
|
||||
$this->addRuntimeInstances($compiledContainer, $basePath, $collector);
|
||||
|
||||
return $compiledContainer;
|
||||
|
||||
} catch (\ParseError $e) {
|
||||
// Parse error in compiled container - delete cache and fallback
|
||||
$this->clearCompiledCache($compiledPath);
|
||||
error_log("Compiled container has syntax error, deleted cache: " . $e->getMessage());
|
||||
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
// Any other error means we fallback to fresh container
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap fresh container and compile for next request
|
||||
*/
|
||||
private function bootstrapFreshContainer(
|
||||
string $basePath,
|
||||
PerformanceCollectorInterface $collector
|
||||
): Container {
|
||||
$container = $this->createFreshContainer($basePath, $collector);
|
||||
|
||||
// Compile for next request (async in production)
|
||||
$this->compileContainerAsync($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fresh container with all bindings
|
||||
*/
|
||||
private function createFreshContainer(
|
||||
string $basePath,
|
||||
PerformanceCollectorInterface $collector
|
||||
): DefaultContainer {
|
||||
// Use the existing container or create new DefaultContainer
|
||||
$container = $this->container instanceof DefaultContainer
|
||||
? $this->container
|
||||
: new DefaultContainer();
|
||||
|
||||
$this->addRuntimeInstances($container, $basePath, $collector);
|
||||
$this->autowire($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add instances that must be created at runtime
|
||||
*/
|
||||
private function addRuntimeInstances(
|
||||
Container $container,
|
||||
string $basePath,
|
||||
PerformanceCollectorInterface $collector
|
||||
): void {
|
||||
// Core services that need runtime data
|
||||
$container->instance(Logger::class, new DefaultLogger());
|
||||
$container->instance(PerformanceCollectorInterface::class, $collector);
|
||||
$container->instance(Cache::class, new CacheInitializer($collector, $container)());
|
||||
$container->instance(PathProvider::class, new PathProvider($basePath));
|
||||
$container->instance(ResponseEmitter::class, new ResponseEmitter());
|
||||
$container->instance(Clock::class, new SystemClock());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile container asynchronously for next request
|
||||
*/
|
||||
private function compileContainerAsync(DefaultContainer $container): void
|
||||
{
|
||||
// Im ContainerBootstrapper
|
||||
$bootstrapper = new DiscoveryServiceBootstrapper($this->container);
|
||||
try {
|
||||
$cacheDir = sys_get_temp_dir() . '/framework-cache';
|
||||
$compiledPath = ContainerCompiler::getCompiledContainerPath($cacheDir);
|
||||
|
||||
$reflectionProvider = new CachedReflectionProvider();
|
||||
$dependencyResolver = new DependencyResolver($reflectionProvider, $container);
|
||||
$compiler = new ContainerCompiler($reflectionProvider, $dependencyResolver);
|
||||
|
||||
// Compile (async in production, sync in development)
|
||||
#$isProduction = $this->config->get('app.environment') === 'production';
|
||||
$isProduction = false;
|
||||
if ($isProduction) {
|
||||
$compiler->compileAsync($container, $compiledPath);
|
||||
} else {
|
||||
$compiler->compile($container, $compiledPath);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Compilation errors should not break the application
|
||||
// In production, log this error
|
||||
}
|
||||
}
|
||||
|
||||
private function autowire(Container $container): void
|
||||
{
|
||||
// Discovery service bootstrapping
|
||||
$clock = $container->get(Clock::class);
|
||||
$bootstrapper = new DiscoveryServiceBootstrapper($container, $clock);
|
||||
$results = $bootstrapper->bootstrap();
|
||||
}
|
||||
|
||||
// Ergebnisse sind automatisch im Container verfügbar
|
||||
/** @var DiscoveryResults $results */
|
||||
$results = $this->container->get(DiscoveryResults::class);
|
||||
/**
|
||||
* Clear compiled container cache files
|
||||
*/
|
||||
private function clearCompiledCache(string $compiledPath): void
|
||||
{
|
||||
try {
|
||||
// Delete the compiled container file
|
||||
if (file_exists($compiledPath)) {
|
||||
@unlink($compiledPath);
|
||||
}
|
||||
|
||||
#$routeResults = $results->get(Route::class);
|
||||
|
||||
|
||||
// Fallback: Versuche auch mit führendem Backslash
|
||||
/*if (empty($routeResults)) {
|
||||
$routeResults = $results->get('\\' . $routeClass);
|
||||
debug('Tried with leading backslash');
|
||||
}*/
|
||||
|
||||
// Fallback: Suche in allen verfügbaren Keys
|
||||
/*if (empty($routeResults)) {
|
||||
foreach (array_keys($results->getAllAttributeResults()) as $key) {
|
||||
if (str_contains($key, 'Route')) {
|
||||
debug("Found Route-related key: $key");
|
||||
$routeResults = $results->get($key);
|
||||
break;
|
||||
// Also clear related cache directory if it exists
|
||||
$cacheDir = dirname($compiledPath);
|
||||
if (is_dir($cacheDir)) {
|
||||
// Clear all cache files in the directory
|
||||
$files = glob($cacheDir . '/*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
#$discovery = new AttributeDiscoveryService($this->container, $pathProvider, $configuration);
|
||||
|
||||
#$dresults = ProcessedResults::fromArray($discovery->discoverAndProcess())->get(Route::class);
|
||||
|
||||
#debug(count($routeResults));
|
||||
#dd(count($dresults));
|
||||
|
||||
|
||||
|
||||
|
||||
#$cache = $this->container->get(Cache::class);
|
||||
|
||||
#$cache->forget('discovery_service');
|
||||
|
||||
#$processedResults = $cache->remember('discovery_service', function () use ($discovery) {
|
||||
#
|
||||
# // Attribute entdecken und verarbeiten
|
||||
# return $discovery->discoverAndProcess();
|
||||
#
|
||||
# })->value;
|
||||
|
||||
#$results = ProcessedResults::fromArray($processedResults);
|
||||
|
||||
#$this->container->instance(ProcessedResults::class, $results);
|
||||
|
||||
#$this->executeInitializers($results);
|
||||
|
||||
}
|
||||
|
||||
private function executeInitializers(DiscoveryResults $initializerClasses): void
|
||||
{
|
||||
foreach ($initializerClasses->get(Initializer::class) as $initializerClass) {
|
||||
|
||||
$instance = $this->container->invoker->invoke($initializerClass['class'], $initializerClass['method']);
|
||||
|
||||
$this->container->instance($initializerClass['return'], $instance);
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore cache cleanup errors, just log them
|
||||
error_log("Error clearing compiled cache: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Discovery\FileScanner;
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
|
||||
class Discovery
|
||||
{
|
||||
private AttributeProcessor $attributeProcessor;
|
||||
private FileScanner $fileScanner;
|
||||
private ClassParser $classParser;
|
||||
private DiscoveryCacheManager $cacheManager;
|
||||
|
||||
/**
|
||||
* @param AttributeMapper[] $mappers
|
||||
*/
|
||||
public function __construct(
|
||||
?string $srcDir = null,
|
||||
AttributeMapper ...$mappers
|
||||
) {
|
||||
// Automatisches Auffinden der Mapper, wenn ein Quellverzeichnis angegeben ist
|
||||
if ($srcDir !== null) {
|
||||
$locator = new AttributeMapperLocator();
|
||||
$autoMappers = $locator->locateMappers($srcDir);
|
||||
$mappers = array_merge($mappers, $autoMappers);
|
||||
}
|
||||
|
||||
// Initialisierung der Komponenten
|
||||
$this->attributeProcessor = new AttributeProcessor($mappers);
|
||||
$this->fileScanner = new FileScanner();
|
||||
$this->classParser = new ClassParser();
|
||||
$this->cacheManager = new DiscoveryCacheManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entdeckt und verarbeitet alle Attribute im angegebenen Verzeichnis
|
||||
*/
|
||||
public function discover(string $directory): array
|
||||
{
|
||||
$cacheFile = $this->cacheManager->getCachePathForDirectory($directory);
|
||||
$latestMTime = $this->fileScanner->getLatestMTime($directory);
|
||||
|
||||
// Versuchen, aus dem Cache zu laden
|
||||
$data = $this->cacheManager->loadCache($cacheFile, $latestMTime);
|
||||
if ($data !== null) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Keine Cache-Daten verfügbar, führe vollständige Entdeckung durch
|
||||
$results = $this->performDiscovery($directory);
|
||||
|
||||
// Speichere Ergebnisse im Cache
|
||||
$results['__discovery_mtime'] = $latestMTime;
|
||||
$this->cacheManager->storeCache($cacheFile, $results);
|
||||
|
||||
// Entferne Metadaten für die Rückgabe
|
||||
unset($results['__discovery_mtime']);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die eigentliche Entdeckung durch
|
||||
*/
|
||||
private function performDiscovery(string $directory): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->fileScanner->scanDirectory($directory) as $file) {
|
||||
try {
|
||||
$className = $this->classParser->getClassNameFromFile($file->getPathname());
|
||||
|
||||
if (!$className || !class_exists($className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$refClass = new ReflectionClass($className);
|
||||
$this->attributeProcessor->processClass($refClass, $results);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Discovery Warnung: Fehler in Datei {$file->getPathname()}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class DiscoveryCacheManager
|
||||
|
||||
{
|
||||
private string $baseCachePath;
|
||||
|
||||
public function __construct(?string $baseCachePath = null)
|
||||
{
|
||||
$this->baseCachePath = $baseCachePath ?? __DIR__ . '/../../../cache';
|
||||
}
|
||||
|
||||
public function getCachePathForDirectory(string $directory): string
|
||||
{
|
||||
$hash = md5(realpath($directory));
|
||||
return $this->baseCachePath . "/discovery_{$hash}.cache.php";
|
||||
}
|
||||
|
||||
public function loadCache(string $cacheFile, int $latestMTime): ?array
|
||||
{
|
||||
if (!file_exists($cacheFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = include $cacheFile;
|
||||
if (
|
||||
!is_array($data)
|
||||
|| !isset($data['__discovery_mtime'])
|
||||
|| $data['__discovery_mtime'] !== $latestMTime
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
unset($data['__discovery_mtime']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function storeCache(string $cacheFile, array $results): void
|
||||
{
|
||||
// Stellen Sie sicher, dass das Verzeichnis existiert
|
||||
$cacheDir = dirname($cacheFile);
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0775, true);
|
||||
}
|
||||
|
||||
$export = var_export($results, true);
|
||||
file_put_contents($cacheFile, "<?php\nreturn {$export};\n");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Router\ValueObjects\ParameterCollection;
|
||||
|
||||
final readonly class DynamicRoute implements Route
|
||||
{
|
||||
public function __construct(
|
||||
@@ -15,6 +17,22 @@ final readonly class DynamicRoute implements Route
|
||||
public array $paramValues = [], // ['id' => '123', 'userId' => '456'] - URL-Parameter-Werte
|
||||
public string $name = '',
|
||||
public string $path = '',
|
||||
public array $attributes = []
|
||||
) {}
|
||||
public array $attributes = [],
|
||||
public ?ParameterCollection $parameterCollection = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type-safe parameter collection (preferred)
|
||||
*/
|
||||
public function getParameterCollection(): ParameterCollection
|
||||
{
|
||||
if ($this->parameterCollection !== null) {
|
||||
return $this->parameterCollection;
|
||||
}
|
||||
|
||||
// Fallback: Create ParameterCollection from legacy array
|
||||
// This should not happen in production with proper RouteCompiler
|
||||
return new ParameterCollection();
|
||||
}
|
||||
}
|
||||
|
||||
150
src/Framework/Core/Encoding/Base32Alphabet.php
Normal file
150
src/Framework/Core/Encoding/Base32Alphabet.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Encoding;
|
||||
|
||||
/**
|
||||
* Base32 Alphabet Enum
|
||||
*
|
||||
* Defines different Base32 alphabets for various use cases:
|
||||
* - RFC3548: Standard Base32 alphabet for TOTP and general use
|
||||
* - CROCKFORD: Crockford's Base32 for ULIDs and human-readable IDs
|
||||
*/
|
||||
enum Base32Alphabet: string
|
||||
{
|
||||
case RFC3548 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
case CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||
|
||||
/**
|
||||
* Get the alphabet string
|
||||
*/
|
||||
public function getAlphabet(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this alphabet uses padding
|
||||
*/
|
||||
public function usesPadding(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::RFC3548 => true,
|
||||
self::CROCKFORD => false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the character count (always 32 for Base32)
|
||||
*/
|
||||
public function getCharacterCount(): int
|
||||
{
|
||||
return 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a character exists in this alphabet
|
||||
*/
|
||||
public function containsCharacter(string $char): bool
|
||||
{
|
||||
return str_contains($this->value, strtoupper($char));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of a character in the alphabet
|
||||
*/
|
||||
public function getCharacterIndex(string $char): int
|
||||
{
|
||||
$index = strpos($this->value, strtoupper($char));
|
||||
|
||||
if ($index === false) {
|
||||
throw new \InvalidArgumentException("Character '{$char}' not found in {$this->name} alphabet");
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character at specific index
|
||||
*/
|
||||
public function getCharacterAt(int $index): string
|
||||
{
|
||||
if ($index < 0 || $index >= 32) {
|
||||
throw new \InvalidArgumentException("Index must be between 0 and 31, got {$index}");
|
||||
}
|
||||
|
||||
return $this->value[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an encoded string against this alphabet
|
||||
*/
|
||||
public function isValidEncoded(string $encoded): bool
|
||||
{
|
||||
// Remove padding if this alphabet uses it
|
||||
if ($this->usesPadding()) {
|
||||
$encoded = rtrim($encoded, '=');
|
||||
}
|
||||
|
||||
$encoded = strtoupper($encoded);
|
||||
|
||||
// Check if all characters are valid
|
||||
for ($i = 0, $len = strlen($encoded); $i < $len; $i++) {
|
||||
if (! $this->containsCharacter($encoded[$i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended use cases for this alphabet
|
||||
*/
|
||||
public function getUseCases(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::RFC3548 => [
|
||||
'TOTP secrets',
|
||||
'General Base32 encoding',
|
||||
'Email verification codes',
|
||||
'API tokens',
|
||||
],
|
||||
self::CROCKFORD => [
|
||||
'ULIDs',
|
||||
'Human-readable identifiers',
|
||||
'Short URLs',
|
||||
'Database primary keys',
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description of this alphabet
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::RFC3548 => 'RFC 3548 standard Base32 alphabet with padding',
|
||||
self::CROCKFORD => 'Crockford\'s Base32 alphabet without padding, optimized for human readability'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random string using this alphabet
|
||||
*/
|
||||
public function generateRandom(int $length): string
|
||||
{
|
||||
if ($length < 1) {
|
||||
throw new \InvalidArgumentException('Length must be positive');
|
||||
}
|
||||
|
||||
$result = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$result .= $this->value[random_int(0, 31)];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
242
src/Framework/Core/Encoding/Base32Encoder.php
Normal file
242
src/Framework/Core/Encoding/Base32Encoder.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Encoding;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Multi-Standard Base32 Encoder/Decoder
|
||||
*
|
||||
* Provides Base32 encoding and decoding functionality supporting multiple standards:
|
||||
* - RFC 3548 (standard): Used for TOTP secrets and general applications
|
||||
* - Crockford's Base32: Used for ULIDs and human-readable identifiers
|
||||
*/
|
||||
final readonly class Base32Encoder
|
||||
{
|
||||
// Padding character
|
||||
private const string PADDING = '=';
|
||||
|
||||
/**
|
||||
* Encode binary data to Base32 string using RFC 3548 alphabet
|
||||
*/
|
||||
public static function encode(string $data): string
|
||||
{
|
||||
return self::encodeWithAlphabet($data, Base32Alphabet::RFC3548);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode binary data using Crockford's Base32 alphabet (for ULID)
|
||||
*/
|
||||
public static function encodeCrockford(string $data): string
|
||||
{
|
||||
return self::encodeWithAlphabet($data, Base32Alphabet::CROCKFORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode binary data to Base32 string with specified alphabet
|
||||
*/
|
||||
public static function encodeWithAlphabet(string $data, Base32Alphabet $alphabet): string
|
||||
{
|
||||
if (empty($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$encoded = '';
|
||||
$dataLength = strlen($data);
|
||||
|
||||
// Process data in 5-byte chunks (40 bits)
|
||||
for ($i = 0; $i < $dataLength; $i += 5) {
|
||||
$chunk = substr($data, $i, 5);
|
||||
$chunkLength = strlen($chunk);
|
||||
|
||||
// Pad chunk to 5 bytes if necessary
|
||||
$chunk = str_pad($chunk, 5, "\0");
|
||||
|
||||
// Convert 5 bytes (40 bits) to 8 Base32 characters
|
||||
$bits = '';
|
||||
for ($j = 0; $j < 5; $j++) {
|
||||
$bits .= str_pad(decbin(ord($chunk[$j])), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// Split into 5-bit groups and encode
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$fiveBits = substr($bits, $j * 5, 5);
|
||||
if (strlen($fiveBits) < 5) {
|
||||
break;
|
||||
}
|
||||
|
||||
$index = bindec($fiveBits);
|
||||
$encoded .= $alphabet->getCharacterAt($index);
|
||||
}
|
||||
|
||||
// Add padding based on input length (if alphabet uses padding)
|
||||
if ($alphabet->usesPadding() && $chunkLength < 5) {
|
||||
$paddingCount = match ($chunkLength) {
|
||||
1 => 6,
|
||||
2 => 4,
|
||||
3 => 3,
|
||||
4 => 1,
|
||||
default => 0
|
||||
};
|
||||
|
||||
$encoded = substr($encoded, 0, -$paddingCount);
|
||||
$encoded .= str_repeat(self::PADDING, $paddingCount);
|
||||
}
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Base32 string to binary data using RFC 3548 alphabet
|
||||
*/
|
||||
public static function decode(string $encoded): string
|
||||
{
|
||||
return self::decodeWithAlphabet($encoded, Base32Alphabet::RFC3548);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Crockford's Base32 string to binary data
|
||||
*/
|
||||
public static function decodeCrockford(string $encoded): string
|
||||
{
|
||||
return self::decodeWithAlphabet($encoded, Base32Alphabet::CROCKFORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Base32 string to binary data with specified alphabet
|
||||
*/
|
||||
public static function decodeWithAlphabet(string $encoded, Base32Alphabet $alphabet): string
|
||||
{
|
||||
if (empty($encoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove padding if alphabet uses it
|
||||
if ($alphabet->usesPadding()) {
|
||||
$encoded = rtrim($encoded, self::PADDING);
|
||||
}
|
||||
|
||||
$encoded = strtoupper($encoded);
|
||||
$encodedLength = strlen($encoded);
|
||||
|
||||
if (! $alphabet->isValidEncoded($encoded)) {
|
||||
throw new InvalidArgumentException("Invalid {$alphabet->name} Base32 string");
|
||||
}
|
||||
|
||||
$decoded = '';
|
||||
$bits = '';
|
||||
|
||||
// Convert each Base32 character to 5 bits
|
||||
for ($i = 0; $i < $encodedLength; $i++) {
|
||||
$char = $encoded[$i];
|
||||
$index = $alphabet->getCharacterIndex($char);
|
||||
$bits .= str_pad(decbin($index), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// Convert bits to bytes
|
||||
$bitsLength = strlen($bits);
|
||||
for ($i = 0; $i < $bitsLength; $i += 8) {
|
||||
$byte = substr($bits, $i, 8);
|
||||
|
||||
// Only process complete bytes
|
||||
if (strlen($byte) === 8) {
|
||||
$decoded .= chr(bindec($byte));
|
||||
}
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Base32 string format using RFC 3548 alphabet
|
||||
*/
|
||||
public static function isValidBase32(string $encoded): bool
|
||||
{
|
||||
return Base32Alphabet::RFC3548->isValidEncoded($encoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Base32 string format with specified alphabet
|
||||
*/
|
||||
public static function isValidWithAlphabet(string $encoded, Base32Alphabet $alphabet): bool
|
||||
{
|
||||
return $alphabet->isValidEncoded($encoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random Base32 string using RFC 3548 alphabet
|
||||
*/
|
||||
public static function generateRandom(int $length = 32): string
|
||||
{
|
||||
return Base32Alphabet::RFC3548->generateRandom($length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random Base32 string with specified alphabet
|
||||
*/
|
||||
public static function generateRandomWithAlphabet(Base32Alphabet $alphabet, int $length = 32): string
|
||||
{
|
||||
return $alphabet->generateRandom($length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Base32 string with spaces for readability
|
||||
*/
|
||||
public static function formatForDisplay(string $encoded, int $groupSize = 4): string
|
||||
{
|
||||
if ($groupSize < 1) {
|
||||
throw new InvalidArgumentException('Group size must be positive');
|
||||
}
|
||||
|
||||
return trim(chunk_split($encoded, $groupSize, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove formatting from Base32 string
|
||||
*/
|
||||
public static function removeFormatting(string $formatted): string
|
||||
{
|
||||
return preg_replace('/\s+/', '', $formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available alphabets
|
||||
*/
|
||||
public static function getAvailableAlphabets(): array
|
||||
{
|
||||
return Base32Alphabet::cases();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the decoded length for a given encoded length
|
||||
*/
|
||||
public static function getDecodedLength(string $encoded): int
|
||||
{
|
||||
$encoded = rtrim($encoded, self::PADDING);
|
||||
$encodedLength = strlen($encoded);
|
||||
|
||||
// Each Base32 character represents 5 bits
|
||||
$totalBits = $encodedLength * 5;
|
||||
|
||||
// Convert to bytes (8 bits each)
|
||||
return (int) floor($totalBits / 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the encoded length for a given binary data length
|
||||
*/
|
||||
public static function getEncodedLength(int $dataLength): int
|
||||
{
|
||||
if ($dataLength < 0) {
|
||||
throw new InvalidArgumentException('Data length cannot be negative');
|
||||
}
|
||||
|
||||
// Each byte is 8 bits, Base32 uses 5 bits per character
|
||||
$totalBits = $dataLength * 8;
|
||||
|
||||
return (int) ceil($totalBits / 5);
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ErrorHandler;
|
||||
|
||||
use App\Application\Security\ExceptionHandlers\SecurityExceptionHandler;
|
||||
use App\Application\Security\Events\System\SystemAnomalyEvent;
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
final class GlobalErrorHandler
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private EventDispatcher $eventDispatcher,
|
||||
private SecurityExceptionHandler $securityHandler,
|
||||
private bool $debugMode = false
|
||||
) {}
|
||||
|
||||
public function handleException(Throwable $exception): void
|
||||
{
|
||||
// Zuerst Security Events dispatchen
|
||||
$this->handleSecurityAspects($exception);
|
||||
|
||||
// Dann Standard Error Handling
|
||||
$this->logException($exception);
|
||||
|
||||
if ($this->debugMode) {
|
||||
$this->displayDebugInfo($exception);
|
||||
} else {
|
||||
$this->displayUserFriendlyError($exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleError(int $severity, string $message, string $file, int $line): bool
|
||||
{
|
||||
// PHP Errors in Exceptions umwandeln für einheitliche Behandlung
|
||||
$exception = new \ErrorException($message, 0, $severity, $file, $line);
|
||||
|
||||
// Security relevante Errors erkennen
|
||||
if ($this->isSecurityRelevantError($severity, $message)) {
|
||||
$this->handleSecurityAspects($exception);
|
||||
}
|
||||
|
||||
$this->logError($severity, $message, $file, $line);
|
||||
|
||||
// true = Error wurde behandelt, false = PHP default error handler
|
||||
return !$this->debugMode;
|
||||
}
|
||||
|
||||
public function handleFatalError(): void
|
||||
{
|
||||
$error = error_get_last();
|
||||
if ($error && $this->isFatal($error['type'])) {
|
||||
$exception = new \ErrorException(
|
||||
$error['message'],
|
||||
0,
|
||||
$error['type'],
|
||||
$error['file'],
|
||||
$error['line']
|
||||
);
|
||||
|
||||
$this->handleSecurityAspects($exception);
|
||||
$this->logException($exception);
|
||||
|
||||
if (!$this->debugMode) {
|
||||
$this->displayFatalErrorPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function handleSecurityAspects(Throwable $exception): void
|
||||
{
|
||||
try {
|
||||
// Delegiere an speziellen Security Exception Handler
|
||||
$this->securityHandler->handle($exception);
|
||||
|
||||
// Zusätzlich: System-weite Anomalien erkennen
|
||||
if ($this->isSystemAnomaly($exception)) {
|
||||
$this->dispatchSystemAnomaly($exception);
|
||||
}
|
||||
|
||||
} catch (Throwable $securityHandlingException) {
|
||||
// Fallback: Security Handler selbst darf nicht crashen
|
||||
$this->logger->critical('Security exception handler failed', [
|
||||
'original_exception' => $exception->getMessage(),
|
||||
'security_handler_exception' => $securityHandlingException->getMessage(),
|
||||
'trace' => $securityHandlingException->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function isSecurityRelevantError(int $severity, string $message): bool
|
||||
{
|
||||
// Sicherheitskritische PHP Errors identifizieren
|
||||
$securityKeywords = [
|
||||
'permission denied', 'access denied', 'authentication',
|
||||
'authorization', 'file_get_contents', 'file_put_contents',
|
||||
'openssl_', 'hash_', 'password_', 'session_',
|
||||
'mysqli_', 'pdo_', 'curl_', 'file_upload'
|
||||
];
|
||||
|
||||
$messageLower = strtolower($message);
|
||||
foreach ($securityKeywords as $keyword) {
|
||||
if (strpos($messageLower, $keyword) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Bestimmte Error-Level sind grundsätzlich sicherheitsrelevant
|
||||
return in_array($severity, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]);
|
||||
}
|
||||
|
||||
private function isSystemAnomaly(Throwable $exception): bool
|
||||
{
|
||||
// System-Anomalien erkennen
|
||||
return $exception instanceof \Error ||
|
||||
$exception instanceof \OutOfMemoryError ||
|
||||
$exception instanceof \ParseError ||
|
||||
strpos($exception->getMessage(), 'Maximum execution time') !== false ||
|
||||
strpos($exception->getMessage(), 'Allowed memory size') !== false;
|
||||
}
|
||||
|
||||
private function dispatchSystemAnomaly(Throwable $exception): void
|
||||
{
|
||||
$metrics = [
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'execution_time' => microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))
|
||||
];
|
||||
|
||||
$severity = match (true) {
|
||||
$exception instanceof \OutOfMemoryError => 'critical',
|
||||
$exception instanceof \Error => 'high',
|
||||
default => 'medium'
|
||||
};
|
||||
|
||||
$this->eventDispatcher->dispatch(new SystemAnomalyEvent(
|
||||
anomalyType: get_class($exception),
|
||||
description: $exception->getMessage(),
|
||||
metrics: $metrics,
|
||||
severity: $severity
|
||||
));
|
||||
}
|
||||
|
||||
private function isFatal(int $type): bool
|
||||
{
|
||||
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
|
||||
}
|
||||
|
||||
private function logException(Throwable $exception): void
|
||||
{
|
||||
$context = [
|
||||
'exception_class' => get_class($exception),
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
'user_id' => $_SESSION['user_id'] ?? null,
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'] ?? null,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null
|
||||
];
|
||||
|
||||
$this->logger->error('Unhandled exception: ' . $exception->getMessage(), $context);
|
||||
}
|
||||
|
||||
private function logError(int $severity, string $message, string $file, int $line): void
|
||||
{
|
||||
$level = $this->mapErrorSeverityToLogLevel($severity);
|
||||
|
||||
$this->logger->log($level, "PHP Error: {$message}", [
|
||||
'severity' => $severity,
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
'user_id' => $_SESSION['user_id'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
private function mapErrorSeverityToLogLevel(int $severity): string
|
||||
{
|
||||
return match ($severity) {
|
||||
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'error',
|
||||
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'warning',
|
||||
E_NOTICE, E_USER_NOTICE => 'notice',
|
||||
E_STRICT => 'info',
|
||||
default => 'debug'
|
||||
};
|
||||
}
|
||||
|
||||
private function displayDebugInfo(Throwable $exception): void
|
||||
{
|
||||
echo "<h1>Exception: " . get_class($exception) . "</h1>";
|
||||
echo "<p><strong>Message:</strong> " . htmlspecialchars($exception->getMessage()) . "</p>";
|
||||
echo "<p><strong>File:</strong> " . htmlspecialchars($exception->getFile()) . ":" . $exception->getLine() . "</p>";
|
||||
echo "<pre>" . htmlspecialchars($exception->getTraceAsString()) . "</pre>";
|
||||
}
|
||||
|
||||
private function displayUserFriendlyError(Throwable $exception): void
|
||||
{
|
||||
http_response_code(500);
|
||||
echo "<!DOCTYPE html><html><head><title>Fehler</title></head><body>";
|
||||
echo "<h1>Es ist ein Fehler aufgetreten</h1>";
|
||||
echo "<p>Entschuldigung, es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut.</p>";
|
||||
echo "</body></html>";
|
||||
}
|
||||
|
||||
private function displayFatalErrorPage(): void
|
||||
{
|
||||
http_response_code(500);
|
||||
echo "<!DOCTYPE html><html><head><title>Schwerwiegender Fehler</title></head><body>";
|
||||
echo "<h1>Schwerwiegender Systemfehler</h1>";
|
||||
echo "<p>Die Anwendung musste aufgrund eines kritischen Fehlers beendet werden.</p>";
|
||||
echo "</body></html>";
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class AfterEmitResponse
|
||||
final readonly class AfterEmitResponse
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class AfterHandleRequest
|
||||
final readonly class AfterHandleRequest
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
@@ -10,5 +11,6 @@ final readonly class ApplicationBooted
|
||||
public string $environment,
|
||||
public string $version = 'dev',
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class BeforeEmitResponse
|
||||
final readonly class BeforeEmitResponse
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
class BeforeHandleRequest
|
||||
final readonly class BeforeHandleRequest
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
@@ -10,5 +11,6 @@ final readonly class ErrorOccurred
|
||||
public string $context = '',
|
||||
public ?string $requestId = null,
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
@@ -8,7 +9,7 @@ use App\Framework\DI\DefaultContainer;
|
||||
/**
|
||||
* Führt abschließende Konfigurationen für das Event-System durch
|
||||
*/
|
||||
final class EventCompilerPass
|
||||
final readonly class EventCompilerPass
|
||||
{
|
||||
/**
|
||||
* Richtet zusätzliche Event-Konfigurationen ein
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
|
||||
/**
|
||||
* Der EventDispatcher ist verantwortlich für die Verarbeitung von Events
|
||||
* und das Aufrufen der entsprechenden Event-Handler
|
||||
*/
|
||||
final class EventDispatcher
|
||||
final class EventDispatcher implements EventDispatcherInterface
|
||||
{
|
||||
/** @var array<string, array> Mapping von Event-Typen zu Handlern */
|
||||
private array $handlers = [];
|
||||
@@ -22,9 +24,10 @@ final class EventDispatcher
|
||||
* @param array|null $eventHandlers Array mit Event-Handlern aus der Autodiscovery
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly DefaultContainer $container,
|
||||
private readonly Container $container,
|
||||
private readonly ?array $eventHandlers = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den EventDispatcher mit den Event-Handlern
|
||||
@@ -149,8 +152,8 @@ final class EventDispatcher
|
||||
'callable' => $handler,
|
||||
'attribute_data' => [
|
||||
'priority' => $priority ?? 0,
|
||||
'stopPropagation' => $stopPropagation
|
||||
]
|
||||
'stopPropagation' => $stopPropagation,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -177,12 +180,12 @@ final class EventDispatcher
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
if (isset($this->handlers[$eventClass]) && !empty($this->handlers[$eventClass])) {
|
||||
if (isset($this->handlers[$eventClass]) && ! empty($this->handlers[$eventClass])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfen auf Handler für Basisklassen/Interfaces
|
||||
return array_any(array_keys($this->handlers), fn($handledEventClass) => is_subclass_of($eventClass, $handledEventClass));
|
||||
return array_any(array_keys($this->handlers), fn ($handledEventClass) => is_subclass_of($eventClass, $handledEventClass));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
|
||||
final readonly class EventDispatcherInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private DiscoveryResults $results
|
||||
){}
|
||||
private DiscoveryRegistry $results
|
||||
) {
|
||||
}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke(): EventDispatcher
|
||||
{
|
||||
return new EventDispatcher($this->container, $this->results->get(OnEvent::class));
|
||||
$eventResults = $this->results->attributes->get(OnEvent::class);
|
||||
|
||||
$events = [];
|
||||
foreach ($eventResults as $discoveredAttribute) {
|
||||
if ($discoveredAttribute->additionalData) {
|
||||
$events[] = $discoveredAttribute->additionalData;
|
||||
}
|
||||
}
|
||||
|
||||
return new EventDispatcher($this->container, $events);
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Framework/Core/Events/EventDispatcherInterface.php
Normal file
16
src/Framework/Core/Events/EventDispatcherInterface.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
/**
|
||||
* Interface für Event Dispatcher
|
||||
*/
|
||||
interface EventDispatcherInterface
|
||||
{
|
||||
/**
|
||||
* Dispatcht ein Event an alle registrierten Handler
|
||||
*/
|
||||
public function dispatch(object $event): array;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
|
||||
use App\Framework\Core\AttributeMapper;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use App\Framework\Reflection\WrappedReflectionClass;
|
||||
use App\Framework\Reflection\WrappedReflectionMethod;
|
||||
|
||||
/**
|
||||
* Mapper für das OnEvent-Attribut
|
||||
@@ -22,14 +23,10 @@ final class EventHandlerMapper implements AttributeMapper
|
||||
|
||||
/**
|
||||
* Implementiert die map-Methode aus dem AttributeMapper-Interface
|
||||
*
|
||||
* @param object $reflectionTarget Das Reflektionsobjekt (ReflectionClass|ReflectionMethod)
|
||||
* @param object $attributeInstance Die Attributinstanz
|
||||
* @return array|null Die Attributdaten oder null, wenn nicht verarbeitet werden kann
|
||||
*/
|
||||
public function map(object $reflectionTarget, object $attributeInstance): ?array
|
||||
public function map(WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget, object $attributeInstance): ?array
|
||||
{
|
||||
if (!($reflectionTarget instanceof ReflectionMethod)) {
|
||||
if (! ($reflectionTarget instanceof WrappedReflectionMethod)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -41,7 +38,7 @@ final class EventHandlerMapper implements AttributeMapper
|
||||
}
|
||||
|
||||
$eventType = $parameters[0]->getType();
|
||||
if (!$eventType || $eventType->isBuiltin()) {
|
||||
if (! $eventType || $eventType->isBuiltin()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,7 +51,7 @@ final class EventHandlerMapper implements AttributeMapper
|
||||
#'reflection' => $reflectionTarget,
|
||||
'attribute_data' => [
|
||||
'priority' => $attributeInstance->priority ?? 0,
|
||||
'stopPropagation' => $attributeInstance->stopPropagation ?? false
|
||||
'stopPropagation' => $attributeInstance->stopPropagation ?? false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
@@ -20,5 +21,6 @@ final class OnEvent
|
||||
public function __construct(
|
||||
public readonly ?int $priority = null,
|
||||
public readonly bool $stopPropagation = false
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Events;
|
||||
@@ -14,5 +15,6 @@ final readonly class UserRegistered
|
||||
public string $username,
|
||||
public \DateTimeImmutable $registeredAt = new \DateTimeImmutable(),
|
||||
public \DateTimeImmutable $occurredAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
final class InvalidRouteCacheFormatException extends FrameworkException
|
||||
@@ -15,9 +16,10 @@ final class InvalidRouteCacheFormatException extends FrameworkException
|
||||
) {
|
||||
parent::__construct(
|
||||
message: "Invalid route cache format.",
|
||||
context: ExceptionContext::forOperation('route_cache_parse', 'Core')
|
||||
->withData(['cacheFile' => $cacheFile]),
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: ['cacheFile' => $cacheFile]
|
||||
previous: $previous
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
final class RouteCacheException extends FrameworkException
|
||||
@@ -15,9 +16,10 @@ final class RouteCacheException extends FrameworkException
|
||||
) {
|
||||
parent::__construct(
|
||||
message: "Route cache file not found: {$cacheFile}",
|
||||
context: ExceptionContext::forOperation('route_cache_load', 'Core')
|
||||
->withData(['cacheFile' => $cacheFile]),
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: ['cacheFile' => $cacheFile]
|
||||
previous: $previous
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
@@ -23,14 +26,14 @@ class InterfaceImplementationLocator
|
||||
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
|
||||
|
||||
foreach ($rii as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'php') {
|
||||
if (! $file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$className = $this->getClassNameFromFile($file->getPathname());
|
||||
|
||||
if (!$className || !class_exists($className)) {
|
||||
if (! $className || ! ClassName::create($className)->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -38,8 +41,8 @@ class InterfaceImplementationLocator
|
||||
|
||||
// Überprüfen, ob die Klasse das gewünschte Interface implementiert
|
||||
if ($refClass->implementsInterface($interfaceName) &&
|
||||
!$refClass->isAbstract() &&
|
||||
!$refClass->isInterface()) {
|
||||
! $refClass->isAbstract() &&
|
||||
! $refClass->isInterface()) {
|
||||
|
||||
if ($instantiate) {
|
||||
// Prüfen, ob der Konstruktor Parameter hat
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
use App\Framework\Discovery\ReflectionAwareVisitor;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
use App\Framework\Reflection\WrappedReflectionClass;
|
||||
|
||||
/**
|
||||
* Visitor zum Auffinden von Interface-Implementierungen
|
||||
*/
|
||||
final class InterfaceImplementationVisitor implements FileVisitor
|
||||
final class InterfaceImplementationVisitor implements FileVisitor, ReflectionAwareVisitor
|
||||
{
|
||||
private array $interfaces = [];
|
||||
|
||||
private array $implementations = [];
|
||||
|
||||
private array $implementationsByClass = [];
|
||||
|
||||
/**
|
||||
* @param array $targetInterfaces Die zu suchenden Interfaces
|
||||
*/
|
||||
public function __construct(private readonly array $targetInterfaces = [])
|
||||
{
|
||||
public function __construct(
|
||||
private readonly array $targetInterfaces = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function onScanStart(): void
|
||||
@@ -40,44 +49,46 @@ final class InterfaceImplementationVisitor implements FileVisitor
|
||||
$this->sortImplementations();
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
public function visitClass(ClassName $className, FilePath $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
// This method is kept for backward compatibility but should not be used
|
||||
// when ReflectionProvider is available. The UnifiedDiscoveryService will
|
||||
// prefer visitClassWithReflection() when possible.
|
||||
}
|
||||
|
||||
public function visitClassWithReflection(ClassName $className, FilePath $filePath, WrappedReflectionClass $reflection): void
|
||||
{
|
||||
try {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Abstrakte Klassen und Interfaces überspringen
|
||||
if ($reflection->isAbstract() || $reflection->isInterface()) {
|
||||
// Skip abstract classes and interfaces - only process instantiable classes
|
||||
if (! $reflection->isInstantiable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alte Einträge für diese Klasse entfernen (wichtig für inkrementelle Scans)
|
||||
$this->removeImplementationsForClass($className);
|
||||
$classNameStr = $className->getFullyQualified();
|
||||
$this->removeImplementationsForClass($classNameStr);
|
||||
|
||||
// Prüfe, ob die Klasse eines der Ziel-Interfaces implementiert
|
||||
foreach ($this->targetInterfaces as $interface) {
|
||||
if ($reflection->implementsInterface($interface)) {
|
||||
if (!isset($this->implementations[$interface])) {
|
||||
if (! isset($this->implementations[$interface])) {
|
||||
$this->implementations[$interface] = [];
|
||||
}
|
||||
|
||||
// Prüfe auf Duplikate
|
||||
if (!in_array($className, $this->implementations[$interface])) {
|
||||
$this->implementations[$interface][] = $className;
|
||||
if (! in_array($classNameStr, $this->implementations[$interface])) {
|
||||
$this->implementations[$interface][] = $classNameStr;
|
||||
|
||||
// Umgekehrten Index für schnelleren Zugriff aufbauen
|
||||
if (!isset($this->implementationsByClass[$className])) {
|
||||
$this->implementationsByClass[$className] = [];
|
||||
if (! isset($this->implementationsByClass[$classNameStr])) {
|
||||
$this->implementationsByClass[$classNameStr] = [];
|
||||
}
|
||||
$this->implementationsByClass[$className][] = $interface;
|
||||
$this->implementationsByClass[$classNameStr][] = $interface;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Prüfen der Interface-Implementierung für {$className}: " . $e->getMessage());
|
||||
// Silent failure - skip this class and continue with discovery
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +97,7 @@ final class InterfaceImplementationVisitor implements FileVisitor
|
||||
*/
|
||||
private function removeImplementationsForClass(string $className): void
|
||||
{
|
||||
if (!isset($this->implementationsByClass[$className])) {
|
||||
if (! isset($this->implementationsByClass[$className])) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,16 +142,16 @@ final class InterfaceImplementationVisitor implements FileVisitor
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
public function getCacheKey(): CacheKey
|
||||
{
|
||||
return 'interface_implementations';
|
||||
return CacheKey::fromString('interface_implementations');
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
public function getCacheableData(): array
|
||||
{
|
||||
return [
|
||||
'implementations' => $this->implementations,
|
||||
'byClass' => $this->implementationsByClass
|
||||
'byClass' => $this->implementationsByClass,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
@@ -9,7 +10,9 @@ namespace App\Framework\Core;
|
||||
final class PathProvider
|
||||
{
|
||||
private string $basePath;
|
||||
|
||||
private array $resolvedPaths = [];
|
||||
|
||||
private ?array $namespacePaths = null {
|
||||
get {
|
||||
if ($this->namespacePaths !== null) {
|
||||
@@ -18,7 +21,7 @@ final class PathProvider
|
||||
|
||||
// Aus composer.json auslesen
|
||||
$composerJsonPath = $this->basePath . '/composer.json';
|
||||
if (!file_exists($composerJsonPath)) {
|
||||
if (! file_exists($composerJsonPath)) {
|
||||
return $this->namespacePaths = [];
|
||||
}
|
||||
|
||||
@@ -83,6 +86,7 @@ final class PathProvider
|
||||
if (str_starts_with($namespace, $prefix)) {
|
||||
$relativeNamespace = substr($namespace, strlen($prefix));
|
||||
$relativePath = str_replace('\\', '/', $relativeNamespace);
|
||||
|
||||
return rtrim($path, '/') . '/' . $relativePath . '.php';
|
||||
}
|
||||
}
|
||||
@@ -98,7 +102,7 @@ final class PathProvider
|
||||
$namespacePaths = $this->namespacePaths;
|
||||
$absolutePath = realpath($path);
|
||||
|
||||
if (!$absolutePath) {
|
||||
if (! $absolutePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -110,6 +114,7 @@ final class PathProvider
|
||||
$relativePath = substr($absolutePath, strlen($realNsPath));
|
||||
$relativePath = rtrim(str_replace('.php', '', $relativePath), '/');
|
||||
$relativeNamespace = str_replace('/', '\\', $relativePath);
|
||||
|
||||
return $namespace . ltrim($relativeNamespace, '\\');
|
||||
}
|
||||
}
|
||||
|
||||
214
src/Framework/Core/Performance/ContainerPerformanceMonitor.php
Normal file
214
src/Framework/Core/Performance/ContainerPerformanceMonitor.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Performance;
|
||||
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Performance\PerformanceCategory;
|
||||
|
||||
/**
|
||||
* Container Performance Monitor
|
||||
*
|
||||
* Monitors container bootstrap performance and provides optimization insights.
|
||||
* Helps identify bottlenecks in dependency injection and service initialization.
|
||||
*/
|
||||
final class ContainerPerformanceMonitor
|
||||
{
|
||||
private array $thresholds;
|
||||
|
||||
private array $operationStartTimes = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly PerformanceCollectorInterface $collector,
|
||||
private readonly ?Logger $logger = null,
|
||||
array $customThresholds = []
|
||||
) {
|
||||
$this->thresholds = array_merge([
|
||||
'bootstrap_total' => 0.100, // 100ms total bootstrap time
|
||||
'container_compilation' => 0.020, // 20ms for container compilation
|
||||
'discovery_service' => 0.050, // 50ms for discovery service
|
||||
'instance_creation' => 0.010, // 10ms per instance creation
|
||||
'interface_binding' => 0.005, // 5ms per interface binding
|
||||
], $customThresholds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring a container operation
|
||||
*/
|
||||
public function startOperation(string $operation): void
|
||||
{
|
||||
$this->operationStartTimes[$operation] = microtime(true);
|
||||
$this->collector->startTiming($operation, PerformanceCategory::SYSTEM);
|
||||
}
|
||||
|
||||
/**
|
||||
* End monitoring and analyze performance
|
||||
*/
|
||||
public function endOperation(string $operation): ContainerPerformanceResult
|
||||
{
|
||||
$this->collector->endTiming($operation);
|
||||
|
||||
// Calculate duration from our own tracking
|
||||
$startTime = $this->operationStartTimes[$operation] ?? microtime(true);
|
||||
$duration = microtime(true) - $startTime;
|
||||
unset($this->operationStartTimes[$operation]);
|
||||
|
||||
$threshold = $this->thresholds[$operation] ?? $this->thresholds['bootstrap_total'];
|
||||
|
||||
$result = new ContainerPerformanceResult(
|
||||
operation: $operation,
|
||||
duration: $duration,
|
||||
threshold: $threshold,
|
||||
isWithinThreshold: $duration <= $threshold,
|
||||
recommendation: $this->generateRecommendation($operation, $duration, $threshold)
|
||||
);
|
||||
|
||||
if (! $result->isWithinThreshold) {
|
||||
$this->logPerformanceIssue($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor container binding performance
|
||||
*/
|
||||
public function monitorBinding(string $interface, callable $bindingFunction): mixed
|
||||
{
|
||||
$this->startOperation('interface_binding');
|
||||
|
||||
try {
|
||||
$result = $bindingFunction();
|
||||
|
||||
$performanceResult = $this->endOperation('interface_binding');
|
||||
|
||||
if (! $performanceResult->isWithinThreshold) {
|
||||
$this->logger?->warning('Slow container binding detected', [
|
||||
'interface' => $interface,
|
||||
'duration' => $performanceResult->duration,
|
||||
'threshold' => $performanceResult->threshold,
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$this->endOperation('interface_binding');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance recommendations based on collected metrics
|
||||
*/
|
||||
public function getOptimizationRecommendations(): array
|
||||
{
|
||||
$metrics = $this->collector->getMetrics();
|
||||
$recommendations = [];
|
||||
|
||||
foreach ($metrics as $key => $metric) {
|
||||
// Check if metric is for SYSTEM category and has a threshold
|
||||
if ($metric->getCategory() === PerformanceCategory::SYSTEM &&
|
||||
isset($this->thresholds[$key])) {
|
||||
|
||||
$threshold = $this->thresholds[$key];
|
||||
$avgDuration = $metric->getAverageDuration();
|
||||
|
||||
if ($avgDuration !== null) {
|
||||
// Convert Duration to seconds (float)
|
||||
$durationInSeconds = $avgDuration->toSeconds();
|
||||
|
||||
if ($durationInSeconds > $threshold) {
|
||||
$recommendations[] = $this->generateRecommendation(
|
||||
$key,
|
||||
$durationInSeconds,
|
||||
$threshold
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($recommendations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate performance recommendations
|
||||
*/
|
||||
private function generateRecommendation(string $operation, float $duration, float $threshold): string
|
||||
{
|
||||
$slownessFactor = $duration / $threshold;
|
||||
|
||||
return match (true) {
|
||||
str_contains($operation, 'bootstrap') && $slownessFactor > 2 =>
|
||||
"Container bootstrap is {$slownessFactor}x slower than threshold. Consider enabling container compilation.",
|
||||
|
||||
str_contains($operation, 'discovery') && $slownessFactor > 1.5 =>
|
||||
"Discovery service is slow. Consider reducing scan paths or improving cache strategy.",
|
||||
|
||||
str_contains($operation, 'compilation') && $slownessFactor > 1.5 =>
|
||||
"Container compilation is slow. Check for circular dependencies or complex bindings.",
|
||||
|
||||
str_contains($operation, 'binding') && $slownessFactor > 2 =>
|
||||
"Interface binding is slow. Consider using singletons for heavy objects.",
|
||||
|
||||
default => "Performance issue detected in {$operation}. Duration: {$duration}s, Threshold: {$threshold}s"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log performance issues for analysis
|
||||
*/
|
||||
private function logPerformanceIssue(ContainerPerformanceResult $result): void
|
||||
{
|
||||
$this->logger?->warning('Container performance threshold exceeded', [
|
||||
'operation' => $result->operation,
|
||||
'duration' => $result->duration,
|
||||
'threshold' => $result->threshold,
|
||||
'slowness_factor' => $result->duration / $result->threshold,
|
||||
'recommendation' => $result->recommendation,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a performance report for debugging
|
||||
*/
|
||||
public function generatePerformanceReport(): array
|
||||
{
|
||||
$metrics = $this->collector->getMetrics();
|
||||
$containerMetrics = [];
|
||||
$slowOperations = 0;
|
||||
|
||||
foreach ($metrics as $key => $metric) {
|
||||
if ($metric->getCategory() === PerformanceCategory::SYSTEM) {
|
||||
$avgDuration = $metric->getAverageDuration();
|
||||
$totalDuration = $metric->getTotalDuration();
|
||||
|
||||
$containerMetrics[$key] = [
|
||||
'key' => $key,
|
||||
'category' => $metric->getCategory()->value,
|
||||
'average_duration' => $avgDuration ? $avgDuration->toSeconds() : 0.0,
|
||||
'count' => $metric->getCount(),
|
||||
'total_duration' => $totalDuration ? $totalDuration : 0.0,
|
||||
];
|
||||
|
||||
// Check if it's a slow operation
|
||||
if (isset($this->thresholds[$key]) && $avgDuration !== null) {
|
||||
$durationInSeconds = $avgDuration->toSeconds();
|
||||
if ($durationInSeconds > $this->thresholds[$key]) {
|
||||
$slowOperations++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_operations' => count($containerMetrics),
|
||||
'slow_operations' => $slowOperations,
|
||||
'recommendations' => $this->getOptimizationRecommendations(),
|
||||
'metrics' => $containerMetrics,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Performance;
|
||||
|
||||
/**
|
||||
* Container Performance Result Value Object
|
||||
*/
|
||||
final readonly class ContainerPerformanceResult
|
||||
{
|
||||
public function __construct(
|
||||
public string $operation,
|
||||
public float $duration,
|
||||
public float $threshold,
|
||||
public bool $isWithinThreshold,
|
||||
public string $recommendation
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'operation' => $this->operation,
|
||||
'duration' => $this->duration,
|
||||
'threshold' => $this->threshold,
|
||||
'within_threshold' => $this->isWithinThreshold,
|
||||
'recommendation' => $this->recommendation,
|
||||
'slowness_factor' => $this->duration / $this->threshold,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class PhpObjectExporter
|
||||
* Eigene Export-Funktion, die rekursiv ein PHP-Array mit echten Konstruktoraufrufen exportiert.
|
||||
* (Variante unten für Standardfälle ausreichend! Für verschachtelte/nicht-indizierte Arrays ggf. anpassen.)
|
||||
*/
|
||||
public static function export($value)
|
||||
public static function export(mixed $value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$exported = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
@@ -9,10 +10,15 @@ namespace App\Framework\Core;
|
||||
final class ProgressMeter
|
||||
{
|
||||
private int $total;
|
||||
|
||||
private int $current = 0;
|
||||
|
||||
private int $lastPercent = 0;
|
||||
|
||||
private bool $isConsole;
|
||||
|
||||
private int $width;
|
||||
|
||||
private string $format;
|
||||
|
||||
/**
|
||||
@@ -62,13 +68,16 @@ final class ProgressMeter
|
||||
switch ($this->format) {
|
||||
case 'bar':
|
||||
$this->displayBar($percent);
|
||||
|
||||
break;
|
||||
case 'percent':
|
||||
$this->displayPercent($percent);
|
||||
|
||||
break;
|
||||
case 'both':
|
||||
default:
|
||||
$this->displayBoth($percent);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Router\ValueObjects\ParameterCollection;
|
||||
|
||||
interface Route
|
||||
{
|
||||
public string $controller {get;}
|
||||
@@ -15,4 +17,11 @@ interface Route
|
||||
public string $name {get;}
|
||||
|
||||
public string $path {get;}
|
||||
|
||||
public array $attributes {get;}
|
||||
|
||||
/**
|
||||
* Get type-safe parameter collection (preferred over legacy $parameters array)
|
||||
*/
|
||||
public function getParameterCollection(): ParameterCollection;
|
||||
}
|
||||
|
||||
@@ -4,78 +4,132 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
||||
use App\Framework\Router\CompiledPattern;
|
||||
use App\Framework\Router\CompiledRoutes;
|
||||
use App\Framework\Router\RouteData;
|
||||
use App\Framework\Router\ValueObjects\MethodParameter;
|
||||
use App\Framework\Router\ValueObjects\ParameterCollection;
|
||||
use App\Framework\Router\ValueObjects\SubdomainPattern;
|
||||
|
||||
final readonly class RouteCompiler implements AttributeCompiler
|
||||
final readonly class RouteCompiler
|
||||
{
|
||||
/** @var array<string, StaticRoute|DynamicRoute> */
|
||||
private array $named;
|
||||
|
||||
/**
|
||||
* @param array<int, array{method: string, path: string, controller: class-string, handler: string}> $routes
|
||||
* @return array<string, array{static: array<string, array>, dynamic: array<int, array{regex: string, params: array, handler: array}>}>
|
||||
* Compile routes directly from DiscoveredAttribute objects with subdomain support
|
||||
* @return array<string, array<string, array{static: array<string, StaticRoute>, dynamic: array<int, DynamicRoute>}>>
|
||||
*/
|
||||
public function compile(array $routes): array
|
||||
public function compile(DiscoveredAttribute ...$discoveredRoutes): array
|
||||
{
|
||||
$compiled = [];
|
||||
$named = [];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
foreach ($discoveredRoutes as $discoveredAttribute) {
|
||||
// Create actual Route attribute instance
|
||||
$routeAttribute = $discoveredAttribute->createAttributeInstance();
|
||||
if (! $routeAttribute instanceof Route) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = is_string($route['http_method']) ? strtoupper($route['http_method']) : $route['http_method']->value;
|
||||
$path = $route['path'];
|
||||
$routeName = $route['name'] ?? '';
|
||||
// Extract route data directly from the Route attribute
|
||||
$method = strtoupper($routeAttribute->method->value);
|
||||
$path = $routeAttribute->path;
|
||||
$routeName = $routeAttribute->name;
|
||||
|
||||
$compiled[$method] ??= ['static' => [], 'dynamic' => []];
|
||||
// Process subdomain patterns
|
||||
$subdomainPatterns = SubdomainPattern::fromInput($routeAttribute->subdomain);
|
||||
|
||||
if (! str_contains($path, '{')) {
|
||||
// Statische Route
|
||||
$staticRoute = new StaticRoute(
|
||||
$route['class'],
|
||||
$route['method'],
|
||||
$route['parameters'] ?? [],
|
||||
$routeName,
|
||||
$path,
|
||||
$route['attributes']
|
||||
);
|
||||
// If no subdomains specified, use default
|
||||
if (empty($subdomainPatterns)) {
|
||||
$subdomainPatterns = [new SubdomainPattern('')];
|
||||
}
|
||||
|
||||
$compiled[$method]['static'][$path] = $staticRoute;
|
||||
foreach ($subdomainPatterns as $subdomainPattern) {
|
||||
$subdomainKey = $subdomainPattern->getCompilationKey();
|
||||
|
||||
if($routeName) {
|
||||
$named[$routeName] = $staticRoute;
|
||||
}
|
||||
$compiled[$method] ??= [];
|
||||
$compiled[$method][$subdomainKey] ??= ['static' => [], 'dynamic' => []];
|
||||
|
||||
} else {
|
||||
// Dynamische Route
|
||||
$paramNames = [];
|
||||
$regex = $this->convertPathToRegex($path, $paramNames);
|
||||
if (! str_contains($path, '{')) {
|
||||
// Static route
|
||||
$parameterCollection = $this->createParameterCollection($discoveredAttribute->additionalData['parameters'] ?? []);
|
||||
$staticRoute = new StaticRoute(
|
||||
controller : $discoveredAttribute->className->getFullyQualified(),
|
||||
action : $discoveredAttribute->methodName?->toString() ?? '',
|
||||
parameters : $discoveredAttribute->additionalData['parameters'] ?? [],
|
||||
name : $routeName ?? '',
|
||||
path : $path,
|
||||
attributes : $discoveredAttribute->additionalData['attributes'] ?? [],
|
||||
parameterCollection: $parameterCollection
|
||||
);
|
||||
|
||||
$dynamicRoute = new DynamicRoute(
|
||||
$regex,
|
||||
$paramNames,
|
||||
$route['class'],
|
||||
$route['method'],
|
||||
$route['parameters'],
|
||||
[],
|
||||
$routeName,
|
||||
$path,
|
||||
$route['attributes']
|
||||
);
|
||||
$compiled[$method][$subdomainKey]['static'][$path] = $staticRoute;
|
||||
|
||||
$compiled[$method]['dynamic'][] = $dynamicRoute;
|
||||
if ($routeName) {
|
||||
$named[$routeName] = $staticRoute;
|
||||
}
|
||||
} else {
|
||||
// Dynamic route
|
||||
$paramNames = [];
|
||||
$regex = $this->convertPathToRegex($path, $paramNames);
|
||||
$parameterCollection = $this->createParameterCollection($discoveredAttribute->additionalData['parameters'] ?? []);
|
||||
|
||||
if($routeName) {
|
||||
$named[$routeName] = $dynamicRoute;
|
||||
$dynamicRoute = new DynamicRoute(
|
||||
regex : $regex,
|
||||
paramNames : $paramNames,
|
||||
controller : $discoveredAttribute->className->getFullyQualified(),
|
||||
action : $discoveredAttribute->methodName?->toString() ?? '',
|
||||
parameters : $discoveredAttribute->additionalData['parameters'] ?? [],
|
||||
paramValues : [],
|
||||
name : $routeName ?? '',
|
||||
path : $path,
|
||||
attributes : $discoveredAttribute->additionalData['attributes'] ?? [],
|
||||
parameterCollection: $parameterCollection
|
||||
);
|
||||
|
||||
$compiled[$method][$subdomainKey]['dynamic'][] = $dynamicRoute;
|
||||
|
||||
if ($routeName) {
|
||||
$named[$routeName] = $dynamicRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($this->named)) {
|
||||
if (! isset($this->named)) {
|
||||
$this->named = $named;
|
||||
}
|
||||
|
||||
return $compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert legacy parameters array to ParameterCollection
|
||||
*/
|
||||
private function createParameterCollection(array $parameters): ParameterCollection
|
||||
{
|
||||
$methodParameters = [];
|
||||
|
||||
foreach ($parameters as $param) {
|
||||
$methodParameters[] = new MethodParameter(
|
||||
name: $param['name'],
|
||||
type: $param['type'],
|
||||
isBuiltin: $param['isBuiltin'],
|
||||
hasDefault: $param['hasDefault'],
|
||||
default: $param['default'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return new ParameterCollection(...$methodParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{method: string, path: string, controller: class-string, handler: string}> $routes
|
||||
* @return array<string, StaticRoute|DynamicRoute>
|
||||
*/
|
||||
public function compileNamedRoutes(array $routes): array
|
||||
{
|
||||
return $this->named;
|
||||
@@ -110,58 +164,153 @@ final readonly class RouteCompiler implements AttributeCompiler
|
||||
return Route::class;
|
||||
}
|
||||
|
||||
public function compileOptimized(array $routes): CompiledRoutes
|
||||
/**
|
||||
* Compile optimized routes directly from DiscoveredAttribute objects with subdomain support
|
||||
*/
|
||||
public function compileOptimized(DiscoveredAttribute ...$discoveredRoutes): CompiledRoutes
|
||||
{
|
||||
$compiled = $this->compile($routes);
|
||||
$compiled = $this->compile(...$discoveredRoutes);
|
||||
$optimizedStatic = [];
|
||||
$optimizedDynamic = [];
|
||||
|
||||
foreach($compiled as $method => $routes) {
|
||||
$optimizedStatic[$method] = $routes['static'];
|
||||
foreach ($compiled as $method => $subdomainRoutes) {
|
||||
foreach ($subdomainRoutes as $subdomainKey => $routes) {
|
||||
$optimizedStatic[$method][$subdomainKey] = $routes['static'];
|
||||
|
||||
if(!empty($routes['dynamic'])) {
|
||||
$optimizedDynamic[$method] = $this->createCompiledPattern($routes['dynamic']);
|
||||
if (! empty($routes['dynamic'])) {
|
||||
$optimizedDynamic[$method][$subdomainKey] = $this->createCompiledPattern($routes['dynamic']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CompiledRoutes($optimizedStatic, $optimizedDynamic, $this->named);
|
||||
}
|
||||
|
||||
private function createCompiledPattern(mixed $dynamicRoutes): CompiledPattern
|
||||
/**
|
||||
* Creates optimized compiled patterns with route prioritization
|
||||
* @param array<int, DynamicRoute> $dynamicRoutes
|
||||
*/
|
||||
private function createCompiledPattern(array $dynamicRoutes): CompiledPattern
|
||||
{
|
||||
$patterns = [];
|
||||
// Sort routes by priority (specific patterns first, then by path complexity)
|
||||
$prioritizedRoutes = $this->prioritizeRoutes($dynamicRoutes);
|
||||
|
||||
// Group routes into batches for smaller regex patterns
|
||||
$routeBatches = $this->groupRoutesByComplexity($prioritizedRoutes);
|
||||
|
||||
$compiledBatches = [];
|
||||
/** @var array<int, RouteData> $routeData */
|
||||
$routeData = [];
|
||||
$currentIndex = 1; // Nach Full Match (Index 0)
|
||||
$globalIndex = 0;
|
||||
|
||||
foreach($dynamicRoutes as $index => $route) {
|
||||
$pattern = $this->stripAnchors($route->regex);
|
||||
$patterns[] = "({$pattern})";
|
||||
foreach ($routeBatches as $batchName => $batch) {
|
||||
$patterns = [];
|
||||
$currentIndex = 1; // After full match (Index 0)
|
||||
|
||||
// Route-Gruppe Index merken
|
||||
$routeGroupIndex = $currentIndex++;
|
||||
foreach ($batch as $route) {
|
||||
$pattern = $this->stripAnchors($route->regex);
|
||||
$patterns[] = "({$pattern})";
|
||||
|
||||
// Parameter-Mapping KORREKT berechnen
|
||||
$paramMap = [];
|
||||
foreach($route->paramNames as $paramName) {
|
||||
$paramMap[$paramName] = $currentIndex++;
|
||||
// Route-Gruppe Index merken
|
||||
$routeGroupIndex = $currentIndex++;
|
||||
|
||||
// Parameter-Mapping berechnen
|
||||
$paramMap = [];
|
||||
foreach ($route->paramNames as $paramName) {
|
||||
$paramMap[$paramName] = $currentIndex++;
|
||||
}
|
||||
|
||||
$routeData[$globalIndex] = new RouteData(
|
||||
route: $route,
|
||||
paramMap: $paramMap,
|
||||
routeGroupIndex: $routeGroupIndex,
|
||||
batch: $batchName,
|
||||
regex: '~^' . $this->stripAnchors($route->regex) . '$~' // Pre-compiled individual regex
|
||||
);
|
||||
|
||||
$globalIndex++;
|
||||
}
|
||||
|
||||
$routeData[$index] = [
|
||||
'route' => $route,
|
||||
'paramMap' => $paramMap,
|
||||
'routeGroupIndex' => $routeGroupIndex
|
||||
$compiledBatches[$batchName] = [
|
||||
'regex' => '~^(?:' . implode('|', $patterns) . ')$~',
|
||||
'routes' => array_slice($routeData, $globalIndex - count($batch), count($batch), true),
|
||||
];
|
||||
}
|
||||
|
||||
$combinedRegex = '~^(?:' . implode('|', $patterns) . ')$~';
|
||||
return new CompiledPattern($combinedRegex, $routeData);
|
||||
// Use the first batch's regex for backward compatibility, but store all batches
|
||||
$primaryRegex = ! empty($compiledBatches) ? reset($compiledBatches)['regex'] : '~^$~';
|
||||
|
||||
return new CompiledPattern($primaryRegex, $routeData, $compiledBatches);
|
||||
}
|
||||
|
||||
private function stripAnchors($regex): string
|
||||
/**
|
||||
* Prioritize routes by specificity and expected frequency
|
||||
* @param array<DynamicRoute> $routes
|
||||
* @return array<DynamicRoute>
|
||||
*/
|
||||
private function prioritizeRoutes(array $routes): array
|
||||
{
|
||||
usort($routes, function ($a, $b) {
|
||||
// 1. API routes first (typically more frequent)
|
||||
$aIsApi = str_starts_with($a->path, '/api/');
|
||||
$bIsApi = str_starts_with($b->path, '/api/');
|
||||
if ($aIsApi !== $bIsApi) {
|
||||
return $bIsApi <=> $aIsApi; // API routes first
|
||||
}
|
||||
|
||||
// 2. Routes with fewer parameters first (more specific)
|
||||
$aParamCount = count($a->paramNames);
|
||||
$bParamCount = count($b->paramNames);
|
||||
if ($aParamCount !== $bParamCount) {
|
||||
return $aParamCount <=> $bParamCount;
|
||||
}
|
||||
|
||||
// 3. Shorter paths first (typically more specific)
|
||||
$aLength = strlen($a->path);
|
||||
$bLength = strlen($b->path);
|
||||
|
||||
return $aLength <=> $bLength;
|
||||
});
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group routes by complexity for optimized batch processing
|
||||
* @param array<DynamicRoute> $routes
|
||||
* @return array<string, array<DynamicRoute>>
|
||||
*/
|
||||
private function groupRoutesByComplexity(array $routes): array
|
||||
{
|
||||
$groups = [
|
||||
'simple' => [], // /api/{id}, /user/{id} - 1 parameter
|
||||
'medium' => [], // /api/{type}/{id} - 2 parameters
|
||||
'complex' => [], // 3+ parameters or wildcards
|
||||
];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$paramCount = count($route->paramNames);
|
||||
$hasWildcard = str_contains($route->path, '{*}');
|
||||
|
||||
if ($hasWildcard || $paramCount >= 3) {
|
||||
$groups['complex'][] = $route;
|
||||
} elseif ($paramCount === 2) {
|
||||
$groups['medium'][] = $route;
|
||||
} else {
|
||||
$groups['simple'][] = $route;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty groups
|
||||
return array_filter($groups, fn ($group) => ! empty($group));
|
||||
}
|
||||
|
||||
private function stripAnchors(string $regex): string
|
||||
{
|
||||
if (preg_match('/^~\^(.+)\$~$/', $regex, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return $regex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Attributes\Route as RouteAttribute;
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Discovery\FileVisitor;
|
||||
|
||||
/**
|
||||
* Visitor zum Auffinden von Routen
|
||||
*/
|
||||
final class RouteDiscoveryVisitor implements FileVisitor
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $routesByPath = [];
|
||||
private array $routesByController = [];
|
||||
private bool $routesSorted = false;
|
||||
|
||||
public function onScanStart(): void
|
||||
{
|
||||
$this->routes = [];
|
||||
$this->routesByPath = [];
|
||||
$this->routesByController = [];
|
||||
$this->routesSorted = false;
|
||||
}
|
||||
|
||||
public function onIncrementalScanStart(): void
|
||||
{
|
||||
// Bei inkrementellem Scan behalten wir bestehende Routen
|
||||
// markieren sie aber als unsortiert
|
||||
$this->routesSorted = false;
|
||||
}
|
||||
|
||||
public function onIncrementalScanComplete(): void
|
||||
{
|
||||
// Nach inkrementellem Scan sortieren
|
||||
$this->sortRoutes();
|
||||
}
|
||||
|
||||
public function visitClass(string $className, string $filePath): void
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new \ReflectionClass($className);
|
||||
|
||||
// Prüfe, ob die Klasse überhaupt Methoden mit Route-Attributen hat
|
||||
$hasRouteMethods = false;
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
if (!empty($method->getAttributes(RouteAttribute::class))) {
|
||||
$hasRouteMethods = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasRouteMethods) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Nur Methoden mit Route-Attribut verarbeiten
|
||||
foreach ($reflection->getMethods() as $method) {
|
||||
$routeAttributes = $method->getAttributes(RouteAttribute::class);
|
||||
|
||||
foreach ($routeAttributes as $routeAttr) {
|
||||
/** @var RouteAttribute $route */
|
||||
$route = $routeAttr->newInstance();
|
||||
|
||||
$routeData = [
|
||||
'path' => $route->path,
|
||||
'method' => $route->method,
|
||||
'controller' => $className,
|
||||
'action' => $method->getName(),
|
||||
'middleware' => $route->middleware ?? [],
|
||||
'priority' => $route->priority ?? 0
|
||||
];
|
||||
|
||||
$this->routes[] = $routeData;
|
||||
|
||||
// Erstelle Indizes für schnelleren Zugriff
|
||||
$path = strtolower($route->path);
|
||||
$httpMethod = strtoupper($route->method->value);
|
||||
$key = "{$httpMethod}:{$path}";
|
||||
|
||||
if (!isset($this->routesByPath[$key])) {
|
||||
$this->routesByPath[$key] = [];
|
||||
}
|
||||
$this->routesByPath[$key][] = $routeData;
|
||||
|
||||
if (!isset($this->routesByController[$className])) {
|
||||
$this->routesByController[$className] = [];
|
||||
}
|
||||
$this->routesByController[$className][] = $routeData;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Fehler beim Verarbeiten der Routen für {$className}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function onScanComplete(): void
|
||||
{
|
||||
// Sortiere Routen nach Priorität
|
||||
$this->sortRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert die Routen nach Priorität (absteigend)
|
||||
*/
|
||||
private function sortRoutes(): void
|
||||
{
|
||||
if ($this->routesSorted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sortiere Hauptrouten-Array
|
||||
usort($this->routes, function($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
|
||||
// Sortiere Indizes
|
||||
foreach ($this->routesByPath as $key => $routes) {
|
||||
usort($this->routesByPath[$key], function($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($this->routesByController as $className => $routes) {
|
||||
usort($this->routesByController[$className], function($a, $b) {
|
||||
return $b['priority'] <=> $a['priority'];
|
||||
});
|
||||
}
|
||||
|
||||
$this->routesSorted = true;
|
||||
}
|
||||
|
||||
public function loadFromCache(Cache $cache): void
|
||||
{
|
||||
$cacheItem = $cache->get($this->getCacheKey());
|
||||
if ($cacheItem->isHit) {
|
||||
if (is_array($cacheItem->value) && isset($cacheItem->value['routes'], $cacheItem->value['byPath'], $cacheItem->value['byController'])) {
|
||||
$this->routes = $cacheItem->value['routes'];
|
||||
$this->routesByPath = $cacheItem->value['byPath'];
|
||||
$this->routesByController = $cacheItem->value['byController'];
|
||||
$this->routesSorted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return 'routes';
|
||||
}
|
||||
|
||||
public function getCacheableData(): mixed
|
||||
{
|
||||
// Stelle sicher, dass die Routen sortiert sind, bevor wir sie cachen
|
||||
if (!$this->routesSorted) {
|
||||
$this->sortRoutes();
|
||||
}
|
||||
|
||||
return [
|
||||
'routes' => $this->routes,
|
||||
'byPath' => $this->routesByPath,
|
||||
'byController' => $this->routesByController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle gefundenen Routen zurück
|
||||
*/
|
||||
public function getRoutes(): array
|
||||
{
|
||||
if (!$this->routesSorted) {
|
||||
$this->sortRoutes();
|
||||
}
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet eine Route für einen bestimmten Pfad und HTTP-Methode
|
||||
*/
|
||||
public function findRoute(string $path, string $httpMethod): ?array
|
||||
{
|
||||
$path = strtolower(rtrim($path, '/'));
|
||||
$httpMethod = strtoupper($httpMethod);
|
||||
$key = "{$httpMethod}:{$path}";
|
||||
|
||||
// Direkte Übereinstimmung
|
||||
if (isset($this->routesByPath[$key]) && !empty($this->routesByPath[$key])) {
|
||||
return $this->routesByPath[$key][0]; // Höchste Priorität zuerst
|
||||
}
|
||||
|
||||
// Prüfe auf parametrisierte Routen, wenn keine direkte Übereinstimmung
|
||||
foreach ($this->routesByPath as $routeKey => $routes) {
|
||||
list($routeMethod, $routePath) = explode(':', $routeKey, 2);
|
||||
|
||||
if ($routeMethod !== $httpMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Konvertiere Route-Pfad in ein reguläres Ausdrucksmuster
|
||||
$pattern = $this->routePathToRegex($routePath);
|
||||
if (preg_match($pattern, $path)) {
|
||||
return $routes[0]; // Höchste Priorität zuerst
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen Route-Pfad in ein reguläres Ausdrucksmuster
|
||||
*/
|
||||
private function routePathToRegex(string $routePath): string
|
||||
{
|
||||
// Ersetze Platzhalter wie {id} durch Regex-Gruppen
|
||||
$pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath);
|
||||
return '/^' . str_replace('/', '\/', $pattern) . '$/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Routen für einen bestimmten Controller zurück
|
||||
*/
|
||||
public function getRoutesForController(string $controllerClass): array
|
||||
{
|
||||
return $this->routesByController[$controllerClass] ?? [];
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use ReflectionMethod;
|
||||
use App\Framework\Reflection\WrappedReflectionClass;
|
||||
use App\Framework\Reflection\WrappedReflectionMethod;
|
||||
use App\Framework\Router\ValueObjects\MethodParameter;
|
||||
use App\Framework\Router\ValueObjects\ParameterCollection;
|
||||
|
||||
final readonly class RouteMapper implements AttributeMapper
|
||||
{
|
||||
@@ -14,26 +17,41 @@ final readonly class RouteMapper implements AttributeMapper
|
||||
return Route::class;
|
||||
}
|
||||
|
||||
public function map(object $reflectionTarget, object $attributeInstance): ?array
|
||||
public function map(WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget, object $attributeInstance): ?array
|
||||
{
|
||||
if (! $reflectionTarget instanceof ReflectionMethod || ! $attributeInstance instanceof Route) {
|
||||
if (! $attributeInstance instanceof Route) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Route attributes are only valid on methods
|
||||
if (! $reflectionTarget instanceof WrappedReflectionMethod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Collect all non-Route attributes on the method
|
||||
$attributes = [];
|
||||
foreach ($reflectionTarget->getAttributes() as $attribute) {
|
||||
if ($attribute->getName() !== Route::class) {
|
||||
$attributes[] = $attribute->getName();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Extract method parameters using type-safe Value Objects
|
||||
$methodParameters = [];
|
||||
foreach ($reflectionTarget->getParameters() as $parameter) {
|
||||
$methodParameters[] = MethodParameter::fromWrappedParameter($parameter);
|
||||
}
|
||||
$parameterCollection = new ParameterCollection(...$methodParameters);
|
||||
|
||||
return [
|
||||
'class' => $reflectionTarget->getDeclaringClass()->getName(),
|
||||
'class' => $reflectionTarget->getDeclaringClass()->getFullyQualified(),
|
||||
'method' => $reflectionTarget->getName(),
|
||||
'http_method' => $attributeInstance->method,
|
||||
'path' => $attributeInstance->path,
|
||||
'name' => $attributeInstance->name,
|
||||
'attributes' => $attributes
|
||||
'parameters' => $parameterCollection->toLegacyArray(), // Backward compatibility
|
||||
'parameter_collection' => $parameterCollection, // New type-safe collection
|
||||
'attributes' => $attributes,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Router\ValueObjects\ParameterCollection;
|
||||
|
||||
final readonly class StaticRoute implements Route
|
||||
{
|
||||
public function __construct(
|
||||
@@ -12,6 +14,22 @@ final readonly class StaticRoute implements Route
|
||||
public array $parameters,
|
||||
public string $name = '',
|
||||
public string $path = '',
|
||||
public array $attributes = []
|
||||
) {}
|
||||
public array $attributes = [],
|
||||
public ?ParameterCollection $parameterCollection = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type-safe parameter collection (preferred)
|
||||
*/
|
||||
public function getParameterCollection(): ParameterCollection
|
||||
{
|
||||
if ($this->parameterCollection !== null) {
|
||||
return $this->parameterCollection;
|
||||
}
|
||||
|
||||
// Fallback: Create ParameterCollection from legacy array
|
||||
// This should not happen in production with proper RouteCompiler
|
||||
return new ParameterCollection();
|
||||
}
|
||||
}
|
||||
|
||||
207
src/Framework/Core/ValueObjects/Byte.php
Normal file
207
src/Framework/Core/ValueObjects/Byte.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class Byte
|
||||
{
|
||||
public function __construct(
|
||||
private int $bytes
|
||||
) {
|
||||
if ($bytes < 0) {
|
||||
throw new InvalidArgumentException('Bytes cannot be negative');
|
||||
}
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function fromBytes(int $bytes): self
|
||||
{
|
||||
return new self($bytes);
|
||||
}
|
||||
|
||||
public static function fromUnit(float $value, ByteUnit $unit): self
|
||||
{
|
||||
return new self((int) round($value * $unit->getMultiplier()));
|
||||
}
|
||||
|
||||
public static function fromKilobytes(float $kilobytes): self
|
||||
{
|
||||
return self::fromUnit($kilobytes, ByteUnit::KILOBYTE);
|
||||
}
|
||||
|
||||
public static function fromMegabytes(float $megabytes): self
|
||||
{
|
||||
return self::fromUnit($megabytes, ByteUnit::MEGABYTE);
|
||||
}
|
||||
|
||||
public static function fromGigabytes(float $gigabytes): self
|
||||
{
|
||||
return self::fromUnit($gigabytes, ByteUnit::GIGABYTE);
|
||||
}
|
||||
|
||||
public static function fromTerabytes(float $terabytes): self
|
||||
{
|
||||
return self::fromUnit($terabytes, ByteUnit::TERABYTE);
|
||||
}
|
||||
|
||||
// Parse from human-readable strings
|
||||
public static function parse(string $value): self
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if (preg_match('/^(\d+(?:\.\d+)?)\s*([A-Za-z]*)$/', $value, $matches)) {
|
||||
$number = (float) $matches[1];
|
||||
$unitString = $matches[2] ?: 'B';
|
||||
|
||||
$unit = ByteUnit::fromString($unitString);
|
||||
|
||||
return self::fromUnit($number, $unit);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("Invalid byte format: $value");
|
||||
}
|
||||
|
||||
// Conversion Methods
|
||||
public function toBytes(): int
|
||||
{
|
||||
return $this->bytes;
|
||||
}
|
||||
|
||||
public function toUnit(ByteUnit $unit, int $precision = 2): float
|
||||
{
|
||||
return round($this->bytes / $unit->getMultiplier(), $precision);
|
||||
}
|
||||
|
||||
public function toKilobytes(int $precision = 2): float
|
||||
{
|
||||
return $this->toUnit(ByteUnit::KILOBYTE, $precision);
|
||||
}
|
||||
|
||||
public function toMegabytes(int $precision = 2): float
|
||||
{
|
||||
return $this->toUnit(ByteUnit::MEGABYTE, $precision);
|
||||
}
|
||||
|
||||
public function toGigabytes(int $precision = 2): float
|
||||
{
|
||||
return $this->toUnit(ByteUnit::GIGABYTE, $precision);
|
||||
}
|
||||
|
||||
public function toTerabytes(int $precision = 2): float
|
||||
{
|
||||
return $this->toUnit(ByteUnit::TERABYTE, $precision);
|
||||
}
|
||||
|
||||
// Human-readable format
|
||||
public function toHumanReadable(int $precision = 2): string
|
||||
{
|
||||
$unit = ByteUnit::bestUnitFor($this->bytes);
|
||||
$value = $this->toUnit($unit, $precision);
|
||||
|
||||
return $value . ' ' . $unit->value;
|
||||
}
|
||||
|
||||
public function getBestUnit(): ByteUnit
|
||||
{
|
||||
return ByteUnit::bestUnitFor($this->bytes);
|
||||
}
|
||||
|
||||
// Arithmetic Operations
|
||||
public function add(Byte $other): self
|
||||
{
|
||||
return new self($this->bytes + $other->bytes);
|
||||
}
|
||||
|
||||
public function subtract(Byte $other): self
|
||||
{
|
||||
$result = $this->bytes - $other->bytes;
|
||||
if ($result < 0) {
|
||||
throw new InvalidArgumentException('Subtraction would result in negative bytes');
|
||||
}
|
||||
|
||||
return new self($result);
|
||||
}
|
||||
|
||||
public function multiply(float $factor): self
|
||||
{
|
||||
if ($factor < 0) {
|
||||
throw new InvalidArgumentException('Factor cannot be negative');
|
||||
}
|
||||
|
||||
return new self((int) round($this->bytes * $factor));
|
||||
}
|
||||
|
||||
public function divide(float $divisor): self
|
||||
{
|
||||
if ($divisor <= 0) {
|
||||
throw new InvalidArgumentException('Divisor must be positive');
|
||||
}
|
||||
|
||||
return new self((int) round($this->bytes / $divisor));
|
||||
}
|
||||
|
||||
// Comparison Methods
|
||||
public function equals(Byte $other): bool
|
||||
{
|
||||
return $this->bytes === $other->bytes;
|
||||
}
|
||||
|
||||
public function greaterThan(Byte $other): bool
|
||||
{
|
||||
return $this->bytes > $other->bytes;
|
||||
}
|
||||
|
||||
public function lessThan(Byte $other): bool
|
||||
{
|
||||
return $this->bytes < $other->bytes;
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->bytes === 0;
|
||||
}
|
||||
|
||||
public function isNotEmpty(): bool
|
||||
{
|
||||
return $this->bytes > 0;
|
||||
}
|
||||
|
||||
public function percentOf(Byte $total): Percentage
|
||||
{
|
||||
if ($total->bytes === 0) {
|
||||
return Percentage::zero();
|
||||
}
|
||||
|
||||
return Percentage::fromRatio($this->bytes, $total->bytes);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toHumanReadable();
|
||||
}
|
||||
|
||||
// Common constants
|
||||
public static function zero(): self
|
||||
{
|
||||
return new self(0);
|
||||
}
|
||||
|
||||
public static function oneKilobyte(): self
|
||||
{
|
||||
return self::fromUnit(1, ByteUnit::KILOBYTE);
|
||||
}
|
||||
|
||||
public static function oneMegabyte(): self
|
||||
{
|
||||
return self::fromUnit(1, ByteUnit::MEGABYTE);
|
||||
}
|
||||
|
||||
public static function oneGigabyte(): self
|
||||
{
|
||||
return self::fromUnit(1, ByteUnit::GIGABYTE);
|
||||
}
|
||||
}
|
||||
63
src/Framework/Core/ValueObjects/ByteUnit.php
Normal file
63
src/Framework/Core/ValueObjects/ByteUnit.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
enum ByteUnit: string
|
||||
{
|
||||
case BYTE = 'B';
|
||||
case KILOBYTE = 'KB';
|
||||
case MEGABYTE = 'MB';
|
||||
case GIGABYTE = 'GB';
|
||||
case TERABYTE = 'TB';
|
||||
|
||||
public function getMultiplier(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::BYTE => 1,
|
||||
self::KILOBYTE => 1024,
|
||||
self::MEGABYTE => 1024 * 1024,
|
||||
self::GIGABYTE => 1024 * 1024 * 1024,
|
||||
self::TERABYTE => 1024 * 1024 * 1024 * 1024,
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::BYTE => 'Byte',
|
||||
self::KILOBYTE => 'Kilobyte',
|
||||
self::MEGABYTE => 'Megabyte',
|
||||
self::GIGABYTE => 'Gigabyte',
|
||||
self::TERABYTE => 'Terabyte',
|
||||
};
|
||||
}
|
||||
|
||||
public static function bestUnitFor(int $bytes): self
|
||||
{
|
||||
if ($bytes >= self::TERABYTE->getMultiplier()) {
|
||||
return self::TERABYTE;
|
||||
} elseif ($bytes >= self::GIGABYTE->getMultiplier()) {
|
||||
return self::GIGABYTE;
|
||||
} elseif ($bytes >= self::MEGABYTE->getMultiplier()) {
|
||||
return self::MEGABYTE;
|
||||
} elseif ($bytes >= self::KILOBYTE->getMultiplier()) {
|
||||
return self::KILOBYTE;
|
||||
} else {
|
||||
return self::BYTE;
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromString(string $unit): self
|
||||
{
|
||||
return match (strtoupper(trim($unit))) {
|
||||
'B', 'BYTE', 'BYTES' => self::BYTE,
|
||||
'K', 'KB', 'KILOBYTE', 'KILOBYTES' => self::KILOBYTE,
|
||||
'M', 'MB', 'MEGABYTE', 'MEGABYTES' => self::MEGABYTE,
|
||||
'G', 'GB', 'GIGABYTE', 'GIGABYTES' => self::GIGABYTE,
|
||||
'T', 'TB', 'TERABYTE', 'TERABYTES' => self::TERABYTE,
|
||||
default => throw new \InvalidArgumentException("Unknown byte unit: $unit"),
|
||||
};
|
||||
}
|
||||
}
|
||||
215
src/Framework/Core/ValueObjects/ClassName.php
Normal file
215
src/Framework/Core/ValueObjects/ClassName.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Immutable class name value object with namespace support
|
||||
*/
|
||||
final readonly class ClassName
|
||||
{
|
||||
/**
|
||||
* @var class-string
|
||||
*/
|
||||
private string $fullyQualified;
|
||||
|
||||
private string $namespace;
|
||||
|
||||
private string $shortName;
|
||||
|
||||
private function __construct(string $className)
|
||||
{
|
||||
if (empty($className)) {
|
||||
throw new InvalidArgumentException('Class name cannot be empty');
|
||||
}
|
||||
|
||||
// Remove the leading backslash
|
||||
$this->fullyQualified = ltrim($className, '\\');
|
||||
|
||||
if (! $this->isValidClassName($this->fullyQualified)) {
|
||||
throw new InvalidArgumentException("Invalid class name: {$className}");
|
||||
}
|
||||
|
||||
// Parse class name components directly in constructor
|
||||
$lastBackslash = strrpos($this->fullyQualified, '\\');
|
||||
|
||||
if ($lastBackslash === false) {
|
||||
$this->namespace = '';
|
||||
$this->shortName = $this->fullyQualified;
|
||||
} else {
|
||||
$this->namespace = substr($this->fullyQualified, 0, $lastBackslash);
|
||||
$this->shortName = substr($this->fullyQualified, $lastBackslash + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from string
|
||||
*/
|
||||
public static function create(string $className): self
|
||||
{
|
||||
return new self($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from object
|
||||
*/
|
||||
public static function fromObject(object $object): self
|
||||
{
|
||||
return new self($object::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fully qualified class name
|
||||
* @return class-string
|
||||
*/
|
||||
public function getFullyQualified(): string
|
||||
{
|
||||
return $this->fullyQualified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a namespace part
|
||||
*/
|
||||
public function getNamespace(): string
|
||||
{
|
||||
return $this->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a short class name (without a namespace)
|
||||
*/
|
||||
public function getShortName(): string
|
||||
{
|
||||
return $this->shortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get class name with leading backslash
|
||||
*/
|
||||
public function getWithLeadingSlash(): string
|
||||
{
|
||||
return '\\' . $this->fullyQualified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if class exists
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
if (empty($this->fullyQualified)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return class_exists($this->fullyQualified) || interface_exists($this->fullyQualified) || trait_exists($this->fullyQualified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if class implements interface
|
||||
*/
|
||||
public function implementsInterface(string $interface): bool
|
||||
{
|
||||
if (! $this->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_subclass_of($this->fullyQualified, $interface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if class extends another class
|
||||
*/
|
||||
public function extends(string $parentClass): bool
|
||||
{
|
||||
if (! $this->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_subclass_of($this->fullyQualified, $parentClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if in same namespace
|
||||
*/
|
||||
public function inSameNamespace(self $other): bool
|
||||
{
|
||||
return $this->namespace === $other->namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if in namespace (or sub-namespace)
|
||||
*/
|
||||
public function inNamespace(string $namespace): bool
|
||||
{
|
||||
$namespace = trim($namespace, '\\');
|
||||
|
||||
return str_starts_with($this->namespace, $namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent namespace
|
||||
*/
|
||||
public function getParentNamespace(): ?string
|
||||
{
|
||||
$parts = explode('\\', $this->namespace);
|
||||
if (count($parts) <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
array_pop($parts);
|
||||
|
||||
return implode('\\', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all namespace parts
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getNamespaceParts(): array
|
||||
{
|
||||
return $this->namespace ? explode('\\', $this->namespace) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if class name matches pattern
|
||||
*/
|
||||
public function matches(string $pattern): bool
|
||||
{
|
||||
return fnmatch($pattern, $this->fullyQualified, FNM_NOESCAPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation (short name for readability)
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->shortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string representation for debugging
|
||||
*/
|
||||
public function toDebugString(): string
|
||||
{
|
||||
return $this->namespace ? "{$this->namespace}\\{$this->shortName}" : $this->shortName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare for equality
|
||||
*/
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->fullyQualified === $other->fullyQualified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate class name format
|
||||
*/
|
||||
private function isValidClassName(string $className): bool
|
||||
{
|
||||
// Basic validation: should contain only alphanumeric, underscore, and backslash
|
||||
return preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\\\\]*$/', $className) === 1;
|
||||
}
|
||||
}
|
||||
202
src/Framework/Core/ValueObjects/Coordinates.php
Normal file
202
src/Framework/Core/ValueObjects/Coordinates.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Geographic coordinates value object (latitude/longitude)
|
||||
*/
|
||||
final readonly class Coordinates
|
||||
{
|
||||
public function __construct(
|
||||
public float $latitude,
|
||||
public float $longitude
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from latitude and longitude
|
||||
*/
|
||||
public static function fromLatLng(float $latitude, float $longitude): self
|
||||
{
|
||||
return new self($latitude, $longitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from decimal degrees string
|
||||
*/
|
||||
public static function fromString(string $coordinates): self
|
||||
{
|
||||
$parts = array_map('trim', explode(',', $coordinates));
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
throw new InvalidArgumentException('Coordinates must be in format "latitude,longitude"');
|
||||
}
|
||||
|
||||
$latitude = (float) $parts[0];
|
||||
$longitude = (float) $parts[1];
|
||||
|
||||
return new self($latitude, $longitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create coordinates for Berlin, Germany
|
||||
*/
|
||||
public static function berlin(): self
|
||||
{
|
||||
return new self(52.5200, 13.4050);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create coordinates for New York, USA
|
||||
*/
|
||||
public static function newYork(): self
|
||||
{
|
||||
return new self(40.7128, -74.0060);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create coordinates for London, UK
|
||||
*/
|
||||
public static function london(): self
|
||||
{
|
||||
return new self(51.5074, -0.1278);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance to another coordinate in kilometers
|
||||
*/
|
||||
public function distanceTo(self $other): float
|
||||
{
|
||||
$earthRadius = 6371; // Earth's radius in kilometers
|
||||
|
||||
$lat1Rad = deg2rad($this->latitude);
|
||||
$lat2Rad = deg2rad($other->latitude);
|
||||
$deltaLatRad = deg2rad($other->latitude - $this->latitude);
|
||||
$deltaLngRad = deg2rad($other->longitude - $this->longitude);
|
||||
|
||||
$a = sin($deltaLatRad / 2) * sin($deltaLatRad / 2) +
|
||||
cos($lat1Rad) * cos($lat2Rad) *
|
||||
sin($deltaLngRad / 2) * sin($deltaLngRad / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
return $earthRadius * $c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coordinates are within a certain radius of another point
|
||||
*/
|
||||
public function isWithinRadius(self $center, float $radiusKm): bool
|
||||
{
|
||||
return $this->distanceTo($center) <= $radiusKm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hemisphere information
|
||||
*/
|
||||
public function getHemisphere(): array
|
||||
{
|
||||
return [
|
||||
'north_south' => $this->latitude >= 0 ? 'Northern' : 'Southern',
|
||||
'east_west' => $this->longitude >= 0 ? 'Eastern' : 'Western',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coordinates are in the northern hemisphere
|
||||
*/
|
||||
public function isNorthern(): bool
|
||||
{
|
||||
return $this->latitude >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coordinates are in the southern hemisphere
|
||||
*/
|
||||
public function isSouthern(): bool
|
||||
{
|
||||
return $this->latitude < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coordinates are in the eastern hemisphere
|
||||
*/
|
||||
public function isEastern(): bool
|
||||
{
|
||||
return $this->longitude >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coordinates are in the western hemisphere
|
||||
*/
|
||||
public function isWestern(): bool
|
||||
{
|
||||
return $this->longitude < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted coordinates as string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return sprintf('%.6f,%.6f', $this->latitude, $this->longitude);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted coordinates with cardinal directions
|
||||
*/
|
||||
public function toCardinalString(): string
|
||||
{
|
||||
$latDirection = $this->latitude >= 0 ? 'N' : 'S';
|
||||
$lngDirection = $this->longitude >= 0 ? 'E' : 'W';
|
||||
|
||||
return sprintf(
|
||||
'%.6f°%s, %.6f°%s',
|
||||
abs($this->latitude),
|
||||
$latDirection,
|
||||
abs($this->longitude),
|
||||
$lngDirection
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'latitude' => $this->latitude,
|
||||
'longitude' => $this->longitude,
|
||||
'hemisphere' => $this->getHemisphere(),
|
||||
'formatted' => $this->toString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate coordinates
|
||||
*/
|
||||
private function validate(): void
|
||||
{
|
||||
if ($this->latitude < -90.0 || $this->latitude > 90.0) {
|
||||
throw new InvalidArgumentException(
|
||||
"Latitude must be between -90 and 90 degrees, got: {$this->latitude}"
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->longitude < -180.0 || $this->longitude > 180.0) {
|
||||
throw new InvalidArgumentException(
|
||||
"Longitude must be between -180 and 180 degrees, got: {$this->longitude}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
244
src/Framework/Core/ValueObjects/CountryCode.php
Normal file
244
src/Framework/Core/ValueObjects/CountryCode.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* ISO 3166-1 alpha-2 country code value object
|
||||
*/
|
||||
final readonly class CountryCode
|
||||
{
|
||||
private const array VALID_CODES = [
|
||||
'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT',
|
||||
'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI',
|
||||
'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY',
|
||||
'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN',
|
||||
'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM',
|
||||
'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK',
|
||||
'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL',
|
||||
'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM',
|
||||
'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR',
|
||||
'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN',
|
||||
'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS',
|
||||
'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK',
|
||||
'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW',
|
||||
'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP',
|
||||
'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM',
|
||||
'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW',
|
||||
'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM',
|
||||
'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF',
|
||||
'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW',
|
||||
'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI',
|
||||
'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW',
|
||||
];
|
||||
|
||||
private const array COUNTRY_NAMES = [
|
||||
'DE' => 'Germany',
|
||||
'US' => 'United States',
|
||||
'GB' => 'United Kingdom',
|
||||
'FR' => 'France',
|
||||
'IT' => 'Italy',
|
||||
'ES' => 'Spain',
|
||||
'NL' => 'Netherlands',
|
||||
'BE' => 'Belgium',
|
||||
'AT' => 'Austria',
|
||||
'CH' => 'Switzerland',
|
||||
'CN' => 'China',
|
||||
'JP' => 'Japan',
|
||||
'KR' => 'South Korea',
|
||||
'IN' => 'India',
|
||||
'RU' => 'Russia',
|
||||
'BR' => 'Brazil',
|
||||
'CA' => 'Canada',
|
||||
'AU' => 'Australia',
|
||||
'MX' => 'Mexico',
|
||||
'AR' => 'Argentina',
|
||||
// Add more as needed
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from string
|
||||
*/
|
||||
public static function fromString(string $code): self
|
||||
{
|
||||
return new self(strtoupper(trim($code)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create for Germany
|
||||
*/
|
||||
public static function germany(): self
|
||||
{
|
||||
return new self('DE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create for United States
|
||||
*/
|
||||
public static function unitedStates(): self
|
||||
{
|
||||
return new self('US');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country name if known
|
||||
*/
|
||||
public function getCountryName(): ?string
|
||||
{
|
||||
return self::COUNTRY_NAMES[$this->value] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a European Union country
|
||||
*/
|
||||
public function isEuropeanUnion(): bool
|
||||
{
|
||||
$euCountries = [
|
||||
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
|
||||
'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
|
||||
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
|
||||
];
|
||||
|
||||
return in_array($this->value, $euCountries, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a high-risk country for security
|
||||
*/
|
||||
public function isHighRiskCountry(): bool
|
||||
{
|
||||
// Common high-risk countries for web attacks
|
||||
$highRiskCountries = [
|
||||
'CN', 'RU', 'KP', 'IR', 'SY', 'AF', 'IQ', 'LY', 'SO', 'YE',
|
||||
];
|
||||
|
||||
return in_array($this->value, $highRiskCountries, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this country is known for bot farms or compromised infrastructure
|
||||
*/
|
||||
public function isBotFarmCountry(): bool
|
||||
{
|
||||
$botFarmCountries = [
|
||||
'CN', 'RU', 'VN', 'IN', 'BD', 'PK', 'UA', 'RO',
|
||||
];
|
||||
|
||||
return in_array($this->value, $botFarmCountries, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an anonymous or unknown country code
|
||||
*/
|
||||
public function isAnonymous(): bool
|
||||
{
|
||||
$anonymousCodes = ['XX', 'ZZ', 'A1', 'A2'];
|
||||
|
||||
return in_array($this->value, $anonymousCodes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get continent
|
||||
*/
|
||||
public function getContinent(): ?string
|
||||
{
|
||||
$continents = [
|
||||
'AD' => 'Europe', 'AE' => 'Asia', 'AF' => 'Asia', 'AG' => 'North America',
|
||||
'AI' => 'North America', 'AL' => 'Europe', 'AM' => 'Asia', 'AO' => 'Africa',
|
||||
'AQ' => 'Antarctica', 'AR' => 'South America', 'AS' => 'Oceania', 'AT' => 'Europe',
|
||||
'AU' => 'Oceania', 'AW' => 'North America', 'AX' => 'Europe', 'AZ' => 'Asia',
|
||||
'BA' => 'Europe', 'BB' => 'North America', 'BD' => 'Asia', 'BE' => 'Europe',
|
||||
'BF' => 'Africa', 'BG' => 'Europe', 'BH' => 'Asia', 'BI' => 'Africa',
|
||||
'BJ' => 'Africa', 'BL' => 'North America', 'BM' => 'North America', 'BN' => 'Asia',
|
||||
'BO' => 'South America', 'BQ' => 'North America', 'BR' => 'South America', 'BS' => 'North America',
|
||||
'BT' => 'Asia', 'BV' => 'Antarctica', 'BW' => 'Africa', 'BY' => 'Europe',
|
||||
'BZ' => 'North America', 'CA' => 'North America', 'CC' => 'Asia', 'CD' => 'Africa',
|
||||
'CF' => 'Africa', 'CG' => 'Africa', 'CH' => 'Europe', 'CI' => 'Africa',
|
||||
'CK' => 'Oceania', 'CL' => 'South America', 'CM' => 'Africa', 'CN' => 'Asia',
|
||||
'CO' => 'South America', 'CR' => 'North America', 'CU' => 'North America', 'CV' => 'Africa',
|
||||
'CW' => 'North America', 'CX' => 'Asia', 'CY' => 'Europe', 'CZ' => 'Europe',
|
||||
'DE' => 'Europe', 'DJ' => 'Africa', 'DK' => 'Europe', 'DM' => 'North America',
|
||||
'DO' => 'North America', 'DZ' => 'Africa', 'EC' => 'South America', 'EE' => 'Europe',
|
||||
'EG' => 'Africa', 'EH' => 'Africa', 'ER' => 'Africa', 'ES' => 'Europe',
|
||||
'ET' => 'Africa', 'FI' => 'Europe', 'FJ' => 'Oceania', 'FK' => 'South America',
|
||||
'FM' => 'Oceania', 'FO' => 'Europe', 'FR' => 'Europe', 'GA' => 'Africa',
|
||||
'GB' => 'Europe', 'GD' => 'North America', 'GE' => 'Asia', 'GF' => 'South America',
|
||||
'GG' => 'Europe', 'GH' => 'Africa', 'GI' => 'Europe', 'GL' => 'North America',
|
||||
'GM' => 'Africa', 'GN' => 'Africa', 'GP' => 'North America', 'GQ' => 'Africa',
|
||||
'GR' => 'Europe', 'GS' => 'Antarctica', 'GT' => 'North America', 'GU' => 'Oceania',
|
||||
'GW' => 'Africa', 'GY' => 'South America', 'HK' => 'Asia', 'HM' => 'Antarctica',
|
||||
'HN' => 'North America', 'HR' => 'Europe', 'HT' => 'North America', 'HU' => 'Europe',
|
||||
'ID' => 'Asia', 'IE' => 'Europe', 'IL' => 'Asia', 'IM' => 'Europe',
|
||||
'IN' => 'Asia', 'IO' => 'Asia', 'IQ' => 'Asia', 'IR' => 'Asia',
|
||||
'IS' => 'Europe', 'IT' => 'Europe', 'JE' => 'Europe', 'JM' => 'North America',
|
||||
'JO' => 'Asia', 'JP' => 'Asia', 'KE' => 'Africa', 'KG' => 'Asia',
|
||||
'KH' => 'Asia', 'KI' => 'Oceania', 'KM' => 'Africa', 'KN' => 'North America',
|
||||
'KP' => 'Asia', 'KR' => 'Asia', 'KW' => 'Asia', 'KY' => 'North America',
|
||||
'KZ' => 'Asia', 'LA' => 'Asia', 'LB' => 'Asia', 'LC' => 'North America',
|
||||
'LI' => 'Europe', 'LK' => 'Asia', 'LR' => 'Africa', 'LS' => 'Africa',
|
||||
'LT' => 'Europe', 'LU' => 'Europe', 'LV' => 'Europe', 'LY' => 'Africa',
|
||||
'MA' => 'Africa', 'MC' => 'Europe', 'MD' => 'Europe', 'ME' => 'Europe',
|
||||
'MF' => 'North America', 'MG' => 'Africa', 'MH' => 'Oceania', 'MK' => 'Europe',
|
||||
'ML' => 'Africa', 'MM' => 'Asia', 'MN' => 'Asia', 'MO' => 'Asia',
|
||||
'MP' => 'Oceania', 'MQ' => 'North America', 'MR' => 'Africa', 'MS' => 'North America',
|
||||
'MT' => 'Europe', 'MU' => 'Africa', 'MV' => 'Asia', 'MW' => 'Africa',
|
||||
'MX' => 'North America', 'MY' => 'Asia', 'MZ' => 'Africa', 'NA' => 'Africa',
|
||||
'NC' => 'Oceania', 'NE' => 'Africa', 'NF' => 'Oceania', 'NG' => 'Africa',
|
||||
'NI' => 'North America', 'NL' => 'Europe', 'NO' => 'Europe', 'NP' => 'Asia',
|
||||
'NR' => 'Oceania', 'NU' => 'Oceania', 'NZ' => 'Oceania', 'OM' => 'Asia',
|
||||
'PA' => 'North America', 'PE' => 'South America', 'PF' => 'Oceania', 'PG' => 'Oceania',
|
||||
'PH' => 'Asia', 'PK' => 'Asia', 'PL' => 'Europe', 'PM' => 'North America',
|
||||
'PN' => 'Oceania', 'PR' => 'North America', 'PS' => 'Asia', 'PT' => 'Europe',
|
||||
'PW' => 'Oceania', 'PY' => 'South America', 'QA' => 'Asia', 'RE' => 'Africa',
|
||||
'RO' => 'Europe', 'RS' => 'Europe', 'RU' => 'Europe', 'RW' => 'Africa',
|
||||
'SA' => 'Asia', 'SB' => 'Oceania', 'SC' => 'Africa', 'SD' => 'Africa',
|
||||
'SE' => 'Europe', 'SG' => 'Asia', 'SH' => 'Africa', 'SI' => 'Europe',
|
||||
'SJ' => 'Europe', 'SK' => 'Europe', 'SL' => 'Africa', 'SM' => 'Europe',
|
||||
'SN' => 'Africa', 'SO' => 'Africa', 'SR' => 'South America', 'SS' => 'Africa',
|
||||
'ST' => 'Africa', 'SV' => 'North America', 'SX' => 'North America', 'SY' => 'Asia',
|
||||
'SZ' => 'Africa', 'TC' => 'North America', 'TD' => 'Africa', 'TF' => 'Antarctica',
|
||||
'TG' => 'Africa', 'TH' => 'Asia', 'TJ' => 'Asia', 'TK' => 'Oceania',
|
||||
'TL' => 'Asia', 'TM' => 'Asia', 'TN' => 'Africa', 'TO' => 'Oceania',
|
||||
'TR' => 'Asia', 'TT' => 'North America', 'TV' => 'Oceania', 'TW' => 'Asia',
|
||||
'TZ' => 'Africa', 'UA' => 'Europe', 'UG' => 'Africa', 'UM' => 'Oceania',
|
||||
'US' => 'North America', 'UY' => 'South America', 'UZ' => 'Asia', 'VA' => 'Europe',
|
||||
'VC' => 'North America', 'VE' => 'South America', 'VG' => 'North America', 'VI' => 'North America',
|
||||
'VN' => 'Asia', 'VU' => 'Oceania', 'WF' => 'Oceania', 'WS' => 'Oceania',
|
||||
'YE' => 'Asia', 'YT' => 'Africa', 'ZA' => 'Africa', 'ZM' => 'Africa', 'ZW' => 'Africa',
|
||||
];
|
||||
|
||||
return $continents[$this->value] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate country code
|
||||
*/
|
||||
private function validate(): void
|
||||
{
|
||||
if (! in_array($this->value, self::VALID_CODES, true)) {
|
||||
throw new InvalidArgumentException("Invalid country code: {$this->value}");
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
234
src/Framework/Core/ValueObjects/Duration.php
Normal file
234
src/Framework/Core/ValueObjects/Duration.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class Duration
|
||||
{
|
||||
private function __construct(
|
||||
private int $nanoseconds
|
||||
) {
|
||||
if ($nanoseconds < 0) {
|
||||
throw new InvalidArgumentException('Duration cannot be negative');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Duration directly from nanoseconds for maximum precision
|
||||
*/
|
||||
public static function fromNanoseconds(int $nanoseconds): self
|
||||
{
|
||||
return new self($nanoseconds);
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function fromSeconds(float $seconds): self
|
||||
{
|
||||
if ($seconds < 0) {
|
||||
throw new InvalidArgumentException('Duration cannot be negative');
|
||||
}
|
||||
|
||||
return new self((int) round($seconds * 1_000_000_000));
|
||||
}
|
||||
|
||||
public static function fromUnit(float $value, TimeUnit $unit): self
|
||||
{
|
||||
if ($value < 0) {
|
||||
throw new InvalidArgumentException('Duration cannot be negative');
|
||||
}
|
||||
$seconds = $value * $unit->getMultiplierToSeconds();
|
||||
|
||||
return new self((int) round($seconds * 1_000_000_000));
|
||||
}
|
||||
|
||||
public static function fromMilliseconds(float $milliseconds): self
|
||||
{
|
||||
return self::fromUnit($milliseconds, TimeUnit::MILLISECOND);
|
||||
}
|
||||
|
||||
public static function fromMicroseconds(float $microseconds): self
|
||||
{
|
||||
return self::fromUnit($microseconds, TimeUnit::MICROSECOND);
|
||||
}
|
||||
|
||||
public static function fromMinutes(float $minutes): self
|
||||
{
|
||||
return self::fromUnit($minutes, TimeUnit::MINUTE);
|
||||
}
|
||||
|
||||
public static function fromHours(float $hours): self
|
||||
{
|
||||
return self::fromUnit($hours, TimeUnit::HOUR);
|
||||
}
|
||||
|
||||
public static function fromDays(float $days): self
|
||||
{
|
||||
return self::fromUnit($days, TimeUnit::DAY);
|
||||
}
|
||||
|
||||
public static function between(Timestamp $timestamp, Timestamp $other): self
|
||||
{
|
||||
return $timestamp->diff($other);
|
||||
}
|
||||
|
||||
// Parse from human-readable strings
|
||||
public static function parse(string $value): self
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if (preg_match('/^(\d+(?:\.\d+)?)\s*([a-zA-Z]*)$/', $value, $matches)) {
|
||||
$number = (float) $matches[1];
|
||||
$unitString = $matches[2] ?: 's';
|
||||
|
||||
$unit = TimeUnit::fromString($unitString);
|
||||
|
||||
return self::fromUnit($number, $unit);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException("Invalid duration format: $value");
|
||||
}
|
||||
|
||||
// Conversion Methods
|
||||
public function toSeconds(): float
|
||||
{
|
||||
return $this->nanoseconds / 1_000_000_000;
|
||||
}
|
||||
|
||||
public function toNanoseconds(): int
|
||||
{
|
||||
return $this->nanoseconds;
|
||||
}
|
||||
|
||||
public function toUnit(TimeUnit $unit, int $precision = 2): float
|
||||
{
|
||||
$seconds = $this->nanoseconds / 1_000_000_000;
|
||||
|
||||
return round($seconds / $unit->getMultiplierToSeconds(), $precision);
|
||||
}
|
||||
|
||||
public function toMilliseconds(): float
|
||||
{
|
||||
return $this->toUnit(TimeUnit::MILLISECOND, 0);
|
||||
}
|
||||
|
||||
public function toMicroseconds(): float
|
||||
{
|
||||
return $this->toUnit(TimeUnit::MICROSECOND, 0);
|
||||
}
|
||||
|
||||
public function toMinutes(): float
|
||||
{
|
||||
return $this->toUnit(TimeUnit::MINUTE);
|
||||
}
|
||||
|
||||
public function toHours(): float
|
||||
{
|
||||
return $this->toUnit(TimeUnit::HOUR);
|
||||
}
|
||||
|
||||
// Human-readable format
|
||||
public function toHumanReadable(): string
|
||||
{
|
||||
if ($this->nanoseconds === 0) {
|
||||
return '0s';
|
||||
}
|
||||
|
||||
$seconds = $this->nanoseconds / 1_000_000_000;
|
||||
$unit = TimeUnit::bestUnitFor($seconds);
|
||||
$value = $this->toUnit($unit);
|
||||
|
||||
return $value . ' ' . $unit->value;
|
||||
}
|
||||
|
||||
// Arithmetic Operations
|
||||
public function add(Duration $other): self
|
||||
{
|
||||
return new self($this->nanoseconds + $other->nanoseconds);
|
||||
}
|
||||
|
||||
public function subtract(Duration $other): self
|
||||
{
|
||||
$result = $this->nanoseconds - $other->nanoseconds;
|
||||
if ($result < 0) {
|
||||
throw new InvalidArgumentException('Subtraction would result in negative duration');
|
||||
}
|
||||
|
||||
return new self($result);
|
||||
}
|
||||
|
||||
public function multiply(float $factor): self
|
||||
{
|
||||
if ($factor < 0) {
|
||||
throw new InvalidArgumentException('Factor cannot be negative');
|
||||
}
|
||||
|
||||
return new self((int) round($this->nanoseconds * $factor));
|
||||
}
|
||||
|
||||
// Comparison Methods
|
||||
public function equals(Duration $other): bool
|
||||
{
|
||||
return $this->nanoseconds === $other->nanoseconds;
|
||||
}
|
||||
|
||||
public function greaterThan(Duration $other): bool
|
||||
{
|
||||
return $this->nanoseconds > $other->nanoseconds;
|
||||
}
|
||||
|
||||
public function lessThan(Duration $other): bool
|
||||
{
|
||||
return $this->nanoseconds < $other->nanoseconds;
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
public function isZero(): bool
|
||||
{
|
||||
return $this->nanoseconds === 0;
|
||||
}
|
||||
|
||||
public function isNotZero(): bool
|
||||
{
|
||||
return $this->nanoseconds > 0;
|
||||
}
|
||||
|
||||
// Framework Integration
|
||||
public function toCacheSeconds(): int
|
||||
{
|
||||
return (int) ceil($this->nanoseconds / 1_000_000_000);
|
||||
}
|
||||
|
||||
public function toTimeoutSeconds(): int
|
||||
{
|
||||
return (int) ceil($this->nanoseconds / 1_000_000_000);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toHumanReadable();
|
||||
}
|
||||
|
||||
// Common constants
|
||||
public static function zero(): self
|
||||
{
|
||||
return new self(0);
|
||||
}
|
||||
|
||||
public static function oneSecond(): self
|
||||
{
|
||||
return new self(1_000_000_000);
|
||||
}
|
||||
|
||||
public static function oneMinute(): self
|
||||
{
|
||||
return self::fromMinutes(1);
|
||||
}
|
||||
|
||||
public static function oneHour(): self
|
||||
{
|
||||
return self::fromHours(1);
|
||||
}
|
||||
}
|
||||
189
src/Framework/Core/ValueObjects/EmailAddress.php
Normal file
189
src/Framework/Core/ValueObjects/EmailAddress.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class EmailAddress
|
||||
{
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function from(string $email): self
|
||||
{
|
||||
return new self($email);
|
||||
}
|
||||
|
||||
public static function parse(string $input): self
|
||||
{
|
||||
$email = trim($input);
|
||||
|
||||
// Remove surrounding quotes if present
|
||||
if (str_starts_with($email, '"') && str_ends_with($email, '"')) {
|
||||
$email = substr($email, 1, -1);
|
||||
}
|
||||
|
||||
// Extract email from "Name <email@domain.com>" format
|
||||
if (preg_match('/<([^>]+)>/', $email, $matches)) {
|
||||
$email = $matches[1];
|
||||
}
|
||||
|
||||
return new self($email);
|
||||
}
|
||||
|
||||
// Validation
|
||||
public static function isValid(string $email): bool
|
||||
{
|
||||
if (empty($email) || strlen($email) > 320) { // RFC 5321 limit
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional domain validation
|
||||
$domain = substr(strrchr($email, '@'), 1);
|
||||
|
||||
return strlen($domain) >= 4 && str_contains($domain, '.');
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getLocalPart(): string
|
||||
{
|
||||
return substr($this->value, 0, strpos($this->value, '@'));
|
||||
}
|
||||
|
||||
public function getDomain(): string
|
||||
{
|
||||
return substr($this->value, strpos($this->value, '@') + 1);
|
||||
}
|
||||
|
||||
// Domain checks
|
||||
public function isDisposable(): bool
|
||||
{
|
||||
$disposableDomains = [
|
||||
'10minutemail.com', 'tempmail.org', 'guerrillamail.com',
|
||||
'mailinator.com', 'throwaway.email', 'temp-mail.org',
|
||||
];
|
||||
|
||||
return in_array(strtolower($this->getDomain()), $disposableDomains, true);
|
||||
}
|
||||
|
||||
public function isCommonProvider(): bool
|
||||
{
|
||||
$commonProviders = [
|
||||
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
|
||||
'web.de', 'gmx.de', 't-online.de', 'freenet.de',
|
||||
];
|
||||
|
||||
return in_array(strtolower($this->getDomain()), $commonProviders, true);
|
||||
}
|
||||
|
||||
public function isCorporate(): bool
|
||||
{
|
||||
return ! $this->isCommonProvider() && ! $this->isDisposable();
|
||||
}
|
||||
|
||||
// Formatting
|
||||
public function normalize(): self
|
||||
{
|
||||
$normalized = strtolower(trim($this->value));
|
||||
|
||||
// Gmail: remove dots from local part and ignore everything after +
|
||||
if (str_ends_with($normalized, '@gmail.com')) {
|
||||
$local = $this->getLocalPart();
|
||||
$local = str_replace('.', '', $local);
|
||||
if (str_contains($local, '+')) {
|
||||
$local = substr($local, 0, strpos($local, '+'));
|
||||
}
|
||||
$normalized = $local . '@gmail.com';
|
||||
}
|
||||
|
||||
return new self($normalized);
|
||||
}
|
||||
|
||||
public function obfuscate(): string
|
||||
{
|
||||
$local = $this->getLocalPart();
|
||||
$domain = $this->getDomain();
|
||||
|
||||
if (strlen($local) <= 2) {
|
||||
$obfuscatedLocal = str_repeat('*', strlen($local));
|
||||
} else {
|
||||
$obfuscatedLocal = $local[0] . str_repeat('*', strlen($local) - 2) . $local[-1];
|
||||
}
|
||||
|
||||
$domainParts = explode('.', $domain);
|
||||
if (count($domainParts) >= 2) {
|
||||
$tld = array_pop($domainParts);
|
||||
$mainDomain = array_pop($domainParts);
|
||||
$obfuscatedDomain = substr($mainDomain, 0, 1) . str_repeat('*', strlen($mainDomain) - 1) . '.' . $tld;
|
||||
} else {
|
||||
$obfuscatedDomain = str_repeat('*', strlen($domain));
|
||||
}
|
||||
|
||||
return $obfuscatedLocal . '@' . $obfuscatedDomain;
|
||||
}
|
||||
|
||||
// Comparison
|
||||
public function equals(EmailAddress $other): bool
|
||||
{
|
||||
return $this->normalize()->value === $other->normalize()->value;
|
||||
}
|
||||
|
||||
public function isSameDomain(EmailAddress $other): bool
|
||||
{
|
||||
return strtolower($this->getDomain()) === strtolower($other->getDomain());
|
||||
}
|
||||
|
||||
// String representation
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
// Validation
|
||||
private function validate(): void
|
||||
{
|
||||
if (empty($this->value)) {
|
||||
throw new InvalidArgumentException('Email address cannot be empty');
|
||||
}
|
||||
|
||||
if (strlen($this->value) > 320) { // RFC 5321 limit
|
||||
throw new InvalidArgumentException('Email address too long (max 320 characters)');
|
||||
}
|
||||
|
||||
if (! filter_var($this->value, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new InvalidArgumentException("Invalid email address: {$this->value}");
|
||||
}
|
||||
|
||||
// Additional domain validation
|
||||
$domain = $this->getDomain();
|
||||
if (strlen($domain) < 4 || ! str_contains($domain, '.')) {
|
||||
throw new InvalidArgumentException("Invalid domain in email address: {$domain}");
|
||||
}
|
||||
|
||||
// Check for valid local part length (RFC 5321)
|
||||
$localPart = $this->getLocalPart();
|
||||
if (strlen($localPart) > 64) {
|
||||
throw new InvalidArgumentException('Email local part too long (max 64 characters)');
|
||||
}
|
||||
}
|
||||
}
|
||||
229
src/Framework/Core/ValueObjects/GrowthRate.php
Normal file
229
src/Framework/Core/ValueObjects/GrowthRate.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents growth rates that can exceed 100% (e.g., memory growth, performance metrics)
|
||||
*/
|
||||
final readonly class GrowthRate
|
||||
{
|
||||
public function __construct(
|
||||
private float $value
|
||||
) {
|
||||
if ($value < 0.0) {
|
||||
throw new InvalidArgumentException('Growth rate cannot be negative');
|
||||
}
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function from(float $percentage): self
|
||||
{
|
||||
return new self($percentage);
|
||||
}
|
||||
|
||||
public static function fromDecimal(float $decimal): self
|
||||
{
|
||||
return new self($decimal * 100);
|
||||
}
|
||||
|
||||
public static function fromRatio(float $current, float $original): self
|
||||
{
|
||||
if ($original <= 0) {
|
||||
throw new InvalidArgumentException('Original value must be positive');
|
||||
}
|
||||
|
||||
return new self(($current / $original - 1) * 100);
|
||||
}
|
||||
|
||||
public static function fromMemoryValues(int $currentBytes, int $originalBytes): self
|
||||
{
|
||||
if ($originalBytes <= 0) {
|
||||
throw new InvalidArgumentException('Original memory value must be positive');
|
||||
}
|
||||
|
||||
return new self(($currentBytes / $originalBytes - 1) * 100);
|
||||
}
|
||||
|
||||
// Parse from string like "150%" or "150"
|
||||
public static function parse(string $value): self
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if (str_ends_with($value, '%')) {
|
||||
$value = substr($value, 0, -1);
|
||||
}
|
||||
|
||||
if (! is_numeric($value)) {
|
||||
throw new InvalidArgumentException("Invalid growth rate format: $value");
|
||||
}
|
||||
|
||||
return new self((float) $value);
|
||||
}
|
||||
|
||||
// Conversion Methods
|
||||
public function getValue(): float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toDecimal(): float
|
||||
{
|
||||
return $this->value / 100;
|
||||
}
|
||||
|
||||
public function toMultiplier(): float
|
||||
{
|
||||
return 1 + ($this->value / 100);
|
||||
}
|
||||
|
||||
public function format(int $precision = 1): string
|
||||
{
|
||||
return number_format($this->value, $precision) . '%';
|
||||
}
|
||||
|
||||
public function formatWithSign(int $precision = 1): string
|
||||
{
|
||||
$sign = $this->value >= 0 ? '+' : '';
|
||||
|
||||
return $sign . number_format($this->value, $precision) . '%';
|
||||
}
|
||||
|
||||
// Comparison Methods
|
||||
public function equals(GrowthRate $other): bool
|
||||
{
|
||||
return abs($this->value - $other->value) < 1e-9;
|
||||
}
|
||||
|
||||
public function greaterThan(GrowthRate $other): bool
|
||||
{
|
||||
return $this->value > $other->value;
|
||||
}
|
||||
|
||||
public function lessThan(GrowthRate $other): bool
|
||||
{
|
||||
return $this->value < $other->value;
|
||||
}
|
||||
|
||||
// Growth Analysis Methods
|
||||
public function isPositiveGrowth(): bool
|
||||
{
|
||||
return $this->value > 0;
|
||||
}
|
||||
|
||||
public function isNegativeGrowth(): bool
|
||||
{
|
||||
return $this->value < 0;
|
||||
}
|
||||
|
||||
public function isNoGrowth(): bool
|
||||
{
|
||||
return abs($this->value) < 1e-9;
|
||||
}
|
||||
|
||||
// Memory-specific analysis
|
||||
public function isModerateMemoryGrowth(): bool
|
||||
{
|
||||
return $this->value >= 10.0 && $this->value < 50.0;
|
||||
}
|
||||
|
||||
public function isHighMemoryGrowth(): bool
|
||||
{
|
||||
return $this->value >= 50.0 && $this->value < 100.0;
|
||||
}
|
||||
|
||||
public function isCriticalMemoryGrowth(): bool
|
||||
{
|
||||
return $this->value >= 100.0;
|
||||
}
|
||||
|
||||
public function isExtremeMemoryGrowth(): bool
|
||||
{
|
||||
return $this->value >= 200.0;
|
||||
}
|
||||
|
||||
// Performance-specific analysis
|
||||
public function isSignificantGrowth(float $threshold = 25.0): bool
|
||||
{
|
||||
return $this->value >= $threshold;
|
||||
}
|
||||
|
||||
public function getGrowthLevel(): string
|
||||
{
|
||||
return match (true) {
|
||||
$this->value < 0 => 'decrease',
|
||||
$this->value < 1 => 'none',
|
||||
$this->value < 10 => 'minimal',
|
||||
$this->value < 25 => 'low',
|
||||
$this->value < 50 => 'moderate',
|
||||
$this->value < 100 => 'high',
|
||||
$this->value < 200 => 'critical',
|
||||
default => 'extreme'
|
||||
};
|
||||
}
|
||||
|
||||
// Arithmetic Operations
|
||||
public function add(GrowthRate $other): self
|
||||
{
|
||||
return new self($this->value + $other->value);
|
||||
}
|
||||
|
||||
public function subtract(GrowthRate $other): self
|
||||
{
|
||||
return new self($this->value - $other->value);
|
||||
}
|
||||
|
||||
public function multiply(float $factor): self
|
||||
{
|
||||
return new self($this->value * $factor);
|
||||
}
|
||||
|
||||
public function divide(float $divisor): self
|
||||
{
|
||||
if ($divisor == 0) {
|
||||
throw new InvalidArgumentException('Cannot divide by zero');
|
||||
}
|
||||
|
||||
return new self($this->value / $divisor);
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
public function toPercentage(): ?Percentage
|
||||
{
|
||||
// Convert to Percentage only if within valid range
|
||||
if ($this->value >= 0.0 && $this->value <= 100.0) {
|
||||
return Percentage::from($this->value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function clampToPercentage(): Percentage
|
||||
{
|
||||
return Percentage::from(min(100.0, max(0.0, $this->value)));
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->format();
|
||||
}
|
||||
|
||||
// Common constants
|
||||
public static function zero(): self
|
||||
{
|
||||
return new self(0.0);
|
||||
}
|
||||
|
||||
public static function doubleGrowth(): self
|
||||
{
|
||||
return new self(100.0);
|
||||
}
|
||||
|
||||
public static function tripleGrowth(): self
|
||||
{
|
||||
return new self(200.0);
|
||||
}
|
||||
}
|
||||
184
src/Framework/Core/ValueObjects/Hash.php
Normal file
184
src/Framework/Core/ValueObjects/Hash.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class Hash
|
||||
{
|
||||
public function __construct(
|
||||
private string $value,
|
||||
private HashAlgorithm $algorithm
|
||||
) {
|
||||
$this->validateHash();
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function create(string $data, HashAlgorithm $algorithm = HashAlgorithm::SHA256): self
|
||||
{
|
||||
if (! $algorithm->isAvailable()) {
|
||||
throw new InvalidArgumentException("Hash algorithm {$algorithm->value} is not available");
|
||||
}
|
||||
|
||||
$hash = hash($algorithm->value, $data);
|
||||
|
||||
return new self($hash, $algorithm);
|
||||
}
|
||||
|
||||
public static function fromString(string $hash, HashAlgorithm $algorithm): self
|
||||
{
|
||||
return new self($hash, $algorithm);
|
||||
}
|
||||
|
||||
public static function fromFile(string $filePath, HashAlgorithm $algorithm = HashAlgorithm::SHA256): self
|
||||
{
|
||||
if (! file_exists($filePath)) {
|
||||
throw new InvalidArgumentException("File does not exist: $filePath");
|
||||
}
|
||||
|
||||
if (! $algorithm->isAvailable()) {
|
||||
throw new InvalidArgumentException("Hash algorithm {$algorithm->value} is not available");
|
||||
}
|
||||
|
||||
$hash = hash_file($algorithm->value, $filePath);
|
||||
if ($hash === false) {
|
||||
throw new InvalidArgumentException("Failed to hash file: $filePath");
|
||||
}
|
||||
|
||||
return new self($hash, $algorithm);
|
||||
}
|
||||
|
||||
// Convenience methods for common algorithms
|
||||
public static function md5(string $data): self
|
||||
{
|
||||
return self::create($data, HashAlgorithm::MD5);
|
||||
}
|
||||
|
||||
public static function sha1(string $data): self
|
||||
{
|
||||
return self::create($data, HashAlgorithm::SHA1);
|
||||
}
|
||||
|
||||
public static function sha256(string $data): self
|
||||
{
|
||||
return self::create($data, HashAlgorithm::SHA256);
|
||||
}
|
||||
|
||||
public static function sha512(string $data): self
|
||||
{
|
||||
return self::create($data, HashAlgorithm::SHA512);
|
||||
}
|
||||
|
||||
// Parse from prefixed format like "sha256:abc123..."
|
||||
public static function parse(string $value): self
|
||||
{
|
||||
if (str_contains($value, ':')) {
|
||||
[$algoString, $hash] = explode(':', $value, 2);
|
||||
|
||||
$algorithm = HashAlgorithm::tryFrom($algoString);
|
||||
if ($algorithm === null) {
|
||||
throw new InvalidArgumentException("Unknown hash algorithm: $algoString");
|
||||
}
|
||||
|
||||
return new self($hash, $algorithm);
|
||||
}
|
||||
|
||||
// Try to detect algorithm by length
|
||||
$algorithm = match (strlen($value)) {
|
||||
32 => HashAlgorithm::MD5,
|
||||
40 => HashAlgorithm::SHA1,
|
||||
64 => HashAlgorithm::SHA256,
|
||||
128 => HashAlgorithm::SHA512,
|
||||
default => throw new InvalidArgumentException("Cannot detect hash algorithm from length"),
|
||||
};
|
||||
|
||||
return new self($value, $algorithm);
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getAlgorithm(): HashAlgorithm
|
||||
{
|
||||
return $this->algorithm;
|
||||
}
|
||||
|
||||
// Verification
|
||||
public function verify(string $data): bool
|
||||
{
|
||||
$expectedHash = hash($this->algorithm->value, $data);
|
||||
|
||||
return hash_equals($this->value, $expectedHash);
|
||||
}
|
||||
|
||||
public function verifyFile(string $filePath): bool
|
||||
{
|
||||
if (! file_exists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fileHash = hash_file($this->algorithm->value, $filePath);
|
||||
if ($fileHash === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($this->value, $fileHash);
|
||||
}
|
||||
|
||||
// Comparison
|
||||
public function equals(Hash $other): bool
|
||||
{
|
||||
return $this->algorithm === $other->algorithm
|
||||
&& hash_equals($this->value, $other->value);
|
||||
}
|
||||
|
||||
// Formatting
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toPrefixedString(): string
|
||||
{
|
||||
return $this->algorithm->value . ':' . $this->value;
|
||||
}
|
||||
|
||||
public function toShort(int $length = 8): string
|
||||
{
|
||||
return substr($this->value, 0, $length);
|
||||
}
|
||||
|
||||
// Security checks
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return $this->algorithm->isSecure();
|
||||
}
|
||||
|
||||
// String representation
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
// Validation
|
||||
private function validateHash(): void
|
||||
{
|
||||
$expectedLength = $this->algorithm->getLength();
|
||||
$actualLength = strlen($this->value);
|
||||
|
||||
if ($actualLength !== $expectedLength) {
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid hash length for {$this->algorithm->value}: expected $expectedLength, got $actualLength"
|
||||
);
|
||||
}
|
||||
|
||||
if (! ctype_xdigit($this->value)) {
|
||||
throw new InvalidArgumentException('Hash must contain only hexadecimal characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/Framework/Core/ValueObjects/HashAlgorithm.php
Normal file
50
src/Framework/Core/ValueObjects/HashAlgorithm.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
enum HashAlgorithm: string
|
||||
{
|
||||
case MD5 = 'md5';
|
||||
case SHA1 = 'sha1';
|
||||
case SHA256 = 'sha256';
|
||||
case SHA512 = 'sha512';
|
||||
case SHA3_256 = 'sha3-256';
|
||||
case SHA3_512 = 'sha3-512';
|
||||
case XXHASH64 = 'xxh64';
|
||||
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::MD5, self::SHA1 => false,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
public function getLength(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::MD5 => 32,
|
||||
self::SHA1 => 40,
|
||||
self::SHA256, self::SHA3_256 => 64,
|
||||
self::SHA512, self::SHA3_512 => 128,
|
||||
self::XXHASH64 => 16,
|
||||
};
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return in_array($this->value, hash_algos(), true);
|
||||
}
|
||||
|
||||
public static function secure(): self
|
||||
{
|
||||
return self::SHA256;
|
||||
}
|
||||
|
||||
public static function fast(): self
|
||||
{
|
||||
return extension_loaded('xxhash') ? self::XXHASH64 : self::SHA256;
|
||||
}
|
||||
}
|
||||
77
src/Framework/Core/ValueObjects/MethodName.php
Normal file
77
src/Framework/Core/ValueObjects/MethodName.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value object for PHP method names with validation
|
||||
*/
|
||||
final readonly class MethodName
|
||||
{
|
||||
private function __construct(
|
||||
public string $name
|
||||
) {
|
||||
if (empty(trim($name))) {
|
||||
throw new InvalidArgumentException('Method name cannot be empty');
|
||||
}
|
||||
|
||||
if (! $this->isValidMethodName($name)) {
|
||||
throw new InvalidArgumentException("Invalid method name: {$name}");
|
||||
}
|
||||
}
|
||||
|
||||
public static function create(string $name): self
|
||||
{
|
||||
return new self($name);
|
||||
}
|
||||
|
||||
public static function invoke(): self
|
||||
{
|
||||
return new self('__invoke');
|
||||
}
|
||||
|
||||
public static function construct(): self
|
||||
{
|
||||
return new self('__construct');
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isMagicMethod(): bool
|
||||
{
|
||||
return str_starts_with($this->name, '__');
|
||||
}
|
||||
|
||||
public function isConstructor(): bool
|
||||
{
|
||||
return $this->name === '__construct';
|
||||
}
|
||||
|
||||
public function isInvokable(): bool
|
||||
{
|
||||
return $this->name === '__invoke';
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->name === $other->name;
|
||||
}
|
||||
|
||||
private function isValidMethodName(string $name): bool
|
||||
{
|
||||
// PHP method names must follow variable naming rules
|
||||
// Can start with letter or underscore, followed by letters, numbers, or underscores
|
||||
return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name) === 1;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
134
src/Framework/Core/ValueObjects/Percentage.php
Normal file
134
src/Framework/Core/ValueObjects/Percentage.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class Percentage
|
||||
{
|
||||
public function __construct(
|
||||
private float $value
|
||||
) {
|
||||
if ($value < 0.0 || $value > 100.0) {
|
||||
throw new InvalidArgumentException('Percentage must be between 0 and 100');
|
||||
}
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function from(float $percentage): self
|
||||
{
|
||||
return new self($percentage);
|
||||
}
|
||||
|
||||
public static function fromDecimal(float $decimal): self
|
||||
{
|
||||
return new self($decimal * 100);
|
||||
}
|
||||
|
||||
public static function fromRatio(int $part, int $total): self
|
||||
{
|
||||
if ($total === 0) {
|
||||
throw new InvalidArgumentException('Total cannot be zero');
|
||||
}
|
||||
|
||||
return new self(($part / $total) * 100);
|
||||
}
|
||||
|
||||
// Parse from string like "75%" or "75"
|
||||
public static function parse(string $value): self
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if (str_ends_with($value, '%')) {
|
||||
$value = substr($value, 0, -1);
|
||||
}
|
||||
|
||||
if (! is_numeric($value)) {
|
||||
throw new InvalidArgumentException("Invalid percentage format: $value");
|
||||
}
|
||||
|
||||
return new self((float) $value);
|
||||
}
|
||||
|
||||
// Conversion Methods
|
||||
public function getValue(): float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toDecimal(): float
|
||||
{
|
||||
return $this->value / 100;
|
||||
}
|
||||
|
||||
public function format(int $precision = 1): string
|
||||
{
|
||||
return number_format($this->value, $precision) . '%';
|
||||
}
|
||||
|
||||
// Comparison Methods
|
||||
public function equals(Percentage $other): bool
|
||||
{
|
||||
return abs($this->value - $other->value) < 1e-9;
|
||||
}
|
||||
|
||||
public function greaterThan(Percentage $other): bool
|
||||
{
|
||||
return $this->value > $other->value;
|
||||
}
|
||||
|
||||
public function lessThan(Percentage $other): bool
|
||||
{
|
||||
return $this->value < $other->value;
|
||||
}
|
||||
|
||||
// Threshold checks (useful for monitoring)
|
||||
public function isAbove(Percentage $threshold): bool
|
||||
{
|
||||
return $this->greaterThan($threshold);
|
||||
}
|
||||
|
||||
public function isBelow(Percentage $threshold): bool
|
||||
{
|
||||
return $this->lessThan($threshold);
|
||||
}
|
||||
|
||||
public function isCritical(float $threshold = 90.0): bool
|
||||
{
|
||||
return $this->value >= $threshold;
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->value === 0.0;
|
||||
}
|
||||
|
||||
public function isFull(): bool
|
||||
{
|
||||
return $this->value === 100.0;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->format();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->format();
|
||||
}
|
||||
|
||||
// Common constants
|
||||
public static function zero(): self
|
||||
{
|
||||
return new self(0.0);
|
||||
}
|
||||
|
||||
public static function full(): self
|
||||
{
|
||||
return new self(100.0);
|
||||
}
|
||||
}
|
||||
99
src/Framework/Core/ValueObjects/Port.php
Normal file
99
src/Framework/Core/ValueObjects/Port.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
enum Port: int
|
||||
{
|
||||
case HTTP = 80;
|
||||
case HTTPS = 443;
|
||||
case FTP = 21;
|
||||
case SSH = 22;
|
||||
case SMTP = 25;
|
||||
case DNS = 53;
|
||||
case DHCP = 67;
|
||||
case POP3 = 110;
|
||||
case IMAP = 143;
|
||||
case SNMP = 161;
|
||||
case LDAP = 389;
|
||||
case SMTPS = 465;
|
||||
case IMAPS = 993;
|
||||
case POP3S = 995;
|
||||
case MYSQL = 3306;
|
||||
case POSTGRESQL = 5432;
|
||||
case REDIS = 6379;
|
||||
case MONGODB = 27017;
|
||||
|
||||
public static function forScheme(string $scheme): ?self
|
||||
{
|
||||
return match (strtolower($scheme)) {
|
||||
'http' => self::HTTP,
|
||||
'https' => self::HTTPS,
|
||||
'ftp' => self::FTP,
|
||||
'ssh' => self::SSH,
|
||||
'smtp' => self::SMTP,
|
||||
'pop3' => self::POP3,
|
||||
'imap' => self::IMAP,
|
||||
'ldap' => self::LDAP,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function getScheme(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::HTTP => 'http',
|
||||
self::HTTPS => 'https',
|
||||
self::FTP => 'ftp',
|
||||
self::SSH => 'ssh',
|
||||
self::SMTP => 'smtp',
|
||||
self::POP3 => 'pop3',
|
||||
self::IMAP => 'imap',
|
||||
self::LDAP => 'ldap',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::HTTP => 'HTTP',
|
||||
self::HTTPS => 'HTTPS',
|
||||
self::FTP => 'FTP',
|
||||
self::SSH => 'SSH',
|
||||
self::SMTP => 'SMTP',
|
||||
self::DNS => 'DNS',
|
||||
self::DHCP => 'DHCP',
|
||||
self::POP3 => 'POP3',
|
||||
self::IMAP => 'IMAP',
|
||||
self::SNMP => 'SNMP',
|
||||
self::LDAP => 'LDAP',
|
||||
self::SMTPS => 'SMTPS',
|
||||
self::IMAPS => 'IMAPS',
|
||||
self::POP3S => 'POP3S',
|
||||
self::MYSQL => 'MySQL',
|
||||
self::POSTGRESQL => 'PostgreSQL',
|
||||
self::REDIS => 'Redis',
|
||||
self::MONGODB => 'MongoDB',
|
||||
};
|
||||
}
|
||||
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::HTTPS, self::SSH, self::SMTPS, self::IMAPS, self::POP3S => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function isWellKnown(): bool
|
||||
{
|
||||
return $this->value < 1024;
|
||||
}
|
||||
|
||||
public static function isValidPort(int $port): bool
|
||||
{
|
||||
return $port >= 1 && $port <= 65535;
|
||||
}
|
||||
}
|
||||
334
src/Framework/Core/ValueObjects/README.md
Normal file
334
src/Framework/Core/ValueObjects/README.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Byte Value Object
|
||||
|
||||
The `Byte` value object provides a type-safe way to work with byte sizes in your application. It offers convenient conversions, arithmetic operations, and human-readable formatting for file sizes, memory usage, and other byte-based measurements.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating Byte Instances
|
||||
|
||||
```php
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Core\ValueObjects\ByteUnit;
|
||||
|
||||
// From raw bytes
|
||||
$size = Byte::fromBytes(1048576); // 1 MB
|
||||
|
||||
// From specific units
|
||||
$fileSize = Byte::fromMegabytes(5.5);
|
||||
$memoryLimit = Byte::fromGigabytes(2);
|
||||
$smallFile = Byte::fromKilobytes(256);
|
||||
|
||||
// From human-readable strings
|
||||
$uploadLimit = Byte::parse('10MB');
|
||||
$cacheSize = Byte::parse('500 KB');
|
||||
$logSize = Byte::parse('1.5GB');
|
||||
|
||||
// Using enum for flexibility
|
||||
$customSize = Byte::fromUnit(100, ByteUnit::MEGABYTE);
|
||||
```
|
||||
|
||||
### Converting Between Units
|
||||
|
||||
```php
|
||||
$size = Byte::fromMegabytes(5.5);
|
||||
|
||||
echo $size->toBytes(); // 5767168
|
||||
echo $size->toKilobytes(); // 5632.0
|
||||
echo $size->toMegabytes(); // 5.5
|
||||
echo $size->toGigabytes(); // 0.01
|
||||
|
||||
// Custom precision
|
||||
echo $size->toGigabytes(4); // 0.0054
|
||||
|
||||
// Using enum for dynamic conversion
|
||||
$unit = ByteUnit::KILOBYTE;
|
||||
echo $size->toUnit($unit); // 5632.0
|
||||
```
|
||||
|
||||
### Human-Readable Formatting
|
||||
|
||||
```php
|
||||
$size = Byte::fromBytes(1536000);
|
||||
|
||||
echo $size->toHumanReadable(); // "1.46 MB"
|
||||
echo $size->toHumanReadable(0); // "1 MB"
|
||||
echo (string) $size; // "1.46 MB" (same as toHumanReadable())
|
||||
|
||||
// Get the best unit for display
|
||||
$unit = $size->getBestUnit(); // ByteUnit::MEGABYTE
|
||||
echo $unit->value; // "MB"
|
||||
echo $unit->getName(); // "Megabyte"
|
||||
```
|
||||
|
||||
## Arithmetic Operations
|
||||
|
||||
```php
|
||||
$file1 = Byte::fromMegabytes(5);
|
||||
$file2 = Byte::fromMegabytes(3);
|
||||
|
||||
// Addition
|
||||
$total = $file1->add($file2); // 8 MB
|
||||
|
||||
// Subtraction
|
||||
$difference = $file1->subtract($file2); // 2 MB
|
||||
|
||||
// Multiplication
|
||||
$doubled = $file1->multiply(2); // 10 MB
|
||||
|
||||
// Division
|
||||
$half = $file1->divide(2); // 2.5 MB
|
||||
|
||||
// Percentage calculation
|
||||
$percentage = $file1->percentOf($total); // 62.5
|
||||
```
|
||||
|
||||
## Comparisons
|
||||
|
||||
```php
|
||||
$file1 = Byte::fromMegabytes(5);
|
||||
$file2 = Byte::fromMegabytes(3);
|
||||
$limit = Byte::fromMegabytes(10);
|
||||
|
||||
// Equality
|
||||
if ($file1->equals($file2)) {
|
||||
echo "Files are the same size";
|
||||
}
|
||||
|
||||
// Size comparisons
|
||||
if ($file1->greaterThan($file2)) {
|
||||
echo "File 1 is larger";
|
||||
}
|
||||
|
||||
if ($file1->lessThan($limit)) {
|
||||
echo "File is under the limit";
|
||||
}
|
||||
|
||||
// Utility checks
|
||||
if ($file1->isEmpty()) {
|
||||
echo "File is empty";
|
||||
}
|
||||
```
|
||||
|
||||
## Framework Integration Examples
|
||||
|
||||
### File Upload Validation
|
||||
|
||||
```php
|
||||
class FileUploadMiddleware
|
||||
{
|
||||
public function validate(UploadedFile $file): void
|
||||
{
|
||||
$maxSize = Byte::parse($_ENV['MAX_UPLOAD_SIZE'] ?? '10MB');
|
||||
$fileSize = Byte::fromBytes($file->getSize());
|
||||
|
||||
if ($fileSize->greaterThan($maxSize)) {
|
||||
throw new ValidationException(
|
||||
"File size {$fileSize} exceeds limit of {$maxSize}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Usage Monitoring
|
||||
|
||||
```php
|
||||
class PerformanceMonitor
|
||||
{
|
||||
public function checkMemoryUsage(): array
|
||||
{
|
||||
$current = Byte::fromBytes(memory_get_usage(true));
|
||||
$peak = Byte::fromBytes(memory_get_peak_usage(true));
|
||||
$limit = Byte::parse(ini_get('memory_limit'));
|
||||
|
||||
return [
|
||||
'current' => $current->toHumanReadable(),
|
||||
'peak' => $peak->toHumanReadable(),
|
||||
'limit' => $limit->toHumanReadable(),
|
||||
'usage_percentage' => $current->percentOf($limit),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database Entity
|
||||
|
||||
```php
|
||||
use App\Framework\Database\Attributes\Column;
|
||||
use App\Framework\Database\Attributes\Entity;
|
||||
|
||||
#[Entity(tableName: 'files')]
|
||||
final readonly class File
|
||||
{
|
||||
public function __construct(
|
||||
#[Column(name: 'id', primary: true)]
|
||||
public int $id,
|
||||
#[Column(name: 'filename')]
|
||||
public string $filename,
|
||||
#[Column(name: 'file_size')]
|
||||
public int $fileSizeBytes,
|
||||
) {}
|
||||
|
||||
public function getFileSize(): Byte
|
||||
{
|
||||
return Byte::fromBytes($this->fileSizeBytes);
|
||||
}
|
||||
|
||||
public function isLargeFile(): bool
|
||||
{
|
||||
return $this->getFileSize()->greaterThan(Byte::fromMegabytes(50));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Size Management
|
||||
|
||||
```php
|
||||
class CacheManager
|
||||
{
|
||||
public function cleanupIfNeeded(): void
|
||||
{
|
||||
$cacheSize = $this->getCurrentCacheSize();
|
||||
$maxSize = Byte::parse($_ENV['CACHE_MAX_SIZE'] ?? '1GB');
|
||||
|
||||
if ($cacheSize->greaterThan($maxSize)) {
|
||||
$this->clearOldEntries();
|
||||
|
||||
$newSize = $this->getCurrentCacheSize();
|
||||
$freed = $cacheSize->subtract($newSize);
|
||||
|
||||
$this->logger->info("Cache cleanup freed {$freed}");
|
||||
}
|
||||
}
|
||||
|
||||
private function getCurrentCacheSize(): Byte
|
||||
{
|
||||
$totalBytes = 0;
|
||||
foreach ($this->getCacheFiles() as $file) {
|
||||
$totalBytes += filesize($file);
|
||||
}
|
||||
return Byte::fromBytes($totalBytes);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ByteUnit Enum
|
||||
|
||||
The `ByteUnit` enum provides type-safe unit definitions:
|
||||
|
||||
```php
|
||||
// Available units
|
||||
ByteUnit::BYTE // 'B'
|
||||
ByteUnit::KILOBYTE // 'KB'
|
||||
ByteUnit::MEGABYTE // 'MB'
|
||||
ByteUnit::GIGABYTE // 'GB'
|
||||
ByteUnit::TERABYTE // 'TB'
|
||||
|
||||
// Get multiplier
|
||||
$multiplier = ByteUnit::MEGABYTE->getMultiplier(); // 1048576
|
||||
|
||||
// Get human name
|
||||
$name = ByteUnit::GIGABYTE->getName(); // "Gigabyte"
|
||||
|
||||
// Find best unit for a size
|
||||
$unit = ByteUnit::bestUnitFor(1048576); // ByteUnit::MEGABYTE
|
||||
|
||||
// Parse from string
|
||||
$unit = ByteUnit::fromString('MB'); // ByteUnit::MEGABYTE
|
||||
$unit = ByteUnit::fromString('megabytes'); // ByteUnit::MEGABYTE
|
||||
```
|
||||
|
||||
## Constants
|
||||
|
||||
```php
|
||||
// Common sizes
|
||||
$empty = Byte::zero(); // 0 B
|
||||
$kb = Byte::oneKilobyte(); // 1 KB
|
||||
$mb = Byte::oneMegabyte(); // 1 MB
|
||||
$gb = Byte::oneGigabyte(); // 1 GB
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# .env file
|
||||
MAX_UPLOAD_SIZE=10MB
|
||||
CACHE_MAX_SIZE=1GB
|
||||
LOG_MAX_SIZE=100MB
|
||||
MEMORY_LIMIT_WARNING=512MB
|
||||
```
|
||||
|
||||
### Application Usage
|
||||
|
||||
```php
|
||||
// Configuration class
|
||||
class AppConfig
|
||||
{
|
||||
public static function getMaxUploadSize(): Byte
|
||||
{
|
||||
return Byte::parse($_ENV['MAX_UPLOAD_SIZE'] ?? '5MB');
|
||||
}
|
||||
|
||||
public static function getCacheLimit(): Byte
|
||||
{
|
||||
return Byte::parse($_ENV['CACHE_MAX_SIZE'] ?? '500MB');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in controllers
|
||||
class FileController
|
||||
{
|
||||
public function upload(Request $request): Response
|
||||
{
|
||||
$maxSize = AppConfig::getMaxUploadSize();
|
||||
|
||||
foreach ($request->files as $file) {
|
||||
$fileSize = Byte::fromBytes($file->getSize());
|
||||
|
||||
if ($fileSize->greaterThan($maxSize)) {
|
||||
return new JsonResponse([
|
||||
'error' => "File {$file->getName()} ({$fileSize}) exceeds maximum size of {$maxSize}"
|
||||
], 413);
|
||||
}
|
||||
}
|
||||
|
||||
// Process upload...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```php
|
||||
try {
|
||||
// Invalid format
|
||||
$size = Byte::parse('invalid');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
echo $e->getMessage(); // "Invalid byte format: invalid"
|
||||
}
|
||||
|
||||
try {
|
||||
// Negative bytes
|
||||
$size = Byte::fromBytes(-100);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
echo $e->getMessage(); // "Bytes cannot be negative"
|
||||
}
|
||||
|
||||
try {
|
||||
// Subtraction resulting in negative
|
||||
$result = Byte::fromBytes(100)->subtract(Byte::fromBytes(200));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
echo $e->getMessage(); // "Subtraction would result in negative bytes"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use in Configuration**: Store all size limits as `Byte` objects for type safety
|
||||
2. **Database Storage**: Store raw bytes as integers, convert to `Byte` objects in getters
|
||||
3. **Validation**: Use `Byte` objects for all size-related validation logic
|
||||
4. **Logging**: Use `toHumanReadable()` for user-friendly log messages
|
||||
5. **APIs**: Accept string formats and parse them into `Byte` objects
|
||||
6. **Performance**: Create `Byte` objects lazily when needed, store raw bytes when possible
|
||||
314
src/Framework/Core/ValueObjects/Score.php
Normal file
314
src/Framework/Core/ValueObjects/Score.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents a normalized score value between 0.0 and 1.0
|
||||
*/
|
||||
final readonly class Score
|
||||
{
|
||||
public function __construct(
|
||||
private float $value,
|
||||
private float $min = 0.0,
|
||||
private float $max = 1.0
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create score from percentage
|
||||
*/
|
||||
public static function fromPercentage(Percentage $percentage): self
|
||||
{
|
||||
return new self($percentage->toDecimal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create score from ratio
|
||||
*/
|
||||
public static function fromRatio(int $numerator, int $denominator): self
|
||||
{
|
||||
if ($denominator <= 0) {
|
||||
throw new InvalidArgumentException('Denominator must be greater than 0');
|
||||
}
|
||||
|
||||
if ($numerator < 0) {
|
||||
throw new InvalidArgumentException('Numerator must be non-negative');
|
||||
}
|
||||
|
||||
return new self(min(1.0, $numerator / $denominator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create critical level score (0.9)
|
||||
*/
|
||||
public static function critical(): self
|
||||
{
|
||||
return new self(0.9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create high level score (0.7)
|
||||
*/
|
||||
public static function high(): self
|
||||
{
|
||||
return new self(0.7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create medium level score (0.5)
|
||||
*/
|
||||
public static function medium(): self
|
||||
{
|
||||
return new self(0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create low level score (0.2)
|
||||
*/
|
||||
public static function low(): self
|
||||
{
|
||||
return new self(0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create minimum score (0.0)
|
||||
*/
|
||||
public static function zero(): self
|
||||
{
|
||||
return new self(0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create maximum score (1.0)
|
||||
*/
|
||||
public static function max(): self
|
||||
{
|
||||
return new self(1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw score value
|
||||
*/
|
||||
public function value(): float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to percentage using framework Percentage object
|
||||
*/
|
||||
public function toPercentage(): Percentage
|
||||
{
|
||||
return Percentage::fromDecimal($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get score level using ScoreLevel enum
|
||||
*/
|
||||
public function toLevel(): ScoreLevel
|
||||
{
|
||||
return ScoreLevel::fromScore($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is at or above a certain level
|
||||
*/
|
||||
public function isAtLevel(ScoreLevel $level): bool
|
||||
{
|
||||
return $this->value >= $level->getThreshold();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is critical level (>= 0.9)
|
||||
*/
|
||||
public function isCritical(): bool
|
||||
{
|
||||
return $this->isAtLevel(ScoreLevel::CRITICAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is high level (>= 0.7)
|
||||
*/
|
||||
public function isHigh(): bool
|
||||
{
|
||||
return $this->isAtLevel(ScoreLevel::HIGH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is medium level (>= 0.3)
|
||||
*/
|
||||
public function isMedium(): bool
|
||||
{
|
||||
return $this->isAtLevel(ScoreLevel::MEDIUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is low level (< 0.3)
|
||||
*/
|
||||
public function isLow(): bool
|
||||
{
|
||||
return ! $this->isAtLevel(ScoreLevel::MEDIUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine with another score using weighted average
|
||||
*/
|
||||
public function combine(Score $other, float $weight = 0.5): self
|
||||
{
|
||||
if ($weight < 0.0 || $weight > 1.0) {
|
||||
throw new InvalidArgumentException('Weight must be between 0.0 and 1.0');
|
||||
}
|
||||
|
||||
$combinedValue = ($this->value * $weight) + ($other->value * (1.0 - $weight));
|
||||
|
||||
return new self($combinedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another score (clamped to max)
|
||||
*/
|
||||
public function add(Score $other): self
|
||||
{
|
||||
return new self(min(1.0, $this->value + $other->value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply by factor (clamped to max)
|
||||
*/
|
||||
public function multiply(float $factor): self
|
||||
{
|
||||
if ($factor < 0.0) {
|
||||
throw new InvalidArgumentException('Factor must be non-negative');
|
||||
}
|
||||
|
||||
return new self(min(1.0, $this->value * $factor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is above threshold
|
||||
*/
|
||||
public function isAbove(Score $threshold): bool
|
||||
{
|
||||
return $this->value > $threshold->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if score is below threshold
|
||||
*/
|
||||
public function isBelow(Score $threshold): bool
|
||||
{
|
||||
return $this->value < $threshold->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inverted score (1.0 - value)
|
||||
*/
|
||||
public function invert(): self
|
||||
{
|
||||
return new self(1.0 - $this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize to custom range
|
||||
*/
|
||||
public function normalize(float $newMin, float $newMax): float
|
||||
{
|
||||
if ($newMin >= $newMax) {
|
||||
throw new InvalidArgumentException('New minimum must be less than new maximum');
|
||||
}
|
||||
|
||||
return $newMin + ($this->value * ($newMax - $newMin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'value' => $this->value,
|
||||
'percentage' => $this->toPercentage()->getValue(),
|
||||
'level' => $this->toLevel()->value,
|
||||
'min' => $this->min,
|
||||
'max' => $this->max,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from array
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
value: $data['value'],
|
||||
min: $data['min'] ?? 0.0,
|
||||
max: $data['max'] ?? 1.0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return sprintf('%.3f (%s)', $this->value, $this->toLevel()->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate score value
|
||||
*/
|
||||
private function validate(): void
|
||||
{
|
||||
if ($this->min >= $this->max) {
|
||||
throw new InvalidArgumentException('Minimum must be less than maximum');
|
||||
}
|
||||
|
||||
if ($this->value < $this->min || $this->value > $this->max) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Score value %.3f must be between %.3f and %.3f', $this->value, $this->min, $this->max)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weighted average of multiple scores
|
||||
*/
|
||||
public static function weightedAverage(array $scores, array $weights = []): self
|
||||
{
|
||||
if (empty($scores)) {
|
||||
throw new InvalidArgumentException('Scores array cannot be empty');
|
||||
}
|
||||
|
||||
if (! empty($weights) && count($scores) !== count($weights)) {
|
||||
throw new InvalidArgumentException('Scores and weights arrays must have the same length');
|
||||
}
|
||||
|
||||
// Use equal weights if no weights provided
|
||||
if (empty($weights)) {
|
||||
$weights = array_fill(0, count($scores), 1.0 / count($scores));
|
||||
}
|
||||
|
||||
// Normalize weights to sum to 1.0
|
||||
$totalWeight = array_sum($weights);
|
||||
if ($totalWeight <= 0.0) {
|
||||
throw new InvalidArgumentException('Total weight must be greater than 0');
|
||||
}
|
||||
|
||||
$normalizedWeights = array_map(fn ($w) => $w / $totalWeight, $weights);
|
||||
|
||||
$weightedSum = 0.0;
|
||||
foreach ($scores as $index => $score) {
|
||||
if (! $score instanceof Score) {
|
||||
throw new InvalidArgumentException('All items must be Score instances');
|
||||
}
|
||||
$weightedSum += $score->value * $normalizedWeights[$index];
|
||||
}
|
||||
|
||||
return new self($weightedSum);
|
||||
}
|
||||
}
|
||||
97
src/Framework/Core/ValueObjects/ScoreLevel.php
Normal file
97
src/Framework/Core/ValueObjects/ScoreLevel.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
/**
|
||||
* Represents different levels of scoring (low, medium, high, critical)
|
||||
*/
|
||||
enum ScoreLevel: string
|
||||
{
|
||||
case LOW = 'low';
|
||||
case MEDIUM = 'medium';
|
||||
case HIGH = 'high';
|
||||
case CRITICAL = 'critical';
|
||||
|
||||
/**
|
||||
* Get the minimum threshold for this level
|
||||
*/
|
||||
public function getThreshold(): float
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW => 0.0,
|
||||
self::MEDIUM => 0.3,
|
||||
self::HIGH => 0.7,
|
||||
self::CRITICAL => 0.9
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric priority for comparisons
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW => 1,
|
||||
self::MEDIUM => 2,
|
||||
self::HIGH => 3,
|
||||
self::CRITICAL => 4
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get descriptive text
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW => 'Low priority - minimal concern',
|
||||
self::MEDIUM => 'Medium priority - monitoring recommended',
|
||||
self::HIGH => 'High priority - action required',
|
||||
self::CRITICAL => 'Critical priority - immediate action required'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this level is higher than another
|
||||
*/
|
||||
public function isHigherThan(ScoreLevel $other): bool
|
||||
{
|
||||
return $this->getPriority() > $other->getPriority();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this level is lower than another
|
||||
*/
|
||||
public function isLowerThan(ScoreLevel $other): bool
|
||||
{
|
||||
return $this->getPriority() < $other->getPriority();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate action recommendation
|
||||
*/
|
||||
public function getRecommendedAction(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW => 'monitor',
|
||||
self::MEDIUM => 'investigate',
|
||||
self::HIGH => 'act',
|
||||
self::CRITICAL => 'immediate_action'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from score value
|
||||
*/
|
||||
public static function fromScore(float $score): self
|
||||
{
|
||||
return match (true) {
|
||||
$score >= 0.9 => self::CRITICAL,
|
||||
$score >= 0.7 => self::HIGH,
|
||||
$score >= 0.3 => self::MEDIUM,
|
||||
default => self::LOW
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects\Services;
|
||||
|
||||
use App\Framework\Core\ValueObjects\EmailAddress;
|
||||
use App\Framework\Core\ValueObjects\Hash;
|
||||
use App\Framework\Core\ValueObjects\HashAlgorithm;
|
||||
|
||||
final class EmailSecurityService
|
||||
{
|
||||
public function hashForStorage(EmailAddress $email): Hash
|
||||
{
|
||||
// Hash normalized email for consistent storage
|
||||
return Hash::create($email->normalize()->getValue(), HashAlgorithm::SHA256);
|
||||
}
|
||||
|
||||
public function hashForVerification(EmailAddress $email, string $secret): Hash
|
||||
{
|
||||
$data = $email->normalize()->getValue() . $secret . time();
|
||||
|
||||
return Hash::create($data, HashAlgorithm::SHA256);
|
||||
}
|
||||
|
||||
public function generateVerificationToken(EmailAddress $email, string $secret): string
|
||||
{
|
||||
$hash = $this->hashForVerification($email, $secret);
|
||||
|
||||
return base64_encode($hash->getValue());
|
||||
}
|
||||
|
||||
public function verifyToken(EmailAddress $email, string $token, string $secret, int $maxAge = 3600): bool
|
||||
{
|
||||
try {
|
||||
$hash = base64_decode($token, true);
|
||||
if ($hash === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For verification tokens, we'd need to store the timestamp
|
||||
// This is a simplified version
|
||||
$expectedHash = $this->hashForVerification($email, $secret);
|
||||
|
||||
return hash_equals($expectedHash->getValue(), $hash);
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function detectSimilarEmails(EmailAddress $email, array $existingEmails): array
|
||||
{
|
||||
$similar = [];
|
||||
$normalized = $email->normalize();
|
||||
|
||||
foreach ($existingEmails as $existing) {
|
||||
if (! $existing instanceof EmailAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if emails are similar but not identical
|
||||
if (! $normalized->equals($existing->normalize())) {
|
||||
$similarity = $this->calculateSimilarity($normalized, $existing);
|
||||
if ($similarity > 0.8) {
|
||||
$similar[] = [
|
||||
'email' => $existing,
|
||||
'similarity' => $similarity,
|
||||
'reason' => $this->getSimilarityReason($normalized, $existing),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $similar;
|
||||
}
|
||||
|
||||
public function isSpammy(EmailAddress $email): bool
|
||||
{
|
||||
$local = $email->getLocalPart();
|
||||
|
||||
// Check for suspicious patterns
|
||||
$spamPatterns = [
|
||||
'/\d{8,}/', // Long numbers
|
||||
'/noreply|no-reply/', // No-reply addresses
|
||||
'/test\d+/', // Test emails with numbers
|
||||
'/admin\d+/', // Admin emails with numbers
|
||||
'/user\d{4,}/', // User emails with many numbers
|
||||
];
|
||||
|
||||
foreach ($spamPatterns as $pattern) {
|
||||
if (preg_match($pattern, strtolower($local))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for disposable domains
|
||||
if ($email->isDisposable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRiskScore(EmailAddress $email): float
|
||||
{
|
||||
$score = 0.0;
|
||||
|
||||
// Disposable email: high risk
|
||||
if ($email->isDisposable()) {
|
||||
$score += 0.7;
|
||||
}
|
||||
|
||||
// Spammy patterns: medium risk
|
||||
if ($this->isSpammy($email)) {
|
||||
$score += 0.5;
|
||||
}
|
||||
|
||||
// Corporate email: low risk
|
||||
if ($email->isCorporate()) {
|
||||
$score -= 0.3;
|
||||
}
|
||||
|
||||
// Common provider: neutral to low risk
|
||||
if ($email->isCommonProvider()) {
|
||||
$score -= 0.1;
|
||||
}
|
||||
|
||||
return max(0.0, min(1.0, $score));
|
||||
}
|
||||
|
||||
private function calculateSimilarity(EmailAddress $email1, EmailAddress $email2): float
|
||||
{
|
||||
$local1 = $email1->getLocalPart();
|
||||
$local2 = $email2->getLocalPart();
|
||||
|
||||
// Levenshtein similarity for local parts
|
||||
$maxLength = max(strlen($local1), strlen($local2));
|
||||
if ($maxLength === 0) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
$distance = levenshtein($local1, $local2);
|
||||
$similarity = 1 - ($distance / $maxLength);
|
||||
|
||||
// Bonus if same domain
|
||||
if ($email1->isSameDomain($email2)) {
|
||||
$similarity += 0.2;
|
||||
}
|
||||
|
||||
return min(1.0, $similarity);
|
||||
}
|
||||
|
||||
private function getSimilarityReason(EmailAddress $email1, EmailAddress $email2): string
|
||||
{
|
||||
if ($email1->isSameDomain($email2)) {
|
||||
return 'Same domain with similar local part';
|
||||
}
|
||||
|
||||
$local1 = $email1->getLocalPart();
|
||||
$local2 = $email2->getLocalPart();
|
||||
|
||||
if (abs(strlen($local1) - strlen($local2)) <= 2) {
|
||||
return 'Similar length and characters';
|
||||
}
|
||||
|
||||
return 'Similar email structure';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects\Services;
|
||||
|
||||
enum EmailValidationLevel: string
|
||||
{
|
||||
case BLOCKED = 'blocked';
|
||||
case DISPOSABLE = 'disposable';
|
||||
case COMMON = 'common';
|
||||
case CORPORATE = 'corporate';
|
||||
case UNKNOWN = 'unknown';
|
||||
|
||||
public function getTrustScore(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::BLOCKED => 0,
|
||||
self::DISPOSABLE => 1,
|
||||
self::UNKNOWN => 2,
|
||||
self::COMMON => 3,
|
||||
self::CORPORATE => 4,
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::BLOCKED => 'Blocked',
|
||||
self::DISPOSABLE => 'Disposable',
|
||||
self::COMMON => 'Common Provider',
|
||||
self::CORPORATE => 'Corporate',
|
||||
self::UNKNOWN => 'Unknown',
|
||||
};
|
||||
}
|
||||
|
||||
public function shouldBlock(): bool
|
||||
{
|
||||
return $this === self::BLOCKED;
|
||||
}
|
||||
|
||||
public function requiresVerification(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::BLOCKED, self::DISPOSABLE => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
public function isHighTrust(): bool
|
||||
{
|
||||
return $this->getTrustScore() >= 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects\Services;
|
||||
|
||||
use App\Framework\Core\ValueObjects\EmailAddress;
|
||||
|
||||
final class EmailValidationService
|
||||
{
|
||||
public function __construct(
|
||||
private array $blockedDomains = [],
|
||||
private array $allowedDomains = [],
|
||||
private bool $blockDisposable = false
|
||||
) {
|
||||
}
|
||||
|
||||
public function isAllowed(EmailAddress $email): bool
|
||||
{
|
||||
// Check blocked domains first
|
||||
if ($this->isBlocked($email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check disposable emails if blocking is enabled
|
||||
if ($this->blockDisposable && $email->isDisposable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If allow list is set, only allow emails from those domains
|
||||
if (! empty($this->allowedDomains)) {
|
||||
return $this->isInAllowedDomains($email);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isBlocked(EmailAddress $email): bool
|
||||
{
|
||||
$domain = strtolower($email->getDomain());
|
||||
|
||||
foreach ($this->blockedDomains as $blockedDomain) {
|
||||
if ($this->domainMatches($domain, $blockedDomain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getValidationLevel(EmailAddress $email): EmailValidationLevel
|
||||
{
|
||||
if ($this->isBlocked($email)) {
|
||||
return EmailValidationLevel::BLOCKED;
|
||||
}
|
||||
|
||||
if ($email->isDisposable()) {
|
||||
return EmailValidationLevel::DISPOSABLE;
|
||||
}
|
||||
|
||||
if ($email->isCommonProvider()) {
|
||||
return EmailValidationLevel::COMMON;
|
||||
}
|
||||
|
||||
if ($email->isCorporate()) {
|
||||
return EmailValidationLevel::CORPORATE;
|
||||
}
|
||||
|
||||
return EmailValidationLevel::UNKNOWN;
|
||||
}
|
||||
|
||||
public function suggestCorrections(string $input): array
|
||||
{
|
||||
$suggestions = [];
|
||||
$email = trim($input);
|
||||
|
||||
// Common typos in domains
|
||||
$typoMap = [
|
||||
'gmail.co' => 'gmail.com',
|
||||
'gmail.om' => 'gmail.com',
|
||||
'gmial.com' => 'gmail.com',
|
||||
'yahoo.co' => 'yahoo.com',
|
||||
'yahoo.om' => 'yahoo.com',
|
||||
'hotmail.co' => 'hotmail.com',
|
||||
'hotmail.om' => 'hotmail.com',
|
||||
'outlok.com' => 'outlook.com',
|
||||
'web.e' => 'web.de',
|
||||
'gmx.e' => 'gmx.de',
|
||||
];
|
||||
|
||||
foreach ($typoMap as $typo => $correction) {
|
||||
if (str_ends_with(strtolower($email), '@' . $typo)) {
|
||||
$corrected = substr($email, 0, -(strlen($typo) + 1)) . '@' . $correction;
|
||||
$suggestions[] = $corrected;
|
||||
}
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
public function validateSyntax(string $email): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (empty($email)) {
|
||||
$errors[] = 'Email address cannot be empty';
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if (strlen($email) > 320) {
|
||||
$errors[] = 'Email address too long (max 320 characters)';
|
||||
}
|
||||
|
||||
if (substr_count($email, '@') !== 1) {
|
||||
$errors[] = 'Email must contain exactly one @ symbol';
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
$parts = explode('@', $email);
|
||||
$localPart = $parts[0];
|
||||
$domain = $parts[1];
|
||||
|
||||
// Local part validation
|
||||
if (empty($localPart)) {
|
||||
$errors[] = 'Local part cannot be empty';
|
||||
} elseif (strlen($localPart) > 64) {
|
||||
$errors[] = 'Local part too long (max 64 characters)';
|
||||
}
|
||||
|
||||
// Domain validation
|
||||
if (empty($domain)) {
|
||||
$errors[] = 'Domain cannot be empty';
|
||||
} elseif (strlen($domain) > 255) {
|
||||
$errors[] = 'Domain too long (max 255 characters)';
|
||||
} elseif (! str_contains($domain, '.')) {
|
||||
$errors[] = 'Domain must contain at least one dot';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function isInAllowedDomains(EmailAddress $email): bool
|
||||
{
|
||||
$domain = strtolower($email->getDomain());
|
||||
|
||||
foreach ($this->allowedDomains as $allowedDomain) {
|
||||
if ($this->domainMatches($domain, $allowedDomain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function domainMatches(string $domain, string $pattern): bool
|
||||
{
|
||||
// Exact match
|
||||
if ($domain === strtolower($pattern)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard subdomain match (*.example.com)
|
||||
if (str_starts_with($pattern, '*.')) {
|
||||
$baseDomain = substr($pattern, 2);
|
||||
|
||||
return str_ends_with($domain, '.' . $baseDomain) || $domain === $baseDomain;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
254
src/Framework/Core/ValueObjects/Services/UrlManipulator.php
Normal file
254
src/Framework/Core/ValueObjects/Services/UrlManipulator.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects\Services;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Url;
|
||||
|
||||
final class UrlManipulator
|
||||
{
|
||||
public function withScheme(Url $url, string $scheme): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
$parsed['scheme'] = $scheme;
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withHost(Url $url, string $host): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
$parsed['host'] = $host;
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withPort(Url $url, ?int $port): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
if ($port === null) {
|
||||
unset($parsed['port']);
|
||||
} else {
|
||||
$parsed['port'] = $port;
|
||||
}
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withPath(Url $url, string $path): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
$parsed['path'] = $path;
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withQuery(Url $url, array $params): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
if (empty($params)) {
|
||||
unset($parsed['query']);
|
||||
} else {
|
||||
$parsed['query'] = http_build_query($params);
|
||||
}
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withQueryParameter(Url $url, string $key, string $value): Url
|
||||
{
|
||||
$params = $url->getQueryParameters();
|
||||
$params[$key] = $value;
|
||||
|
||||
return $this->withQuery($url, $params);
|
||||
}
|
||||
|
||||
public function withoutQueryParameter(Url $url, string $key): Url
|
||||
{
|
||||
$params = $url->getQueryParameters();
|
||||
unset($params[$key]);
|
||||
|
||||
return $this->withQuery($url, $params);
|
||||
}
|
||||
|
||||
public function withFragment(Url $url, ?string $fragment): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
if ($fragment === null) {
|
||||
unset($parsed['fragment']);
|
||||
} else {
|
||||
$parsed['fragment'] = $fragment;
|
||||
}
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withCredentials(Url $url, string $user, ?string $password = null): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
$parsed['user'] = $user;
|
||||
if ($password !== null) {
|
||||
$parsed['pass'] = $password;
|
||||
}
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function withoutCredentials(Url $url): Url
|
||||
{
|
||||
$parsed = parse_url($url->getValue());
|
||||
unset($parsed['user'], $parsed['pass']);
|
||||
|
||||
return Url::from($this->buildUrl($parsed));
|
||||
}
|
||||
|
||||
public function resolve(Url $baseUrl, string $relative): Url
|
||||
{
|
||||
if (Url::isValid($relative)) {
|
||||
return Url::from($relative);
|
||||
}
|
||||
|
||||
$base = $baseUrl->scheme . '://' . $baseUrl->host;
|
||||
if ($baseUrl->port !== null) {
|
||||
$base .= ':' . $baseUrl->port;
|
||||
}
|
||||
|
||||
if (str_starts_with($relative, '/')) {
|
||||
return Url::from($base . $relative);
|
||||
}
|
||||
|
||||
$basePath = rtrim(dirname($baseUrl->path), '/');
|
||||
|
||||
return Url::from($base . $basePath . '/' . $relative);
|
||||
}
|
||||
|
||||
public function normalize(Url $url): Url
|
||||
{
|
||||
$normalized = strtolower($url->scheme) . '://';
|
||||
$normalized .= strtolower($url->host);
|
||||
|
||||
if ($url->port !== null && ! $this->isDefaultPort($url->scheme, $url->port)) {
|
||||
$normalized .= ':' . $url->port;
|
||||
}
|
||||
|
||||
$path = $url->path;
|
||||
if (empty($path)) {
|
||||
$path = '/';
|
||||
}
|
||||
$normalized .= $path;
|
||||
|
||||
if (! empty($url->query)) {
|
||||
$normalized .= '?' . $url->query;
|
||||
}
|
||||
|
||||
if (! empty($url->fragment)) {
|
||||
$normalized .= '#' . $url->fragment;
|
||||
}
|
||||
|
||||
return Url::from($normalized);
|
||||
}
|
||||
|
||||
public function removeTrailingSlash(Url $url): Url
|
||||
{
|
||||
if ($url->path !== '/' && str_ends_with($url->path, '/')) {
|
||||
return $this->withPath($url, rtrim($url->path, '/'));
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function addTrailingSlash(Url $url): Url
|
||||
{
|
||||
if (! str_ends_with($url->path, '/')) {
|
||||
return $this->withPath($url, $url->path . '/');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function appendPath(Url $url, string $pathSegment): Url
|
||||
{
|
||||
$pathSegment = trim($pathSegment, '/');
|
||||
$currentPath = rtrim($url->path, '/');
|
||||
|
||||
return $this->withPath($url, $currentPath . '/' . $pathSegment);
|
||||
}
|
||||
|
||||
public function prependPath(Url $url, string $pathSegment): Url
|
||||
{
|
||||
$pathSegment = trim($pathSegment, '/');
|
||||
$currentPath = ltrim($url->path, '/');
|
||||
|
||||
return $this->withPath($url, '/' . $pathSegment . '/' . $currentPath);
|
||||
}
|
||||
|
||||
public function secure(Url $url): Url
|
||||
{
|
||||
if ($url->scheme === 'http') {
|
||||
return $this->withScheme($url, 'https');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function insecure(Url $url): Url
|
||||
{
|
||||
if ($url->scheme === 'https') {
|
||||
return $this->withScheme($url, 'http');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function buildUrl(array $parsed): string
|
||||
{
|
||||
$url = '';
|
||||
|
||||
if (isset($parsed['scheme'])) {
|
||||
$url .= $parsed['scheme'] . '://';
|
||||
}
|
||||
|
||||
if (isset($parsed['user'])) {
|
||||
$url .= $parsed['user'];
|
||||
if (isset($parsed['pass'])) {
|
||||
$url .= ':' . $parsed['pass'];
|
||||
}
|
||||
$url .= '@';
|
||||
}
|
||||
|
||||
if (isset($parsed['host'])) {
|
||||
$url .= $parsed['host'];
|
||||
}
|
||||
|
||||
if (isset($parsed['port'])) {
|
||||
$url .= ':' . $parsed['port'];
|
||||
}
|
||||
|
||||
if (isset($parsed['path'])) {
|
||||
$url .= $parsed['path'];
|
||||
}
|
||||
|
||||
if (isset($parsed['query'])) {
|
||||
$url .= '?' . $parsed['query'];
|
||||
}
|
||||
|
||||
if (isset($parsed['fragment'])) {
|
||||
$url .= '#' . $parsed['fragment'];
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function isDefaultPort(string $scheme, int $port): bool
|
||||
{
|
||||
$defaults = [
|
||||
'http' => 80,
|
||||
'https' => 443,
|
||||
'ftp' => 21,
|
||||
'ssh' => 22,
|
||||
];
|
||||
|
||||
return isset($defaults[$scheme]) && $defaults[$scheme] === $port;
|
||||
}
|
||||
}
|
||||
144
src/Framework/Core/ValueObjects/Services/VersionParser.php
Normal file
144
src/Framework/Core/ValueObjects/Services/VersionParser.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects\Services;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Service für das Parsen von Versions-Strings
|
||||
* Unterstützt SemVer 2.0.0 Format: major.minor.patch[-prerelease][+buildmetadata]
|
||||
*/
|
||||
final class VersionParser
|
||||
{
|
||||
private const string VERSION_PATTERN = '/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\-\.]+))?(?:\+([a-zA-Z0-9\-\.]+))?$/';
|
||||
private const string SIMPLE_VERSION_PATTERN = '/^(\d+)\.(\d+)(?:\.(\d+))?(?:-([a-zA-Z0-9\-\.]+))?(?:\+([a-zA-Z0-9\-\.]+))?$/';
|
||||
|
||||
/**
|
||||
* Parst einen Versions-String in seine Komponenten
|
||||
*/
|
||||
public function parse(string $version): array
|
||||
{
|
||||
if (empty($version)) {
|
||||
throw new InvalidArgumentException('Version cannot be empty');
|
||||
}
|
||||
|
||||
// Versuche vollständiges SemVer Format
|
||||
if (preg_match(self::VERSION_PATTERN, $version, $matches)) {
|
||||
return $this->buildParseResult($matches, true);
|
||||
}
|
||||
|
||||
// Versuche vereinfachtes Format (major.minor optional .patch)
|
||||
if (preg_match(self::SIMPLE_VERSION_PATTERN, $version, $matches)) {
|
||||
return $this->buildParseResult($matches, false);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid version format: $version. Expected format: major.minor[.patch][-prerelease][+buildmetadata]"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Versions-String gültig ist
|
||||
*/
|
||||
public function isValid(string $version): bool
|
||||
{
|
||||
try {
|
||||
$this->parse($version);
|
||||
|
||||
return true;
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein String dem vollständigen SemVer Format entspricht
|
||||
*/
|
||||
public function isFullSemVer(string $version): bool
|
||||
{
|
||||
return preg_match(self::VERSION_PATTERN, $version) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein String dem vereinfachten Format entspricht
|
||||
*/
|
||||
public function isSimpleFormat(string $version): bool
|
||||
{
|
||||
return preg_match(self::SIMPLE_VERSION_PATTERN, $version) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Pre-Release Format
|
||||
*/
|
||||
public function isValidPreRelease(string $preRelease): bool
|
||||
{
|
||||
return preg_match('/^[a-zA-Z0-9\-\.]+$/', $preRelease) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Build-Metadaten Format
|
||||
*/
|
||||
public function isValidBuildMetadata(string $buildMetadata): bool
|
||||
{
|
||||
return preg_match('/^[a-zA-Z0-9\-\.]+$/', $buildMetadata) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert eine Version zu vollständigem SemVer Format
|
||||
*/
|
||||
public function normalize(string $version): string
|
||||
{
|
||||
$parts = $this->parse($version);
|
||||
|
||||
$normalized = "{$parts['major']}.{$parts['minor']}.{$parts['patch']}";
|
||||
|
||||
if ($parts['preRelease'] !== null) {
|
||||
$normalized .= "-{$parts['preRelease']}";
|
||||
}
|
||||
|
||||
if ($parts['buildMetadata'] !== null) {
|
||||
$normalized .= "+{$parts['buildMetadata']}";
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut das Parse-Ergebnis aus den Regex-Matches
|
||||
*/
|
||||
private function buildParseResult(array $matches, bool $isFullSemVer): array
|
||||
{
|
||||
$result = [
|
||||
'major' => (int) $matches[1],
|
||||
'minor' => (int) $matches[2],
|
||||
'patch' => 0,
|
||||
'preRelease' => null,
|
||||
'buildMetadata' => null,
|
||||
];
|
||||
|
||||
if ($isFullSemVer) {
|
||||
// Vollständiges SemVer: major.minor.patch[-prerelease][+buildmetadata]
|
||||
$result['patch'] = (int) $matches[3];
|
||||
$result['preRelease'] = isset($matches[4]) && $matches[4] !== '' ? $matches[4] : null;
|
||||
$result['buildMetadata'] = isset($matches[5]) && $matches[5] !== '' ? $matches[5] : null;
|
||||
} else {
|
||||
// Vereinfachtes Format: major.minor[.patch][-prerelease][+buildmetadata]
|
||||
$result['patch'] = isset($matches[3]) && $matches[3] !== '' ? (int) $matches[3] : 0;
|
||||
$result['preRelease'] = isset($matches[4]) && $matches[4] !== '' ? $matches[4] : null;
|
||||
$result['buildMetadata'] = isset($matches[5]) && $matches[5] !== '' ? $matches[5] : null;
|
||||
}
|
||||
|
||||
// Validiere Pre-Release und Build-Metadaten
|
||||
if ($result['preRelease'] !== null && ! $this->isValidPreRelease($result['preRelease'])) {
|
||||
throw new InvalidArgumentException("Invalid pre-release format: {$result['preRelease']}");
|
||||
}
|
||||
|
||||
if ($result['buildMetadata'] !== null && ! $this->isValidBuildMetadata($result['buildMetadata'])) {
|
||||
throw new InvalidArgumentException("Invalid build metadata format: {$result['buildMetadata']}");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
379
src/Framework/Core/ValueObjects/Statistics.php
Normal file
379
src/Framework/Core/ValueObjects/Statistics.php
Normal file
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value Object für Statistiken mit verschiedenen Metriken
|
||||
* Unterstützt Zähler, Memory-Usage, Hit-Ratios, Metadaten und Empfehlungen
|
||||
*/
|
||||
final readonly class Statistics
|
||||
{
|
||||
/**
|
||||
* @param array<string, int> $counters
|
||||
* @param array<string, mixed> $metadata
|
||||
* @param array<string> $recommendations
|
||||
*/
|
||||
public function __construct(
|
||||
private array $counters = [],
|
||||
private ?float $memoryUsageMb = null,
|
||||
private ?float $hitRatioPercent = null,
|
||||
private array $metadata = [],
|
||||
private array $recommendations = []
|
||||
) {
|
||||
$this->validateCounters();
|
||||
$this->validateHitRatio();
|
||||
$this->validateMemoryUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Statistiken nur mit Zählern
|
||||
* @param array<string, int> $counters
|
||||
*/
|
||||
public static function countersOnly(array $counters): self
|
||||
{
|
||||
return new self(counters: $counters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Statistiken mit Zählern und Speicherverbrauch
|
||||
* @param array<string, int> $counters
|
||||
*/
|
||||
public static function withMemory(array $counters, float $memoryUsageMb): self
|
||||
{
|
||||
return new self(
|
||||
counters: $counters,
|
||||
memoryUsageMb: $memoryUsageMb
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Performance-Statistiken mit Hit-Ratio
|
||||
* @param array<string, int> $counters
|
||||
*/
|
||||
public static function performance(
|
||||
array $counters,
|
||||
float $hitRatioPercent,
|
||||
?float $memoryUsageMb = null
|
||||
): self {
|
||||
return new self(
|
||||
counters: $counters,
|
||||
memoryUsageMb: $memoryUsageMb,
|
||||
hitRatioPercent: $hitRatioPercent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt umfassende Statistiken mit allen Metriken
|
||||
* @param array<string, int> $counters
|
||||
* @param array<string, mixed> $metadata
|
||||
* @param array<string> $recommendations
|
||||
*/
|
||||
public static function comprehensive(
|
||||
array $counters,
|
||||
?float $memoryUsageMb = null,
|
||||
?float $hitRatioPercent = null,
|
||||
array $metadata = [],
|
||||
array $recommendations = []
|
||||
): self {
|
||||
return new self(
|
||||
counters: $counters,
|
||||
memoryUsageMb: $memoryUsageMb,
|
||||
hitRatioPercent: $hitRatioPercent,
|
||||
metadata: $metadata,
|
||||
recommendations: $recommendations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt aus Array (für Legacy-Kompatibilität)
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
// Separiere Zähler von anderen Daten
|
||||
$counters = [];
|
||||
$metadata = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_int($value) && ! in_array($key, ['hits', 'misses', 'requests'])) {
|
||||
$counters[$key] = $value;
|
||||
} elseif (! in_array($key, ['memory_usage_mb', 'hit_ratio_percent', 'recommendations'])) {
|
||||
$metadata[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
counters: $counters,
|
||||
memoryUsageMb: $data['memory_usage_mb'] ?? $data['estimated_memory_mb'] ?? null,
|
||||
hitRatioPercent: $data['hit_ratio_percent'] ?? null,
|
||||
metadata: $metadata,
|
||||
recommendations: $data['recommendations'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Zähler zurück
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public function getCounters(): array
|
||||
{
|
||||
return $this->counters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen spezifischen Zähler zurück
|
||||
*/
|
||||
public function getCounter(string $name): int
|
||||
{
|
||||
return $this->counters[$name] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein Zähler existiert
|
||||
*/
|
||||
public function hasCounter(string $name): bool
|
||||
{
|
||||
return isset($this->counters[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Gesamtanzahl aller Zähler zurück
|
||||
*/
|
||||
public function getTotalCount(): int
|
||||
{
|
||||
return array_sum($this->counters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Speicherverbrauch in MB zurück
|
||||
*/
|
||||
public function getMemoryUsageMb(): ?float
|
||||
{
|
||||
return $this->memoryUsageMb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Hit-Ratio in Prozent zurück
|
||||
*/
|
||||
public function getHitRatioPercent(): ?float
|
||||
{
|
||||
return $this->hitRatioPercent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Metadaten zurück
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getMetadata(): array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen spezifischen Metadaten-Wert zurück
|
||||
*/
|
||||
public function getMetadataValue(string $key): mixed
|
||||
{
|
||||
return $this->metadata[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Empfehlungen zurück
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getRecommendations(): array
|
||||
{
|
||||
return $this->recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Empfehlungen vorhanden sind
|
||||
*/
|
||||
public function hasRecommendations(): bool
|
||||
{
|
||||
return ! empty($this->recommendations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Instanz mit zusätzlichen Zählern
|
||||
* @param array<string, int> $additionalCounters
|
||||
*/
|
||||
public function withAdditionalCounters(array $additionalCounters): self
|
||||
{
|
||||
return new self(
|
||||
counters: array_merge($this->counters, $additionalCounters),
|
||||
memoryUsageMb: $this->memoryUsageMb,
|
||||
hitRatioPercent: $this->hitRatioPercent,
|
||||
metadata: $this->metadata,
|
||||
recommendations: $this->recommendations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Instanz mit zusätzlichen Metadaten
|
||||
* @param array<string, mixed> $additionalMetadata
|
||||
*/
|
||||
public function withAdditionalMetadata(array $additionalMetadata): self
|
||||
{
|
||||
return new self(
|
||||
counters: $this->counters,
|
||||
memoryUsageMb: $this->memoryUsageMb,
|
||||
hitRatioPercent: $this->hitRatioPercent,
|
||||
metadata: array_merge($this->metadata, $additionalMetadata),
|
||||
recommendations: $this->recommendations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Instanz mit zusätzlichen Empfehlungen
|
||||
* @param array<string> $additionalRecommendations
|
||||
*/
|
||||
public function withAdditionalRecommendations(array $additionalRecommendations): self
|
||||
{
|
||||
return new self(
|
||||
counters: $this->counters,
|
||||
memoryUsageMb: $this->memoryUsageMb,
|
||||
hitRatioPercent: $this->hitRatioPercent,
|
||||
metadata: $this->metadata,
|
||||
recommendations: array_merge($this->recommendations, $additionalRecommendations)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kombiniert diese Statistiken mit anderen
|
||||
*/
|
||||
public function merge(self $other): self
|
||||
{
|
||||
$mergedCounters = $this->counters;
|
||||
foreach ($other->counters as $key => $value) {
|
||||
$mergedCounters[$key] = ($mergedCounters[$key] ?? 0) + $value;
|
||||
}
|
||||
|
||||
return new self(
|
||||
counters: $mergedCounters,
|
||||
memoryUsageMb: ($this->memoryUsageMb ?? 0) + ($other->memoryUsageMb ?? 0) ?: null,
|
||||
hitRatioPercent: $this->calculateAverageHitRatio($other),
|
||||
metadata: array_merge($this->metadata, $other->metadata),
|
||||
recommendations: array_unique(array_merge($this->recommendations, $other->recommendations))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert zu Array für Serialisierung/Legacy-Kompatibilität
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = $this->counters;
|
||||
|
||||
if ($this->memoryUsageMb !== null) {
|
||||
$array['memory_usage_mb'] = $this->memoryUsageMb;
|
||||
}
|
||||
|
||||
if ($this->hitRatioPercent !== null) {
|
||||
$array['hit_ratio_percent'] = $this->hitRatioPercent;
|
||||
}
|
||||
|
||||
if (! empty($this->metadata)) {
|
||||
$array = array_merge($array, $this->metadata);
|
||||
}
|
||||
|
||||
if (! empty($this->recommendations)) {
|
||||
$array['recommendations'] = $this->recommendations;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine formatierte String-Repräsentation zurück
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (! empty($this->counters)) {
|
||||
$counterStrings = [];
|
||||
foreach ($this->counters as $name => $count) {
|
||||
$counterStrings[] = "$name: $count";
|
||||
}
|
||||
$parts[] = 'Counters: ' . implode(', ', $counterStrings);
|
||||
}
|
||||
|
||||
if ($this->memoryUsageMb !== null) {
|
||||
$parts[] = sprintf('Memory: %.2f MB', $this->memoryUsageMb);
|
||||
}
|
||||
|
||||
if ($this->hitRatioPercent !== null) {
|
||||
$parts[] = sprintf('Hit Ratio: %.1f%%', $this->hitRatioPercent);
|
||||
}
|
||||
|
||||
return implode(' | ', $parts);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Zähler
|
||||
*/
|
||||
private function validateCounters(): void
|
||||
{
|
||||
foreach ($this->counters as $name => $value) {
|
||||
if (! is_string($name) || empty($name)) {
|
||||
throw new InvalidArgumentException('Counter names must be non-empty strings');
|
||||
}
|
||||
|
||||
if (! is_int($value) || $value < 0) {
|
||||
throw new InvalidArgumentException("Counter '$name' must be a non-negative integer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Hit-Ratio
|
||||
*/
|
||||
private function validateHitRatio(): void
|
||||
{
|
||||
if ($this->hitRatioPercent !== null && ($this->hitRatioPercent < 0 || $this->hitRatioPercent > 100)) {
|
||||
throw new InvalidArgumentException('Hit ratio must be between 0 and 100 percent');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert den Speicherverbrauch
|
||||
*/
|
||||
private function validateMemoryUsage(): void
|
||||
{
|
||||
if ($this->memoryUsageMb !== null && $this->memoryUsageMb < 0) {
|
||||
throw new InvalidArgumentException('Memory usage must be non-negative');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die durchschnittliche Hit-Ratio beim Mergen
|
||||
*/
|
||||
private function calculateAverageHitRatio(self $other): ?float
|
||||
{
|
||||
if ($this->hitRatioPercent === null && $other->hitRatioPercent === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->hitRatioPercent === null) {
|
||||
return $other->hitRatioPercent;
|
||||
}
|
||||
|
||||
if ($other->hitRatioPercent === null) {
|
||||
return $this->hitRatioPercent;
|
||||
}
|
||||
|
||||
// Einfacher Durchschnitt (könnte gewichtet werden basierend auf Zählern)
|
||||
return ($this->hitRatioPercent + $other->hitRatioPercent) / 2;
|
||||
}
|
||||
}
|
||||
97
src/Framework/Core/ValueObjects/TimeUnit.php
Normal file
97
src/Framework/Core/ValueObjects/TimeUnit.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
enum TimeUnit: string
|
||||
{
|
||||
case NANOSECOND = 'ns';
|
||||
case MICROSECOND = 'μs';
|
||||
case MILLISECOND = 'ms';
|
||||
case SECOND = 's';
|
||||
case MINUTE = 'min';
|
||||
case HOUR = 'h';
|
||||
case DAY = 'd';
|
||||
case WEEK = 'w';
|
||||
|
||||
public function getMultiplierToSeconds(): float
|
||||
{
|
||||
return match ($this) {
|
||||
self::NANOSECOND => 1e-9,
|
||||
self::MICROSECOND => 1e-6,
|
||||
self::MILLISECOND => 0.001,
|
||||
self::SECOND => 1.0,
|
||||
self::MINUTE => 60.0,
|
||||
self::HOUR => 3600.0,
|
||||
self::DAY => 86400.0,
|
||||
self::WEEK => 604800.0,
|
||||
};
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NANOSECOND => 'Nanosecond',
|
||||
self::MICROSECOND => 'Microsecond',
|
||||
self::MILLISECOND => 'Millisecond',
|
||||
self::SECOND => 'Second',
|
||||
self::MINUTE => 'Minute',
|
||||
self::HOUR => 'Hour',
|
||||
self::DAY => 'Day',
|
||||
self::WEEK => 'Week',
|
||||
};
|
||||
}
|
||||
|
||||
public function getNamePlural(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NANOSECOND => 'Nanoseconds',
|
||||
self::MICROSECOND => 'Microseconds',
|
||||
self::MILLISECOND => 'Milliseconds',
|
||||
self::SECOND => 'Seconds',
|
||||
self::MINUTE => 'Minutes',
|
||||
self::HOUR => 'Hours',
|
||||
self::DAY => 'Days',
|
||||
self::WEEK => 'Weeks',
|
||||
};
|
||||
}
|
||||
|
||||
public static function bestUnitFor(float $seconds): self
|
||||
{
|
||||
$absSeconds = abs($seconds);
|
||||
|
||||
if ($absSeconds >= 604800) { // 1 week
|
||||
return self::WEEK;
|
||||
} elseif ($absSeconds >= 86400) { // 1 day
|
||||
return self::DAY;
|
||||
} elseif ($absSeconds >= 3600) { // 1 hour
|
||||
return self::HOUR;
|
||||
} elseif ($absSeconds >= 60) { // 1 minute
|
||||
return self::MINUTE;
|
||||
} elseif ($absSeconds >= 1) {
|
||||
return self::SECOND;
|
||||
} elseif ($absSeconds >= 0.001) {
|
||||
return self::MILLISECOND;
|
||||
} elseif ($absSeconds >= 1e-6) {
|
||||
return self::MICROSECOND;
|
||||
} else {
|
||||
return self::NANOSECOND;
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromString(string $unit): self
|
||||
{
|
||||
return match (strtolower(trim($unit))) {
|
||||
'ns', 'nano', 'nanosecond', 'nanoseconds' => self::NANOSECOND,
|
||||
'μs', 'us', 'micro', 'microsecond', 'microseconds' => self::MICROSECOND,
|
||||
'ms', 'milli', 'millisecond', 'milliseconds' => self::MILLISECOND,
|
||||
's', 'sec', 'second', 'seconds' => self::SECOND,
|
||||
'min', 'minute', 'minutes' => self::MINUTE,
|
||||
'h', 'hr', 'hour', 'hours' => self::HOUR,
|
||||
'd', 'day', 'days' => self::DAY,
|
||||
'w', 'week', 'weeks' => self::WEEK,
|
||||
default => throw new \InvalidArgumentException("Unknown time unit: $unit"),
|
||||
};
|
||||
}
|
||||
}
|
||||
228
src/Framework/Core/ValueObjects/Timestamp.php
Normal file
228
src/Framework/Core/ValueObjects/Timestamp.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\DateTime\DateTime as FrameworkDateTime;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* Immutable timestamp value object for events
|
||||
*/
|
||||
final readonly class Timestamp
|
||||
{
|
||||
private function __construct(
|
||||
private float $microTimestamp
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timestamp from Clock instance
|
||||
*/
|
||||
public static function fromClock(Clock $clock): self
|
||||
{
|
||||
return $clock->time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timestamp from microtime float
|
||||
*/
|
||||
public static function fromFloat(float $microtime): self
|
||||
{
|
||||
return new self($microtime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timestamp for current time
|
||||
*/
|
||||
public static function now(): self
|
||||
{
|
||||
return new SystemClock()->time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timestamp from DateTimeImmutable
|
||||
*/
|
||||
public static function fromDateTime(DateTimeImmutable $dateTime): self
|
||||
{
|
||||
$timestamp = $dateTime->getTimestamp();
|
||||
$microseconds = (int) $dateTime->format('u');
|
||||
|
||||
return new self($timestamp + ($microseconds / 1_000_000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get as microtime float
|
||||
*/
|
||||
public function toFloat(): float
|
||||
{
|
||||
return $this->microTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get as unix timestamp (loses microsecond precision)
|
||||
*/
|
||||
public function toTimestamp(): int
|
||||
{
|
||||
return (int) $this->microTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get microseconds part (0-999999)
|
||||
*/
|
||||
public function getMicroseconds(): int
|
||||
{
|
||||
return (int) (($this->microTimestamp - floor($this->microTimestamp)) * 1_000_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to DateTimeImmutable
|
||||
*/
|
||||
public function toDateTime(?DateTimeZone $timezone = null): DateTimeImmutable
|
||||
{
|
||||
$timestamp = $this->toTimestamp();
|
||||
$microseconds = $this->getMicroseconds();
|
||||
|
||||
$dateTime = FrameworkDateTime::fromTimestamp($timestamp, $timezone);
|
||||
|
||||
// Add microseconds
|
||||
if ($microseconds > 0) {
|
||||
$microString = str_pad((string) $microseconds, 6, '0', STR_PAD_LEFT);
|
||||
$dateTime = $dateTime->modify("+{$microString} microseconds");
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to ISO 8601 string format
|
||||
*/
|
||||
public function toIso8601(): string
|
||||
{
|
||||
return $this->toDateTime(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s.uP');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp
|
||||
*/
|
||||
public function format(string $format, ?DateTimeZone $timezone = null): string
|
||||
{
|
||||
return $this->toDateTime($timezone)->format($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO 8601 string representation
|
||||
*/
|
||||
public function toIsoString(?DateTimeZone $timezone = null): string
|
||||
{
|
||||
return $this->toDateTime($timezone)->format('c');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare with another timestamp
|
||||
*/
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return abs($this->microTimestamp - $other->microTimestamp) < 0.000001; // 1 microsecond tolerance
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this timestamp is before another
|
||||
*/
|
||||
public function isBefore(self $other): bool
|
||||
{
|
||||
return $this->microTimestamp < $other->microTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this timestamp is after another
|
||||
*/
|
||||
public function isAfter(self $other): bool
|
||||
{
|
||||
return $this->microTimestamp > $other->microTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration between timestamps
|
||||
*/
|
||||
public function diff(self $other): Duration
|
||||
{
|
||||
$diffInSeconds = abs($this->microTimestamp - $other->microTimestamp);
|
||||
|
||||
return Duration::fromSeconds($diffInSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration since this timestamp
|
||||
*/
|
||||
public function age(Clock $clock): Duration
|
||||
{
|
||||
return $clock->time()->diff($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate duration until this timestamp
|
||||
*/
|
||||
public function timeUntil(Clock $clock): Duration
|
||||
{
|
||||
$now = $clock->time();
|
||||
|
||||
return $this->isAfter($now) ? $this->diff($now) : Duration::zero();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human readable time ago
|
||||
*/
|
||||
public function timeAgo(Clock $clock): string
|
||||
{
|
||||
$duration = $this->age($clock);
|
||||
|
||||
if ($duration->toMilliseconds() < 1000) {
|
||||
$ms = (int) $duration->toMilliseconds();
|
||||
|
||||
return "{$ms}ms ago";
|
||||
}
|
||||
|
||||
if ($duration->toSeconds() < 60) {
|
||||
$sec = (int) $duration->toSeconds();
|
||||
|
||||
return "{$sec}s ago";
|
||||
}
|
||||
|
||||
if ($duration->toMinutes() < 60) {
|
||||
$min = (int) $duration->toMinutes();
|
||||
|
||||
return "{$min}m ago";
|
||||
}
|
||||
|
||||
if ($duration->toHours() < 24) {
|
||||
$hours = (int) $duration->toHours();
|
||||
|
||||
return "{$hours}h ago";
|
||||
}
|
||||
|
||||
$days = (int) ($duration->toSeconds() / 86400);
|
||||
|
||||
return "{$days}d ago";
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON serialization
|
||||
*/
|
||||
public function jsonSerialize(): float
|
||||
{
|
||||
return $this->microTimestamp;
|
||||
}
|
||||
}
|
||||
232
src/Framework/Core/ValueObjects/Timezone.php
Normal file
232
src/Framework/Core/ValueObjects/Timezone.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use DateTimeZone;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Timezone value object
|
||||
*/
|
||||
final readonly class Timezone
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from timezone string
|
||||
*/
|
||||
public static function fromString(string $timezone): self
|
||||
{
|
||||
return new self($timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UTC timezone
|
||||
*/
|
||||
public static function utc(): self
|
||||
{
|
||||
return new self('UTC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timezone for Europe/Berlin
|
||||
*/
|
||||
public static function berlin(): self
|
||||
{
|
||||
return new self('Europe/Berlin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timezone for America/New_York
|
||||
*/
|
||||
public static function newYork(): self
|
||||
{
|
||||
return new self('America/New_York');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timezone for Asia/Tokyo
|
||||
*/
|
||||
public static function tokyo(): self
|
||||
{
|
||||
return new self('Asia/Tokyo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DateTimeZone object
|
||||
*/
|
||||
public function toDateTimeZone(): DateTimeZone
|
||||
{
|
||||
return new DateTimeZone($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone offset in seconds from UTC
|
||||
*/
|
||||
public function getOffsetFromUtc(): int
|
||||
{
|
||||
$timezone = $this->toDateTimeZone();
|
||||
$now = new \DateTime('now', $timezone);
|
||||
|
||||
return $timezone->getOffset($now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone offset as string (+/-HH:MM)
|
||||
*/
|
||||
public function getOffsetString(): string
|
||||
{
|
||||
$offset = $this->getOffsetFromUtc();
|
||||
$hours = intval($offset / 3600);
|
||||
$minutes = abs(intval(($offset % 3600) / 60));
|
||||
|
||||
return sprintf('%+03d:%02d', $hours, $minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timezone is UTC
|
||||
*/
|
||||
public function isUtc(): bool
|
||||
{
|
||||
return $this->value === 'UTC';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timezone uses daylight saving time
|
||||
*/
|
||||
public function usesDaylightSaving(): bool
|
||||
{
|
||||
$timezone = $this->toDateTimeZone();
|
||||
$transitions = $timezone->getTransitions();
|
||||
|
||||
// If there are transitions, it likely uses DST
|
||||
return count($transitions) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone name
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone abbreviation
|
||||
*/
|
||||
public function getAbbreviation(): string
|
||||
{
|
||||
$timezone = $this->toDateTimeZone();
|
||||
$now = new \DateTime('now', $timezone);
|
||||
|
||||
return $now->format('T');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get continent
|
||||
*/
|
||||
public function getContinent(): ?string
|
||||
{
|
||||
$parts = explode('/', $this->value);
|
||||
|
||||
return $parts[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get city/region
|
||||
*/
|
||||
public function getCity(): ?string
|
||||
{
|
||||
$parts = explode('/', $this->value);
|
||||
|
||||
return $parts[1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a valid timezone for geographic region
|
||||
*/
|
||||
public function isValidForCountry(CountryCode $countryCode): bool
|
||||
{
|
||||
$countryTimezones = [
|
||||
'DE' => ['Europe/Berlin'],
|
||||
'US' => [
|
||||
'America/New_York', 'America/Chicago', 'America/Denver',
|
||||
'America/Los_Angeles', 'America/Anchorage', 'Pacific/Honolulu',
|
||||
],
|
||||
'GB' => ['Europe/London'],
|
||||
'FR' => ['Europe/Paris'],
|
||||
'JP' => ['Asia/Tokyo'],
|
||||
'CN' => ['Asia/Shanghai'],
|
||||
'IN' => ['Asia/Kolkata'],
|
||||
'RU' => [
|
||||
'Europe/Moscow', 'Asia/Yekaterinburg', 'Asia/Novosibirsk',
|
||||
'Asia/Krasnoyarsk', 'Asia/Irkutsk', 'Asia/Yakutsk',
|
||||
'Asia/Vladivostok', 'Asia/Magadan', 'Asia/Kamchatka',
|
||||
],
|
||||
'AU' => [
|
||||
'Australia/Sydney', 'Australia/Melbourne', 'Australia/Brisbane',
|
||||
'Australia/Perth', 'Australia/Adelaide', 'Australia/Darwin',
|
||||
],
|
||||
'BR' => [
|
||||
'America/Sao_Paulo', 'America/Manaus', 'America/Fortaleza',
|
||||
'America/Recife', 'America/Bahia',
|
||||
],
|
||||
'CA' => [
|
||||
'America/Toronto', 'America/Vancouver', 'America/Montreal',
|
||||
'America/Calgary', 'America/Edmonton', 'America/Winnipeg',
|
||||
],
|
||||
];
|
||||
|
||||
$validTimezones = $countryTimezones[$countryCode->value] ?? [];
|
||||
|
||||
return in_array($this->value, $validTimezones, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'timezone' => $this->value,
|
||||
'offset' => $this->getOffsetString(),
|
||||
'offset_seconds' => $this->getOffsetFromUtc(),
|
||||
'abbreviation' => $this->getAbbreviation(),
|
||||
'continent' => $this->getContinent(),
|
||||
'city' => $this->getCity(),
|
||||
'is_utc' => $this->isUtc(),
|
||||
'uses_daylight_saving' => $this->usesDaylightSaving(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate timezone
|
||||
*/
|
||||
private function validate(): void
|
||||
{
|
||||
try {
|
||||
new DateTimeZone($this->value);
|
||||
} catch (\Exception $e) {
|
||||
throw new InvalidArgumentException("Invalid timezone: {$this->value}");
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
291
src/Framework/Core/ValueObjects/Url.php
Normal file
291
src/Framework/Core/ValueObjects/Url.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class Url
|
||||
{
|
||||
public string $scheme;
|
||||
|
||||
public string $host;
|
||||
|
||||
public ?int $port;
|
||||
|
||||
public string $path;
|
||||
|
||||
public string $query;
|
||||
|
||||
public string $fragment;
|
||||
|
||||
public string $user;
|
||||
|
||||
public string $password;
|
||||
|
||||
public function __construct(
|
||||
private string $value
|
||||
) {
|
||||
$this->validate();
|
||||
$this->parseComponents();
|
||||
}
|
||||
|
||||
// Factory Methods
|
||||
public static function from(string $url): self
|
||||
{
|
||||
return new self($url);
|
||||
}
|
||||
|
||||
public static function parse(string $url): self
|
||||
{
|
||||
$url = trim($url);
|
||||
|
||||
// Add protocol if missing
|
||||
if (! preg_match('/^https?:\/\//', $url)) {
|
||||
$url = 'https://' . $url;
|
||||
}
|
||||
|
||||
return new self($url);
|
||||
}
|
||||
|
||||
public static function build(
|
||||
string $scheme,
|
||||
string $host,
|
||||
?int $port = null,
|
||||
string $path = '',
|
||||
array $query = [],
|
||||
?string $fragment = null
|
||||
): self {
|
||||
$url = $scheme . '://' . $host;
|
||||
|
||||
if ($port !== null && ! self::isDefaultPort($scheme, $port)) {
|
||||
$url .= ':' . $port;
|
||||
}
|
||||
|
||||
if ($path && ! str_starts_with($path, '/')) {
|
||||
$path = '/' . $path;
|
||||
}
|
||||
$url .= $path;
|
||||
|
||||
if (! empty($query)) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
if ($fragment !== null) {
|
||||
$url .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return new self($url);
|
||||
}
|
||||
|
||||
// Validation
|
||||
public static function isValid(string $url): bool
|
||||
{
|
||||
try {
|
||||
new self($url);
|
||||
|
||||
return true;
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getQueryParameters(): array
|
||||
{
|
||||
if (empty($this->query)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
parse_str($this->query, $params);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function getEffectivePort(): int
|
||||
{
|
||||
return $this->port ?? $this->getDefaultPort();
|
||||
}
|
||||
|
||||
public function getPortEnum(): ?Port
|
||||
{
|
||||
if ($this->port === null) {
|
||||
return Port::forScheme($this->scheme);
|
||||
}
|
||||
|
||||
return Port::tryFrom($this->port);
|
||||
}
|
||||
|
||||
// URL checks
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return $this->scheme === 'https' || $this->getPortEnum()?->isSecure() === true;
|
||||
}
|
||||
|
||||
public function isHttp(): bool
|
||||
{
|
||||
return in_array($this->scheme, ['http', 'https'], true);
|
||||
}
|
||||
|
||||
public function isLocal(): bool
|
||||
{
|
||||
return in_array($this->host, ['localhost', '127.0.0.1', '::1'], true) ||
|
||||
str_ends_with($this->host, '.local') ||
|
||||
preg_match('/^192\.168\./', $this->host) ||
|
||||
preg_match('/^10\./', $this->host) ||
|
||||
preg_match('/^172\.(1[6-9]|2[0-9]|3[01])\./', $this->host);
|
||||
}
|
||||
|
||||
public function isAbsolute(): bool
|
||||
{
|
||||
return ! empty($this->scheme);
|
||||
}
|
||||
|
||||
public function isRelative(): bool
|
||||
{
|
||||
return ! $this->isAbsolute();
|
||||
}
|
||||
|
||||
public function hasCredentials(): bool
|
||||
{
|
||||
return ! empty($this->user);
|
||||
}
|
||||
|
||||
public function getOrigin(): self
|
||||
{
|
||||
$origin = $this->scheme . '://' . $this->host;
|
||||
if ($this->port !== null && ! self::isDefaultPort($this->scheme, $this->port)) {
|
||||
$origin .= ':' . $this->port;
|
||||
}
|
||||
|
||||
return new self($origin);
|
||||
}
|
||||
|
||||
// Comparison
|
||||
public function equals(Url $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function isSameOrigin(Url $other): bool
|
||||
{
|
||||
return $this->getOrigin()->equals($other->getOrigin());
|
||||
}
|
||||
|
||||
public function isSameDomain(Url $other): bool
|
||||
{
|
||||
return strtolower($this->host) === strtolower($other->host);
|
||||
}
|
||||
|
||||
// String representation
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private function validate(): void
|
||||
{
|
||||
if (empty($this->value)) {
|
||||
throw new InvalidArgumentException('URL cannot be empty');
|
||||
}
|
||||
|
||||
if (strlen($this->value) > 2048) {
|
||||
throw new InvalidArgumentException('URL too long (max 2048 characters)');
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
$trimmed = trim($this->value);
|
||||
if ($trimmed !== $this->value) {
|
||||
throw new InvalidArgumentException("URL cannot have leading or trailing whitespace: {$this->value}");
|
||||
}
|
||||
|
||||
$parsed = parse_url($this->value);
|
||||
if ($parsed === false) {
|
||||
throw new InvalidArgumentException("Invalid URL: {$this->value}");
|
||||
}
|
||||
|
||||
if (! isset($parsed['scheme'], $parsed['host'])) {
|
||||
throw new InvalidArgumentException("URL must have scheme and host: {$this->value}");
|
||||
}
|
||||
|
||||
// Validate scheme
|
||||
$scheme = strtolower($parsed['scheme']);
|
||||
if (! in_array($scheme, ['http', 'https', 'ftp', 'ftps', 'ssh', 'file', 'data'], true)) {
|
||||
throw new InvalidArgumentException("Unsupported URL scheme: {$parsed['scheme']}");
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if (empty($parsed['host']) || strlen($parsed['host']) > 253) {
|
||||
throw new InvalidArgumentException("Invalid host: {$parsed['host']}");
|
||||
}
|
||||
|
||||
// Check for invalid characters in host
|
||||
if (preg_match('/[\s<>"\']/', $parsed['host'])) {
|
||||
throw new InvalidArgumentException("Host contains invalid characters: {$parsed['host']}");
|
||||
}
|
||||
|
||||
// Check for consecutive dots in host
|
||||
if (strpos($parsed['host'], '..') !== false) {
|
||||
throw new InvalidArgumentException("Host cannot contain consecutive dots: {$parsed['host']}");
|
||||
}
|
||||
|
||||
// Check for host starting or ending with dot or dash
|
||||
if (preg_match('/^[.-]|[.-]$/', $parsed['host'])) {
|
||||
throw new InvalidArgumentException("Host cannot start or end with dot or dash: {$parsed['host']}");
|
||||
}
|
||||
|
||||
// Validate IP addresses (if host looks like an IP)
|
||||
if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $parsed['host'])) {
|
||||
if (! filter_var($parsed['host'], FILTER_VALIDATE_IP)) {
|
||||
throw new InvalidArgumentException("Invalid IP address: {$parsed['host']}");
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed['port']) && ! Port::isValidPort($parsed['port'])) {
|
||||
throw new InvalidArgumentException("Invalid port: {$parsed['port']}");
|
||||
}
|
||||
|
||||
// Additional validation for specific URL patterns that should be rejected
|
||||
if (in_array($this->value, ['http://', 'https://', 'ftp://'], true)) {
|
||||
throw new InvalidArgumentException("URL cannot be just a scheme: {$this->value}");
|
||||
}
|
||||
}
|
||||
|
||||
private function parseComponents(): void
|
||||
{
|
||||
$parsed = parse_url($this->value);
|
||||
|
||||
$this->scheme = $parsed['scheme'] ?? '';
|
||||
$this->host = $parsed['host'] ?? '';
|
||||
$this->port = isset($parsed['port']) ? (int) $parsed['port'] : null;
|
||||
$this->path = $parsed['path'] ?? '/';
|
||||
$this->query = $parsed['query'] ?? '';
|
||||
$this->fragment = $parsed['fragment'] ?? '';
|
||||
$this->user = $parsed['user'] ?? '';
|
||||
$this->password = $parsed['pass'] ?? '';
|
||||
}
|
||||
|
||||
private function getDefaultPort(): int
|
||||
{
|
||||
return Port::forScheme($this->scheme)?->value ?? 80;
|
||||
}
|
||||
|
||||
private static function isDefaultPort(string $scheme, int $port): bool
|
||||
{
|
||||
$defaultPort = Port::forScheme($scheme);
|
||||
|
||||
return $defaultPort?->value === $port;
|
||||
}
|
||||
}
|
||||
256
src/Framework/Core/ValueObjects/Version.php
Normal file
256
src/Framework/Core/ValueObjects/Version.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\ValueObjects;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Services\VersionParser;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value Object für semantische Versionierung
|
||||
* Unterstützt SemVer 2.0.0 Format: major.minor.patch[-prerelease][+buildmetadata]
|
||||
*/
|
||||
final readonly class Version
|
||||
{
|
||||
public function __construct(
|
||||
private int $major,
|
||||
private int $minor,
|
||||
private int $patch = 0,
|
||||
private ?string $preRelease = null,
|
||||
private ?string $buildMetadata = null
|
||||
) {
|
||||
if ($major < 0 || $minor < 0 || $patch < 0) {
|
||||
throw new InvalidArgumentException('Version components must be non-negative');
|
||||
}
|
||||
|
||||
$parser = new VersionParser();
|
||||
|
||||
if ($preRelease !== null && ! $parser->isValidPreRelease($preRelease)) {
|
||||
throw new InvalidArgumentException("Invalid pre-release format: $preRelease");
|
||||
}
|
||||
|
||||
if ($buildMetadata !== null && ! $parser->isValidBuildMetadata($buildMetadata)) {
|
||||
throw new InvalidArgumentException("Invalid build metadata format: $buildMetadata");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Version aus String
|
||||
*/
|
||||
public static function fromString(string $version): self
|
||||
{
|
||||
$parser = new VersionParser();
|
||||
$parts = $parser->parse($version);
|
||||
|
||||
return new self(
|
||||
major: $parts['major'],
|
||||
minor: $parts['minor'],
|
||||
patch: $parts['patch'],
|
||||
preRelease: $parts['preRelease'],
|
||||
buildMetadata: $parts['buildMetadata']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Version aus Komponenten (alias für Konstruktor für bessere Lesbarkeit)
|
||||
*/
|
||||
public static function fromComponents(
|
||||
int $major,
|
||||
int $minor,
|
||||
int $patch = 0,
|
||||
?string $preRelease = null,
|
||||
?string $buildMetadata = null
|
||||
): self {
|
||||
return new self($major, $minor, $patch, $preRelease, $buildMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den vollständigen Versions-String zurück
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
$version = "{$this->major}.{$this->minor}.{$this->patch}";
|
||||
|
||||
if ($this->preRelease !== null) {
|
||||
$version .= "-{$this->preRelease}";
|
||||
}
|
||||
|
||||
if ($this->buildMetadata !== null) {
|
||||
$version .= "+{$this->buildMetadata}";
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Major-Version zurück
|
||||
*/
|
||||
public function getMajor(): int
|
||||
{
|
||||
return $this->major;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Minor-Version zurück
|
||||
*/
|
||||
public function getMinor(): int
|
||||
{
|
||||
return $this->minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Patch-Version zurück
|
||||
*/
|
||||
public function getPatch(): int
|
||||
{
|
||||
return $this->patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Pre-Release-String zurück (falls vorhanden)
|
||||
*/
|
||||
public function getPreRelease(): ?string
|
||||
{
|
||||
return $this->preRelease;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Build-Metadaten zurück (falls vorhanden)
|
||||
*/
|
||||
public function getBuildMetadata(): ?string
|
||||
{
|
||||
return $this->buildMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob diese Version mit einer anderen kompatibel ist (gleiche Major-Version)
|
||||
*/
|
||||
public function isCompatibleWith(self $other): bool
|
||||
{
|
||||
return $this->major === $other->major;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob diese Version neuer als eine andere ist
|
||||
*/
|
||||
public function isNewerThan(self $other): bool
|
||||
{
|
||||
if ($this->major !== $other->major) {
|
||||
return $this->major > $other->major;
|
||||
}
|
||||
|
||||
if ($this->minor !== $other->minor) {
|
||||
return $this->minor > $other->minor;
|
||||
}
|
||||
|
||||
if ($this->patch !== $other->patch) {
|
||||
return $this->patch > $other->patch;
|
||||
}
|
||||
|
||||
// Bei gleicher Kern-Version: Stable > Pre-Release
|
||||
if ($this->preRelease === null && $other->preRelease !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->preRelease !== null && $other->preRelease === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Beide haben Pre-Release oder beide nicht
|
||||
if ($this->preRelease !== null && $other->preRelease !== null) {
|
||||
return version_compare($this->preRelease, $other->preRelease, '>');
|
||||
}
|
||||
|
||||
return false; // Gleiche Version
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob diese Version älter als eine andere ist
|
||||
*/
|
||||
public function isOlderThan(self $other): bool
|
||||
{
|
||||
return $other->isNewerThan($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob diese Version gleich einer anderen ist
|
||||
*/
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->major === $other->major
|
||||
&& $this->minor === $other->minor
|
||||
&& $this->patch === $other->patch
|
||||
&& $this->preRelease === $other->preRelease;
|
||||
// Build-Metadaten werden bei Gleichheit ignoriert (SemVer 2.0.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob dies eine Pre-Release-Version ist
|
||||
*/
|
||||
public function isPreRelease(): bool
|
||||
{
|
||||
return $this->preRelease !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob dies eine stabile Version ist
|
||||
*/
|
||||
public function isStable(): bool
|
||||
{
|
||||
return $this->preRelease === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine neue Version mit erhöhter Major-Version zurück
|
||||
*/
|
||||
public function incrementMajor(): self
|
||||
{
|
||||
return new self($this->major + 1, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine neue Version mit erhöhter Minor-Version zurück
|
||||
*/
|
||||
public function incrementMinor(): self
|
||||
{
|
||||
return new self($this->major, $this->minor + 1, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine neue Version mit erhöhter Patch-Version zurück
|
||||
*/
|
||||
public function incrementPatch(): self
|
||||
{
|
||||
return new self($this->major, $this->minor, $this->patch + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt nur die Kern-Version ohne Pre-Release und Build-Metadaten zurück
|
||||
*/
|
||||
public function getVersionCore(): string
|
||||
{
|
||||
return "{$this->major}.{$this->minor}.{$this->patch}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine kurze Version (major.minor) zurück
|
||||
*/
|
||||
public function getShortVersion(): string
|
||||
{
|
||||
return "{$this->major}.{$this->minor}";
|
||||
}
|
||||
|
||||
/**
|
||||
* String-Repräsentation
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->getValue();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
interface ViewModel
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
357
src/Framework/Core/Warmup/ContainerWarmupStrategy.php
Normal file
357
src/Framework/Core/Warmup/ContainerWarmupStrategy.php
Normal file
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core\Warmup;
|
||||
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\EnvironmentType;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\ContainerCompiler;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\DI\DependencyResolver;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
|
||||
/**
|
||||
* Container Warmup Strategy
|
||||
*
|
||||
* Provides intelligent container warmup strategies for different environments.
|
||||
* Optimizes container compilation and dependency resolution for production use.
|
||||
*/
|
||||
final readonly class ContainerWarmupStrategy
|
||||
{
|
||||
public function __construct(
|
||||
private ?Logger $logger = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Warmup container based on environment and usage patterns
|
||||
*/
|
||||
public function warmup(DefaultContainer $container, Environment $env): WarmupResult
|
||||
{
|
||||
$envType = EnvironmentType::fromEnvironment($env);
|
||||
|
||||
return match (true) {
|
||||
$envType->isProduction() => $this->productionWarmup($container),
|
||||
$envType->isTesting() => $this->testingWarmup($container),
|
||||
default => $this->developmentWarmup($container)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Production warmup strategy - aggressive optimization
|
||||
*/
|
||||
private function productionWarmup(DefaultContainer $container): WarmupResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$operations = [];
|
||||
|
||||
try {
|
||||
// 1. Precompile container
|
||||
$operations[] = $this->precompileContainer($container);
|
||||
|
||||
// 2. Warm critical services
|
||||
$operations[] = $this->warmCriticalServices($container);
|
||||
|
||||
// 3. Prime reflection cache
|
||||
$operations[] = $this->primeReflectionCache($container);
|
||||
|
||||
// 4. Validate dependencies
|
||||
$operations[] = $this->validateDependencies($container);
|
||||
|
||||
$duration = microtime(true) - $startTime;
|
||||
|
||||
return new WarmupResult(
|
||||
success: true,
|
||||
duration: $duration,
|
||||
operations: $operations,
|
||||
strategy: 'production',
|
||||
optimizations: [
|
||||
'container_compilation' => true,
|
||||
'critical_services_preload' => true,
|
||||
'reflection_cache_prime' => true,
|
||||
'dependency_validation' => true,
|
||||
]
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger?->error('Production warmup failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'operations_completed' => count($operations),
|
||||
]);
|
||||
|
||||
return new WarmupResult(
|
||||
success: false,
|
||||
duration: microtime(true) - $startTime,
|
||||
operations: $operations,
|
||||
strategy: 'production',
|
||||
error: $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Development warmup strategy - minimal overhead
|
||||
*/
|
||||
private function developmentWarmup(DefaultContainer $container): WarmupResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$operations = [];
|
||||
|
||||
// Light warmup for development
|
||||
$operations[] = $this->validateDependencies($container);
|
||||
|
||||
return new WarmupResult(
|
||||
success: true,
|
||||
duration: microtime(true) - $startTime,
|
||||
operations: $operations,
|
||||
strategy: 'development',
|
||||
optimizations: [
|
||||
'dependency_validation' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing warmup strategy - fast and reliable
|
||||
*/
|
||||
private function testingWarmup(DefaultContainer $container): WarmupResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$operations = [];
|
||||
|
||||
// Moderate warmup for testing
|
||||
$operations[] = $this->validateDependencies($container);
|
||||
$operations[] = $this->warmCriticalServices($container);
|
||||
|
||||
return new WarmupResult(
|
||||
success: true,
|
||||
duration: microtime(true) - $startTime,
|
||||
operations: $operations,
|
||||
strategy: 'testing',
|
||||
optimizations: [
|
||||
'critical_services_preload' => true,
|
||||
'dependency_validation' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Precompile container for maximum performance
|
||||
*/
|
||||
private function precompileContainer(DefaultContainer $container): WarmupOperation
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$cacheDir = sys_get_temp_dir() . '/framework-cache';
|
||||
$compiledPath = ContainerCompiler::getCompiledContainerPath($cacheDir);
|
||||
|
||||
$reflectionProvider = new CachedReflectionProvider();
|
||||
$dependencyResolver = new DependencyResolver($reflectionProvider, $container);
|
||||
$compiler = new ContainerCompiler($reflectionProvider, $dependencyResolver);
|
||||
|
||||
// Force compilation for production
|
||||
$compiler->compile($container, $compiledPath);
|
||||
|
||||
return new WarmupOperation(
|
||||
name: 'container_compilation',
|
||||
success: true,
|
||||
duration: microtime(true) - $startTime,
|
||||
details: "Container compiled to {$compiledPath}"
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return new WarmupOperation(
|
||||
name: 'container_compilation',
|
||||
success: false,
|
||||
duration: microtime(true) - $startTime,
|
||||
error: $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm up critical services by instantiating them
|
||||
*/
|
||||
private function warmCriticalServices(DefaultContainer $container): WarmupOperation
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$warmedServices = [];
|
||||
|
||||
$criticalServices = [
|
||||
'App\\Framework\\Logging\\Logger',
|
||||
'App\\Framework\\Cache\\Cache',
|
||||
'App\\Framework\\Http\\ResponseEmitter',
|
||||
'App\\Framework\\DateTime\\Clock',
|
||||
];
|
||||
|
||||
try {
|
||||
foreach ($criticalServices as $service) {
|
||||
if ($container->has($service)) {
|
||||
$container->get($service);
|
||||
$warmedServices[] = $service;
|
||||
}
|
||||
}
|
||||
|
||||
return new WarmupOperation(
|
||||
name: 'critical_services_warmup',
|
||||
success: true,
|
||||
duration: microtime(true) - $startTime,
|
||||
details: 'Warmed ' . count($warmedServices) . ' critical services'
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return new WarmupOperation(
|
||||
name: 'critical_services_warmup',
|
||||
success: false,
|
||||
duration: microtime(true) - $startTime,
|
||||
error: $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime reflection cache for faster dependency resolution
|
||||
*/
|
||||
private function primeReflectionCache(DefaultContainer $container): WarmupOperation
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$reflectionProvider = new CachedReflectionProvider();
|
||||
|
||||
// Prime cache with common framework classes
|
||||
$commonClasses = [
|
||||
'App\\Framework\\Core\\Application',
|
||||
'App\\Framework\\Http\\MiddlewareManager',
|
||||
'App\\Framework\\Core\\Events\\EventDispatcher',
|
||||
'App\\Framework\\Discovery\\UnifiedDiscoveryService',
|
||||
];
|
||||
|
||||
$primedCount = 0;
|
||||
foreach ($commonClasses as $class) {
|
||||
if (class_exists($class)) {
|
||||
$reflectionProvider->getClass($class);
|
||||
$primedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return new WarmupOperation(
|
||||
name: 'reflection_cache_prime',
|
||||
success: true,
|
||||
duration: microtime(true) - $startTime,
|
||||
details: "Primed {$primedCount} reflection cache entries"
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return new WarmupOperation(
|
||||
name: 'reflection_cache_prime',
|
||||
success: false,
|
||||
duration: microtime(true) - $startTime,
|
||||
error: $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all container dependencies
|
||||
*/
|
||||
private function validateDependencies(DefaultContainer $container): WarmupOperation
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$reflectionProvider = new CachedReflectionProvider();
|
||||
$dependencyResolver = new DependencyResolver($reflectionProvider, $container);
|
||||
|
||||
// This will validate all bindings and their dependencies
|
||||
$stats = $dependencyResolver->getCacheStats();
|
||||
|
||||
return new WarmupOperation(
|
||||
name: 'dependency_validation',
|
||||
success: true,
|
||||
duration: microtime(true) - $startTime,
|
||||
details: "Validated dependencies: {$stats['cache_hits']} hits, {$stats['cache_misses']} misses"
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return new WarmupOperation(
|
||||
name: 'dependency_validation',
|
||||
success: false,
|
||||
duration: microtime(true) - $startTime,
|
||||
error: $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warmup Result Value Object
|
||||
*/
|
||||
final readonly class WarmupResult
|
||||
{
|
||||
/**
|
||||
* @param array<WarmupOperation> $operations
|
||||
* @param array<string, bool> $optimizations
|
||||
*/
|
||||
public function __construct(
|
||||
public bool $success,
|
||||
public float $duration,
|
||||
public array $operations,
|
||||
public string $strategy,
|
||||
public array $optimizations = [],
|
||||
public ?string $error = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSuccessfulOperations(): array
|
||||
{
|
||||
return array_filter($this->operations, fn ($op) => $op->success);
|
||||
}
|
||||
|
||||
public function getFailedOperations(): array
|
||||
{
|
||||
return array_filter($this->operations, fn ($op) => ! $op->success);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->success,
|
||||
'duration' => $this->duration,
|
||||
'strategy' => $this->strategy,
|
||||
'operations' => array_map(fn ($op) => $op->toArray(), $this->operations),
|
||||
'optimizations' => $this->optimizations,
|
||||
'error' => $this->error,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warmup Operation Value Object
|
||||
*/
|
||||
final readonly class WarmupOperation
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public bool $success,
|
||||
public float $duration,
|
||||
public ?string $details = null,
|
||||
public ?string $error = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'success' => $this->success,
|
||||
'duration' => $this->duration,
|
||||
'details' => $this->details,
|
||||
'error' => $this->error,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user