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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
interface ApplicationInterface
{
public function run(): void;
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
interface AttributeCompiler

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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
*/

View File

@@ -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');;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View 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;
}
}

View 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);
}
}

View File

@@ -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>";
}
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\Events;
class AfterEmitResponse
final readonly class AfterEmitResponse
{
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\Events;
class AfterHandleRequest
final readonly class AfterHandleRequest
{
}

View File

@@ -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()
) {}
) {
}
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\Events;
class BeforeEmitResponse
final readonly class BeforeEmitResponse
{
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\Events;
class BeforeHandleRequest
final readonly class BeforeHandleRequest
{
}

View File

@@ -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()
) {}
) {
}
}

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View 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;
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core\Events;

View File

@@ -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,
],
];
}

View File

@@ -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
) {}
) {
}
}

View File

@@ -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()
) {}
) {
}
}

View File

@@ -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
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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

View File

@@ -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,
];
}

View File

@@ -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, '\\');
}
}

View 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,
];
}
}

View File

@@ -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,
];
}
}

View File

@@ -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 = [];

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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] ?? [];
}
}

View File

@@ -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,
];
}
}

View File

@@ -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();
}
}

View 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);
}
}

View 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"),
};
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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);
}
}

View 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)');
}
}
}

View 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);
}
}

View 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');
}
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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

View 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);
}
}

View 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
};
}
}

View File

@@ -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';
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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"),
};
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View File

@@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
interface ViewModel
{
}

View 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,
];
}
}