From bfce93ce775a21b41b5d5474ea9da974d8ae692f Mon Sep 17 00:00:00 2001 From: Michael Schiemer Date: Tue, 4 Nov 2025 13:44:27 +0100 Subject: [PATCH] refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives --- phpstan-baseline.neon | 36 +- phpstan-baseline.neon.backup | 36 +- resources/js/core/logger-next.js | 277 --------- .../populate_images_from_filesystem.php | 9 +- src/Application/Admin/ShowImageUpload.php | 4 +- .../Api/Images/ImageApiController.php | 2 +- .../Api/Images/UploadImageController.php | 2 +- src/Domain/Media/Image.php | 2 +- src/Domain/Media/ImageResizer.php | 3 - src/Domain/SmartLink/ValueObjects/ClickId.php | 2 +- .../SmartLink/ValueObjects/GeoRuleId.php | 2 +- .../SmartLink/ValueObjects/SmartLinkId.php | 2 +- .../PerformanceBasedAnalyticsStorage.php | 2 +- src/Framework/Audit/ValueObjects/AuditId.php | 2 +- src/Framework/Config/EnvKey.php | 1 + src/Framework/Console/CliSapi.php | 149 +++++ .../Console/Components/ConsoleDialog.php | 548 ++++++++++++++++++ .../Components/DialogCommandExecutor.php | 69 +++ src/Framework/Console/Components/Table.php | 2 +- .../Console/Components/TreeHelper.php | 16 +- src/Framework/Console/ConsoleApplication.php | 67 +++ src/Framework/Console/Result/TableResult.php | 10 +- src/Framework/Console/TerminalDetector.php | 90 +++ .../Console/ValueObjects/TerminalStream.php | 143 +++++ .../Core/System/Ini/IniDirective.php | 15 +- src/Framework/Core/System/Ini/IniKey.php | 14 +- src/Framework/Core/System/Ini/IniManager.php | 158 +++++ src/Framework/Core/System/PhpIni.php | 30 - src/Framework/Core/System/SystemConfig.php | 26 + .../Database/Migration/MigrationRunner.php | 2 +- .../Database/Services/EntityPersister.php | 2 +- .../Database/TypeCaster/UlidCaster.php | 2 +- src/Framework/ErrorAggregation/ErrorEvent.php | 2 +- .../ErrorAggregation/ErrorPattern.php | 2 +- .../Storage/DatabaseErrorStorage.php | 32 +- .../Middleware/ErrorBoundaryMiddleware.php | 4 +- src/Framework/Filesystem/Directory.php | 37 +- .../Filesystem/FilesystemFactory.php | 59 +- src/Framework/Filesystem/InMemoryStorage.php | 6 +- src/Framework/Filesystem/LoggableStorage.php | 55 +- src/Framework/Filesystem/StorageTrait.php | 6 +- .../Filesystem/Traits/AtomicStorageTrait.php | 2 +- .../Filesystem/ValueObjects/FilePath.php | 2 +- .../Id/Contracts/IdGeneratorInterface.php | 31 + src/Framework/Id/Contracts/IdInterface.php | 26 + src/Framework/{ => Id}/Cuid/Cuid.php | 12 +- src/Framework/{ => Id}/Cuid/CuidGenerator.php | 5 +- src/Framework/Id/IdGeneratorFactory.php | 91 +++ src/Framework/Id/IdType.php | 56 ++ src/Framework/{ => Id}/Ksuid/Ksuid.php | 12 +- .../{ => Id}/Ksuid/KsuidGenerator.php | 7 +- src/Framework/{ => Id}/NanoId/NanoId.php | 12 +- .../{ => Id}/NanoId/NanoIdGenerator.php | 5 +- .../{ => Id}/Ulid/StringConverter.php | 2 +- src/Framework/{ => Id}/Ulid/Ulid.php | 33 +- src/Framework/{ => Id}/Ulid/UlidGenerator.php | 35 +- src/Framework/{ => Id}/Ulid/UlidParser.php | 2 +- src/Framework/{ => Id}/Ulid/UlidValidator.php | 2 +- src/Framework/Id/UnifiedIdGenerator.php | 121 ++++ .../Formatter/DevelopmentFormatter.php | 39 +- .../Logging/Formatter/LineFormatter.php | 43 +- src/Framework/Logging/HandlerFactory.php | 190 ++++++ .../Logging/Handlers/ConsoleHandler.php | 100 +++- src/Framework/Logging/LoggerFactory.php | 61 -- src/Framework/Logging/LoggerInitializer.php | 132 +---- src/Framework/Logging/QueueFactory.php | 39 ++ .../Logging/ValueObjects/CorrelationId.php | 2 +- .../CachePerformanceStorage.php | 3 +- .../Services/CacheMagicLinkService.php | 2 +- .../Services/InMemoryMagicLinkService.php | 2 +- src/Framework/Mail/SmtpTransport.php | 8 +- src/Framework/Mail/Testing/MockTransport.php | 2 +- .../Notification/Templates/TemplateId.php | 2 +- .../ValueObjects/NotificationId.php | 4 +- .../Queue/Entities/DeadLetterJob.php | 2 +- .../Queue/Entities/JobChainEntry.php | 2 +- .../Queue/Entities/JobDependencyEntry.php | 2 +- .../Queue/Entities/JobMetricsEntry.php | 2 +- .../Queue/Entities/JobProgressEntry.php | 2 +- src/Framework/Queue/FileQueue.php | 4 +- .../QueueJobFeatureExtractor.php | 2 +- .../Services/DatabaseJobBatchManager.php | 2 +- src/Framework/Queue/ValueObjects/JobId.php | 2 +- .../Queue/ValueObjects/JobMetadata.php | 2 +- src/Framework/Queue/ValueObjects/WorkerId.php | 2 +- .../Exporters/ConsoleTraceExporter.php | 2 +- .../Exporters/DatabaseTraceExporter.php | 12 +- .../Tracing/Exporters/JaegerExporter.php | 2 +- .../TypeCaster/Casters/UlidCaster.php | 2 +- src/Framework/Validation/Rules/Ulid.php | 2 +- src/Framework/Vault/DatabaseVault.php | 2 +- tests/Feature/ImageApiControllerTest.php | 2 +- tests/Feature/ImageSystemTest.php | 2 +- tests/Feature/ShowImageControllerTest.php | 2 +- .../Queue/ValueObjects/JobIdTest.php | 2 +- .../Framework/Core/System/Ini/AccessTest.php | 44 ++ .../Core/System/Ini/IniDirectiveTest.php | 95 +++ .../Framework/Core/System/Ini/IniKeyTest.php | 70 +++ .../Core/System/Ini/IniManagerTest.php | 165 ++++++ .../Core/System/SystemConfigTest.php | 113 ++++ .../Unit/Framework/Cuid/CuidGeneratorTest.php | 4 +- tests/Unit/Framework/Cuid/CuidTest.php | 2 +- .../Framework/Ksuid/KsuidGeneratorTest.php | 4 +- tests/Unit/Framework/Ksuid/KsuidTest.php | 2 +- .../Framework/Logging/CorrelationIdTest.php | 2 +- .../Services/InMemoryMagicLinkServiceTest.php | 2 +- .../Framework/NanoId/NanoIdGeneratorTest.php | 4 +- tests/Unit/Framework/NanoId/NanoIdTest.php | 2 +- tests/debug/test-aggregator-internals.php | 10 +- .../debug/test-queue-anomaly-integration.php | 16 +- 110 files changed, 2828 insertions(+), 774 deletions(-) delete mode 100644 resources/js/core/logger-next.js create mode 100644 src/Framework/Console/CliSapi.php create mode 100644 src/Framework/Console/Components/ConsoleDialog.php create mode 100644 src/Framework/Console/Components/DialogCommandExecutor.php create mode 100644 src/Framework/Console/TerminalDetector.php create mode 100644 src/Framework/Console/ValueObjects/TerminalStream.php create mode 100644 src/Framework/Core/System/Ini/IniManager.php delete mode 100644 src/Framework/Core/System/PhpIni.php create mode 100644 src/Framework/Core/System/SystemConfig.php create mode 100644 src/Framework/Id/Contracts/IdGeneratorInterface.php create mode 100644 src/Framework/Id/Contracts/IdInterface.php rename src/Framework/{ => Id}/Cuid/Cuid.php (96%) rename src/Framework/{ => Id}/Cuid/CuidGenerator.php (98%) create mode 100644 src/Framework/Id/IdGeneratorFactory.php create mode 100644 src/Framework/Id/IdType.php rename src/Framework/{ => Id}/Ksuid/Ksuid.php (96%) rename src/Framework/{ => Id}/Ksuid/KsuidGenerator.php (95%) rename src/Framework/{ => Id}/NanoId/NanoId.php (93%) rename src/Framework/{ => Id}/NanoId/NanoIdGenerator.php (97%) rename src/Framework/{ => Id}/Ulid/StringConverter.php (97%) rename src/Framework/{ => Id}/Ulid/Ulid.php (78%) rename src/Framework/{ => Id}/Ulid/UlidGenerator.php (69%) rename src/Framework/{ => Id}/Ulid/UlidParser.php (98%) rename src/Framework/{ => Id}/Ulid/UlidValidator.php (91%) create mode 100644 src/Framework/Id/UnifiedIdGenerator.php create mode 100644 src/Framework/Logging/HandlerFactory.php delete mode 100644 src/Framework/Logging/LoggerFactory.php create mode 100644 src/Framework/Logging/QueueFactory.php create mode 100644 tests/Unit/Framework/Core/System/Ini/AccessTest.php create mode 100644 tests/Unit/Framework/Core/System/Ini/IniDirectiveTest.php create mode 100644 tests/Unit/Framework/Core/System/Ini/IniKeyTest.php create mode 100644 tests/Unit/Framework/Core/System/Ini/IniManagerTest.php create mode 100644 tests/Unit/Framework/Core/System/SystemConfigTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6adc07fd..f2b7ae39 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -14426,7 +14426,7 @@ parameters: path: src/Framework/ErrorAggregation/Alerting/AlertManager.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 3 path: src/Framework/ErrorAggregation/Alerting/AlertManager.php @@ -14618,7 +14618,7 @@ parameters: path: src/Framework/ErrorAggregation/Alerting/AlertManager.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 4 path: src/Framework/ErrorAggregation/Alerting/EmailAlertChannel.php @@ -14799,7 +14799,7 @@ parameters: path: src/Framework/ErrorAggregation/ErrorAggregator.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 5 path: src/Framework/ErrorAggregation/ErrorAggregator.php @@ -14930,13 +14930,13 @@ parameters: path: src/Framework/ErrorAggregation/ErrorAggregator.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorEvent.php - - message: '#^Call to an undefined static method App\\Framework\\Ulid\\Ulid\:\:generate\(\)\.$#' + message: '#^Call to an undefined static method App\\Framework\\Id\\Ulid\\Ulid\:\:generate\(\)\.$#' identifier: staticMethod.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorEvent.php @@ -15001,19 +15001,19 @@ parameters: path: src/Framework/ErrorAggregation/ErrorEvent.php - - message: '#^Static method App\\Framework\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' + message: '#^Static method App\\Framework\\Id\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' identifier: arguments.count count: 1 path: src/Framework/ErrorAggregation/ErrorEvent.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorPattern.php - - message: '#^Call to an undefined static method App\\Framework\\Ulid\\Ulid\:\:generate\(\)\.$#' + message: '#^Call to an undefined static method App\\Framework\\Id\\Ulid\\Ulid\:\:generate\(\)\.$#' identifier: staticMethod.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorPattern.php @@ -15054,13 +15054,13 @@ parameters: path: src/Framework/ErrorAggregation/ErrorPattern.php - - message: '#^Static method App\\Framework\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' + message: '#^Static method App\\Framework\\Id\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' identifier: arguments.count count: 1 path: src/Framework/ErrorAggregation/ErrorPattern.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 2 path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php @@ -15149,7 +15149,7 @@ parameters: path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php - - message: '#^Static method App\\Framework\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' + message: '#^Static method App\\Framework\\Id\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' identifier: arguments.count count: 2 path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php @@ -26703,43 +26703,43 @@ parameters: path: src/Framework/Ulid/StringConverter.php - - message: '#^Class App\\Framework\\Ulid\\UlidGenerator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDGenerator\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidGenerator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDGenerator\.$#' identifier: class.nameCase count: 1 path: src/Framework/Ulid/Ulid.php - - message: '#^Class App\\Framework\\Ulid\\UlidParser referenced with incorrect case\: App\\Framework\\Ulid\\ULIDParser\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidParser referenced with incorrect case\: App\\Framework\\Ulid\\ULIDParser\.$#' identifier: class.nameCase count: 2 path: src/Framework/Ulid/Ulid.php - - message: '#^Class App\\Framework\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' identifier: class.nameCase count: 4 path: src/Framework/Ulid/Ulid.php - - message: '#^Method App\\Framework\\Ulid\\Ulid\:\:__debugInfo\(\) never returns null so it can be removed from the return type\.$#' + message: '#^Method App\\Framework\\Id\\Ulid\\Ulid\:\:__debugInfo\(\) never returns null so it can be removed from the return type\.$#' identifier: return.unusedType count: 1 path: src/Framework/Ulid/Ulid.php - - message: '#^Method App\\Framework\\Ulid\\Ulid\:\:__debugInfo\(\) return type has no value type specified in iterable type array\.$#' + message: '#^Method App\\Framework\\Id\\Ulid\\Ulid\:\:__debugInfo\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/Framework/Ulid/Ulid.php - - message: '#^Class App\\Framework\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' identifier: class.nameCase count: 2 path: src/Framework/Ulid/UlidParser.php - - message: '#^Method App\\Framework\\Ulid\\UlidParser\:\:getTimestampMs\(\) should return int but returns float\|int\.$#' + message: '#^Method App\\Framework\\Id\\Ulid\\UlidParser\:\:getTimestampMs\(\) should return int but returns float\|int\.$#' identifier: return.type count: 1 path: src/Framework/Ulid/UlidParser.php diff --git a/phpstan-baseline.neon.backup b/phpstan-baseline.neon.backup index d969ccaf..86b2266e 100644 --- a/phpstan-baseline.neon.backup +++ b/phpstan-baseline.neon.backup @@ -9775,7 +9775,7 @@ parameters: path: src/Framework/ErrorAggregation/Alerting/AlertManager.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 3 path: src/Framework/ErrorAggregation/Alerting/AlertManager.php @@ -9943,7 +9943,7 @@ parameters: path: src/Framework/ErrorAggregation/Alerting/AlertManager.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 4 path: src/Framework/ErrorAggregation/Alerting/EmailAlertChannel.php @@ -10124,7 +10124,7 @@ parameters: path: src/Framework/ErrorAggregation/ErrorAggregator.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 5 path: src/Framework/ErrorAggregation/ErrorAggregator.php @@ -10237,13 +10237,13 @@ parameters: path: src/Framework/ErrorAggregation/ErrorAggregator.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorEvent.php - - message: '#^Call to an undefined static method App\\Framework\\Ulid\\Ulid\:\:generate\(\)\.$#' + message: '#^Call to an undefined static method App\\Framework\\Id\\Ulid\\Ulid\:\:generate\(\)\.$#' identifier: staticMethod.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorEvent.php @@ -10302,19 +10302,19 @@ parameters: path: src/Framework/ErrorAggregation/ErrorEvent.php - - message: '#^Static method App\\Framework\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' + message: '#^Static method App\\Framework\\Id\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' identifier: arguments.count count: 1 path: src/Framework/ErrorAggregation/ErrorEvent.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorPattern.php - - message: '#^Call to an undefined static method App\\Framework\\Ulid\\Ulid\:\:generate\(\)\.$#' + message: '#^Call to an undefined static method App\\Framework\\Id\\Ulid\\Ulid\:\:generate\(\)\.$#' identifier: staticMethod.notFound count: 1 path: src/Framework/ErrorAggregation/ErrorPattern.php @@ -10355,13 +10355,13 @@ parameters: path: src/Framework/ErrorAggregation/ErrorPattern.php - - message: '#^Static method App\\Framework\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' + message: '#^Static method App\\Framework\\Id\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' identifier: arguments.count count: 1 path: src/Framework/ErrorAggregation/ErrorPattern.php - - message: '#^Call to an undefined method App\\Framework\\Ulid\\Ulid\:\:toString\(\)\.$#' + message: '#^Call to an undefined method App\\Framework\\Id\\Ulid\\Ulid\:\:toString\(\)\.$#' identifier: method.notFound count: 2 path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php @@ -10450,7 +10450,7 @@ parameters: path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php - - message: '#^Static method App\\Framework\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' + message: '#^Static method App\\Framework\\Id\\Ulid\\Ulid\:\:fromString\(\) invoked with 1 parameter, 2 required\.$#' identifier: arguments.count count: 2 path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php @@ -18231,43 +18231,43 @@ parameters: path: src/Framework/Ulid/StringConverter.php - - message: '#^Class App\\Framework\\Ulid\\UlidGenerator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDGenerator\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidGenerator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDGenerator\.$#' identifier: class.nameCase count: 1 path: src/Framework/Ulid/Ulid.php - - message: '#^Class App\\Framework\\Ulid\\UlidParser referenced with incorrect case\: App\\Framework\\Ulid\\ULIDParser\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidParser referenced with incorrect case\: App\\Framework\\Ulid\\ULIDParser\.$#' identifier: class.nameCase count: 2 path: src/Framework/Ulid/Ulid.php - - message: '#^Class App\\Framework\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' identifier: class.nameCase count: 4 path: src/Framework/Ulid/Ulid.php - - message: '#^Method App\\Framework\\Ulid\\Ulid\:\:__debugInfo\(\) never returns null so it can be removed from the return type\.$#' + message: '#^Method App\\Framework\\Id\\Ulid\\Ulid\:\:__debugInfo\(\) never returns null so it can be removed from the return type\.$#' identifier: return.unusedType count: 1 path: src/Framework/Ulid/Ulid.php - - message: '#^Method App\\Framework\\Ulid\\Ulid\:\:__debugInfo\(\) return type has no value type specified in iterable type array\.$#' + message: '#^Method App\\Framework\\Id\\Ulid\\Ulid\:\:__debugInfo\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/Framework/Ulid/Ulid.php - - message: '#^Class App\\Framework\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' + message: '#^Class App\\Framework\\Id\\Ulid\\UlidValidator referenced with incorrect case\: App\\Framework\\Ulid\\ULIDValidator\.$#' identifier: class.nameCase count: 2 path: src/Framework/Ulid/UlidParser.php - - message: '#^Method App\\Framework\\Ulid\\UlidParser\:\:getTimestampMs\(\) should return int but returns float\|int\.$#' + message: '#^Method App\\Framework\\Id\\Ulid\\UlidParser\:\:getTimestampMs\(\) should return int but returns float\|int\.$#' identifier: return.type count: 1 path: src/Framework/Ulid/UlidParser.php diff --git a/resources/js/core/logger-next.js b/resources/js/core/logger-next.js deleted file mode 100644 index 9a55d239..00000000 --- a/resources/js/core/logger-next.js +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Erweiterter Logger mit Processor-Architektur, Request-ID-Unterstützung und Server-Kommunikation. - */ -export class Logger { - /** - * Konfiguration des Loggers - */ - static config = { - enabled: true, - apiEndpoint: '/api/log', - consoleEnabled: true, - serverEnabled: true, - minLevel: 'debug', - }; - - /** - * Liste der registrierten Processors - */ - static processors = []; - - /** - * Registrierte Handler - */ - static handlers = []; - - /** - * Aktive RequestID - */ - static requestId = null; - - /** - * Logger initialisieren - */ - static initialize(config = {}) { - // Konfiguration überschreiben - this.config = { ...this.config, ...config }; - - // Standard-Processors registrieren - this.registerProcessor(this.requestIdProcessor); - this.registerProcessor(this.timestampProcessor); - - // Standard-Handler registrieren - if (this.config.consoleEnabled) { - this.registerHandler(this.consoleHandler); - } - - if (this.config.serverEnabled) { - this.registerHandler(this.serverHandler); - } - - // Request-ID aus dem Document laden, wenn vorhanden - if (typeof document !== 'undefined') { - this.initFromDocument(); - } - - // Unhandled Errors abfangen - this.setupErrorHandling(); - } - - /** - * Debug-Nachricht loggen - */ - static debug(...args) { - this.log('debug', ...args); - } - - /** - * Info-Nachricht loggen - */ - static info(...args) { - this.log('info', ...args); - } - - /** - * Warnungs-Nachricht loggen - */ - static warn(...args) { - this.log('warn', ...args); - } - - /** - * Fehler-Nachricht loggen - */ - static error(...args) { - this.log('error', ...args); - } - - /** - * Log-Nachricht mit beliebigem Level erstellen - */ - static log(level, ...args) { - if (!this.config.enabled) return; - - // Level-Validierung - const validLevels = ['debug', 'info', 'warn', 'error']; - if (!validLevels.includes(level)) { - level = 'info'; - } - - // Nachricht und Kontext extrahieren - const message = this.formatMessage(args); - const context = args.find(arg => typeof arg === 'object' && arg !== null && !(arg instanceof Error)) || {}; - - // Exception extrahieren, falls vorhanden - const error = args.find(arg => arg instanceof Error); - if (error) { - context.exception = error; - } - - // Log-Record erstellen - let record = { - level, - message, - context, - timestamp: new Date(), - extra: {}, - }; - - // Alle Processors durchlaufen - this.processors.forEach(processor => { - record = processor(record); - }); - - // Log-Level-Prüfung (nach Processors, da sie das Level ändern könnten) - const levelPriority = { - debug: 100, - info: 200, - warn: 300, - error: 400, - }; - - if (levelPriority[record.level] < levelPriority[this.config.minLevel]) { - return; - } - - // Alle Handler durchlaufen - this.handlers.forEach(handler => { - handler(record); - }); - } - - /** - * Nachricht aus verschiedenen Argumenten formatieren - */ - static formatMessage(args) { - return args - .filter(arg => !(arg instanceof Error) && (typeof arg !== 'object' || arg === null)) - .map(arg => String(arg)) - .join(' '); - } - - /** - * Processor registrieren - */ - static registerProcessor(processor) { - if (typeof processor !== 'function') return; - this.processors.push(processor); - } - - /** - * Handler registrieren - */ - static registerHandler(handler) { - if (typeof handler !== 'function') return; - this.handlers.push(handler); - } - - /** - * Error-Handling-Setup - */ - static setupErrorHandling() { - if (typeof window !== 'undefined') { - // Unbehandelte Fehler abfangen - window.addEventListener('error', (event) => { - this.error('Unbehandelter Fehler:', event.error || event.message); - }); - - // Unbehandelte Promise-Rejects abfangen - window.addEventListener('unhandledrejection', (event) => { - this.error('Unbehandelte Promise-Ablehnung:', event.reason); - }); - } - } - - /** - * Request-ID aus dem Document laden - */ - static initFromDocument() { - const meta = document.querySelector('meta[name="request-id"]'); - if (meta) { - const fullRequestId = meta.getAttribute('content'); - // Nur den ID-Teil ohne Signatur verwenden - this.requestId = fullRequestId.split('.')[0] || null; - } - } - - /*** STANDARD-PROCESSORS ***/ - - /** - * Processor für Request-ID - */ - static requestIdProcessor(record) { - if (Logger.requestId) { - record.extra.request_id = Logger.requestId; - } - return record; - } - - /** - * Processor für Timestamp-Formatierung - */ - static timestampProcessor(record) { - record.formattedTimestamp = record.timestamp.toLocaleTimeString('de-DE'); - return record; - } - - /*** STANDARD-HANDLERS ***/ - - /** - * Handler für Console-Ausgabe - */ - static consoleHandler(record) { - const levelColors = { - debug: 'color: gray', - info: 'color: green', - warn: 'color: orange', - error: 'color: red', - }; - - const color = levelColors[record.level] || 'color: black'; - const requestIdStr = record.extra.request_id ? `[${record.extra.request_id}] ` : ''; - const formattedMessage = `[${record.formattedTimestamp}] [${record.level.toUpperCase()}] ${requestIdStr}${record.message}`; - - // Farbige Ausgabe in der Konsole - console[record.level]( - `%c${formattedMessage}`, - color, - ...(record.context ? [record.context] : []) - ); - } - - /** - * Handler für Server-Kommunikation - */ - static serverHandler(record) { - fetch(Logger.config.apiEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Request-ID': Logger.requestId || '' - }, - body: JSON.stringify({ - level: record.level, - message: record.message, - context: record.context || {} - }) - }) - .then(response => { - // Request-ID aus dem Header extrahieren - const requestId = response.headers.get('X-Request-ID'); - if (requestId) { - // Nur den ID-Teil ohne Signatur speichern - const idPart = requestId.split('.')[0]; - if (idPart) { - Logger.requestId = idPart; - } - } - return response.json(); - }) - .catch(() => { - // Fehler beim Senden des Logs ignorieren (keine rekursive Fehlerbehandlung) - }); - } -} - -// Standard-Initialisierung -Logger.initialize(); diff --git a/scripts/maintenance/populate_images_from_filesystem.php b/scripts/maintenance/populate_images_from_filesystem.php index a839cf0e..52f47e90 100644 --- a/scripts/maintenance/populate_images_from_filesystem.php +++ b/scripts/maintenance/populate_images_from_filesystem.php @@ -11,15 +11,8 @@ declare(strict_types=1); require_once 'vendor/autoload.php'; -use App\Domain\Media\Image; -use App\Framework\Core\ValueObjects\FileSize; -use App\Framework\Core\ValueObjects\Hash; -use App\Framework\Database\DatabaseManager; -use App\Framework\Filesystem\ValueObjects\FilePath; -use App\Framework\Http\MimeType; -use App\Framework\Ulid\Ulid; -use App\Framework\Ulid\UlidGenerator; use App\Framework\DateTime\SystemClock; +use App\Framework\Id\Ulid\UlidGenerator; class ImageMigrationScript { diff --git a/src/Application/Admin/ShowImageUpload.php b/src/Application/Admin/ShowImageUpload.php index bb4533ca..e49e1793 100644 --- a/src/Application/Admin/ShowImageUpload.php +++ b/src/Application/Admin/ShowImageUpload.php @@ -16,10 +16,10 @@ use App\Framework\Http\Method; use App\Framework\Http\Request; use App\Framework\Http\Session\FormIdGenerator; use App\Framework\Http\UploadedFile; +use App\Framework\Id\Ulid\StringConverter; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Meta\MetaData; use App\Framework\Router\Result\ViewResult; -use App\Framework\Ulid\StringConverter; -use App\Framework\Ulid\Ulid; use App\Framework\View\FormBuilder; use App\Framework\View\RawHtml; diff --git a/src/Application/Api/Images/ImageApiController.php b/src/Application/Api/Images/ImageApiController.php index 36ef9b78..0bba5850 100644 --- a/src/Application/Api/Images/ImageApiController.php +++ b/src/Application/Api/Images/ImageApiController.php @@ -17,8 +17,8 @@ use App\Framework\Http\Exception\NotFound; use App\Framework\Http\HttpRequest; use App\Framework\Http\Method; use App\Framework\Http\Responses\JsonResponse; +use App\Framework\Id\Ulid\UlidGenerator; use App\Framework\Router\Result\FileResult; -use App\Framework\Ulid\UlidGenerator; final readonly class ImageApiController { diff --git a/src/Application/Api/Images/UploadImageController.php b/src/Application/Api/Images/UploadImageController.php index ab18b82c..0d4f62a2 100644 --- a/src/Application/Api/Images/UploadImageController.php +++ b/src/Application/Api/Images/UploadImageController.php @@ -21,7 +21,7 @@ use App\Framework\Http\Request; use App\Framework\Http\Responses\JsonResponse; use App\Framework\Http\Status; use App\Framework\Http\UploadedFile; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; final readonly class UploadImageController { diff --git a/src/Domain/Media/Image.php b/src/Domain/Media/Image.php index 4a356c6d..9528e367 100644 --- a/src/Domain/Media/Image.php +++ b/src/Domain/Media/Image.php @@ -12,7 +12,7 @@ use App\Framework\Database\Attributes\Entity; use App\Framework\Database\Attributes\Type; use App\Framework\Filesystem\ValueObjects\FilePath; use App\Framework\Http\MimeType; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; #[Entity(tableName: 'images', idColumn: 'ulid')] final readonly class Image diff --git a/src/Domain/Media/ImageResizer.php b/src/Domain/Media/ImageResizer.php index fbf135ab..64776dbe 100644 --- a/src/Domain/Media/ImageResizer.php +++ b/src/Domain/Media/ImageResizer.php @@ -4,9 +4,6 @@ declare(strict_types=1); namespace App\Domain\Media; -use App\Framework\Ulid\StringConverter; -use App\Framework\Ulid\Ulid; - final readonly class ImageResizer { public function __construct() diff --git a/src/Domain/SmartLink/ValueObjects/ClickId.php b/src/Domain/SmartLink/ValueObjects/ClickId.php index 92431161..c4e73ba9 100644 --- a/src/Domain/SmartLink/ValueObjects/ClickId.php +++ b/src/Domain/SmartLink/ValueObjects/ClickId.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Domain\SmartLink\ValueObjects; use App\Framework\DateTime\Clock; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; final readonly class ClickId { diff --git a/src/Domain/SmartLink/ValueObjects/GeoRuleId.php b/src/Domain/SmartLink/ValueObjects/GeoRuleId.php index 57e5eb1e..18cfd7dc 100644 --- a/src/Domain/SmartLink/ValueObjects/GeoRuleId.php +++ b/src/Domain/SmartLink/ValueObjects/GeoRuleId.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Domain\SmartLink\ValueObjects; use App\Framework\DateTime\Clock; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; final readonly class GeoRuleId { diff --git a/src/Domain/SmartLink/ValueObjects/SmartLinkId.php b/src/Domain/SmartLink/ValueObjects/SmartLinkId.php index 122018e1..7e7a52d5 100644 --- a/src/Domain/SmartLink/ValueObjects/SmartLinkId.php +++ b/src/Domain/SmartLink/ValueObjects/SmartLinkId.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Domain\SmartLink\ValueObjects; use App\Framework\DateTime\Clock; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; final readonly class SmartLinkId { diff --git a/src/Framework/Analytics/Storage/PerformanceBasedAnalyticsStorage.php b/src/Framework/Analytics/Storage/PerformanceBasedAnalyticsStorage.php index 9c11a04b..6e8ddd61 100644 --- a/src/Framework/Analytics/Storage/PerformanceBasedAnalyticsStorage.php +++ b/src/Framework/Analytics/Storage/PerformanceBasedAnalyticsStorage.php @@ -285,7 +285,7 @@ final class PerformanceBasedAnalyticsStorage implements AnalyticsStorage return; } - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $filename = $this->dataPath . '/raw_' . date('Y-m-d_H-i-s') . '_' . $generator->generate() . '.' . $this->serializer->getFileExtension(); $content = $this->serializer->serialize($this->rawDataBuffer); diff --git a/src/Framework/Audit/ValueObjects/AuditId.php b/src/Framework/Audit/ValueObjects/AuditId.php index 722d781f..46ea9627 100644 --- a/src/Framework/Audit/ValueObjects/AuditId.php +++ b/src/Framework/Audit/ValueObjects/AuditId.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Framework\Audit\ValueObjects; use App\Framework\DateTime\Clock; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; /** * Audit Entry ID value object (ULID-based) diff --git a/src/Framework/Config/EnvKey.php b/src/Framework/Config/EnvKey.php index 4d6077c4..58ae0011 100644 --- a/src/Framework/Config/EnvKey.php +++ b/src/Framework/Config/EnvKey.php @@ -17,6 +17,7 @@ enum EnvKey: string // Feature Flags case ENABLE_CONTEXT_AWARE_INITIALIZERS = 'ENABLE_CONTEXT_AWARE_INITIALIZERS'; case MCP_SERVER_MODE = 'MCP_SERVER_MODE'; + case LOG_COLOR_OUTPUT = 'LOG_COLOR_OUTPUT'; // Database case DB_DRIVER = 'DB_DRIVER'; diff --git a/src/Framework/Console/CliSapi.php b/src/Framework/Console/CliSapi.php new file mode 100644 index 00000000..5985fd51 --- /dev/null +++ b/src/Framework/Console/CliSapi.php @@ -0,0 +1,149 @@ +isCli; + } + + /** + * Prüft ob aktueller Kontext Web ist. + */ + public function isWeb(): bool + { + return !$this->isCli; + } + + /** + * Prüft ob ein Stream ein Terminal ist. + * + * Delegiert an TerminalDetector. + * + * @param TerminalStream|null $stream Terminal Stream (oder null für stdout) + * @return bool True wenn Stream ein Terminal ist + */ + public function isTerminal(?TerminalStream $stream = null): bool + { + return TerminalDetector::isTerminal($stream ?? $this->stdout); + } + + /** + * Prüft ob ein Stream ein Terminal ist und Farben unterstützt. + * + * Delegiert an TerminalDetector. + * + * @param TerminalStream|null $stream Terminal Stream (oder null für stdout) + * @return bool True wenn Terminal vorhanden und Farben unterstützt werden + */ + public function supportsColors(?TerminalStream $stream = null): bool + { + return TerminalDetector::supportsColors($stream ?? $this->stdout); + } + + /** + * Prüft ob STDOUT ein Terminal ist. + */ + public function isStdoutTerminal(): bool + { + return $this->isTerminal($this->stdout); + } + + /** + * Prüft ob STDERR ein Terminal ist. + */ + public function isStderrTerminal(): bool + { + return $this->isTerminal($this->stderr); + } + + /** + * Prüft ob STDOUT Farben unterstützt. + */ + public function stdoutSupportsColors(): bool + { + return $this->supportsColors($this->stdout); + } + + /** + * Prüft ob STDERR Farben unterstützt. + */ + public function stderrSupportsColors(): bool + { + return $this->supportsColors($this->stderr); + } +} + diff --git a/src/Framework/Console/Components/ConsoleDialog.php b/src/Framework/Console/Components/ConsoleDialog.php new file mode 100644 index 00000000..77dc845a --- /dev/null +++ b/src/Framework/Console/Components/ConsoleDialog.php @@ -0,0 +1,548 @@ + ' + ) { + $this->readlineAvailable = extension_loaded('readline'); + $this->suggestionEngine = new CommandSuggestionEngine($commandList); + $this->helpGenerator = new CommandHelpGenerator(new ParameterInspector()); + + if ($this->readlineAvailable) { + $this->setupReadline(); + } + } + + /** + * Run the dialog mode + */ + public function run(): ExitCode + { + try { + $this->showWelcome(); + $this->mainLoop(); + } catch (\Throwable $e) { + $this->output->writeError('Fatal error: ' . $e->getMessage()); + if ($this->output->isTerminal()) { + $this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY); + } + + return ExitCode::GENERAL_ERROR; + } finally { + $this->cleanup(); + } + + return ExitCode::SUCCESS; + } + + /** + * Setup readline for history and completion + */ + private function setupReadline(): void + { + // Load history from CommandHistory + $history = $this->commandHistory->getRecentHistory(100); + foreach ($history as $entry) { + if (isset($entry['command'])) { + readline_add_history($entry['command']); + } + } + + // Set completion function + readline_completion_function([$this, 'completeCommand']); + + // Enable completion + readline_info('completion_append_character', ' '); + readline_info('completion_suppress_append', false); + } + + /** + * Readline completion callback + * + * @param string $text + * @param int $start + * @param int $end + * @return array + */ + public function completeCommand(string $text, int $start, int $end): array + { + $line = readline_info('line_buffer'); + $words = explode(' ', $line); + $currentWord = $words[$start] ?? ''; + + // If we're at the first word (command name), suggest commands + if ($start === 0) { + return $this->getCommandSuggestions($currentWord); + } + + // For subsequent words, could implement argument completion + // For now, return empty + return []; + } + + /** + * Get command suggestions for autocomplete + * + * @return array + */ + private function getCommandSuggestions(string $partial): array + { + $suggestions = []; + + // Get suggestions from history first + $historySuggestions = $this->commandHistory->getSuggestions($partial, 10); + foreach ($historySuggestions as $suggestion) { + $suggestions[] = $suggestion['command']; + } + + // Get suggestions from all commands + $commands = $this->commandList->getAllCommands(); + foreach ($commands as $command) { + if (str_starts_with(strtolower($command->name), strtolower($partial))) { + if (! in_array($command->name, $suggestions)) { + $suggestions[] = $command->name; + } + } + } + + return array_unique($suggestions); + } + + /** + * Main dialog loop + */ + private function mainLoop(): void + { + $running = true; + + while ($running) { + // Handle signals + if (function_exists('pcntl_signal_dispatch')) { + pcntl_signal_dispatch(); + } + + // Read input + $input = $this->readInput(); + + // Handle empty input + if ($input === null || trim($input) === '') { + continue; + } + + $input = trim($input); + + // Handle exit commands + if (in_array(strtolower($input), ['exit', 'quit', 'q', ':q'])) { + $this->output->writeLine('Goodbye! 👋', ConsoleColor::CYAN); + break; + } + + // Handle help command + if (in_array(strtolower($input), ['help', '?', ':help', 'h'])) { + $this->showHelp(); + continue; + } + + // Handle help syntax + if (preg_match('/^help\s+(.+)$/i', $input, $matches)) { + $commandName = trim($matches[1]); + $this->showCommandHelp($commandName); + continue; + } + + // Handle history command + if (strtolower($input) === 'history' || strtolower($input) === ':history') { + $this->showHistory(); + continue; + } + + // Handle clear command + if (strtolower($input) === 'clear' || strtolower($input) === ':clear') { + $this->clearScreen(); + continue; + } + + // Parse command and arguments + $parts = $this->parseInput($input); + $commandName = $parts['command']; + $arguments = $parts['arguments']; + + // Check if command exists + if (! $this->commandList->has($commandName)) { + $this->handleCommandNotFound($commandName); + continue; + } + + // Execute command + $exitCode = $this->commandExecutor->executeCommand($commandName, $arguments); + + // Add to readline history if available + if ($this->readlineAvailable) { + readline_add_history($input); + } + + // Show suggestions if command failed + if ($exitCode->value !== 0) { + $this->showContextualHelp($commandName); + } + } + } + + /** + * Read input from user + */ + private function readInput(): ?string + { + if ($this->readlineAvailable) { + $input = readline($this->prompt); + if ($input === false) { + return null; + } + + return $input; + } + + // Fallback to fgets if readline not available + $this->output->write($this->prompt, ConsoleColor::BRIGHT_CYAN); + $input = fgets(STDIN); + if ($input === false) { + return null; + } + + return rtrim($input, "\n\r"); + } + + /** + * Parse input into command and arguments + * + * @return array{command: string, arguments: array} + */ + private function parseInput(string $input): array + { + // Handle quoted strings + $parts = []; + $current = ''; + $inQuotes = false; + $quoteChar = ''; + + for ($i = 0; $i < strlen($input); $i++) { + $char = $input[$i]; + + if (($char === '"' || $char === "'") && ($i === 0 || $input[$i - 1] !== '\\')) { + if ($inQuotes && $char === $quoteChar) { + $inQuotes = false; + $quoteChar = ''; + } elseif (! $inQuotes) { + $inQuotes = true; + $quoteChar = $char; + } + } elseif ($char === ' ' && ! $inQuotes) { + if ($current !== '') { + $parts[] = $current; + $current = ''; + } + } else { + $current .= $char; + } + } + + if ($current !== '') { + $parts[] = $current; + } + + if (empty($parts)) { + return ['command' => '', 'arguments' => []]; + } + + return [ + 'command' => $parts[0], + 'arguments' => array_slice($parts, 1), + ]; + } + + /** + * Handle command not found + */ + private function handleCommandNotFound(string $commandName): void + { + $this->output->writeLine(''); + $this->output->writeError("Command '{$commandName}' not found."); + + // Get suggestions + $suggestions = $this->suggestionEngine->suggestCommand($commandName); + + if ($suggestions->hasSuggestions()) { + $this->output->writeLine(''); + $this->output->writeLine('Did you mean one of these?', ConsoleColor::YELLOW); + + foreach ($suggestions->suggestions as $suggestion) { + $confidence = round($suggestion->similarity * 100); + $this->output->writeLine( + " • {$suggestion->command->name} ({$confidence}% match)", + ConsoleColor::CYAN + ); + if ($suggestion->command->description) { + $this->output->writeLine( + " {$suggestion->command->description}", + ConsoleColor::GRAY + ); + } + } + } + + $this->output->writeLine(''); + $this->output->writeLine('Type "help" to see all available commands.', ConsoleColor::GRAY); + $this->output->writeLine(''); + } + + /** + * Show contextual help for a command + */ + private function showContextualHelp(string $commandName): void + { + if (! $this->commandList->has($commandName)) { + return; + } + + $command = $this->commandList->get($commandName); + + $this->output->writeLine(''); + $this->output->writeLine('💡 Tip: Use "help ' . $commandName . '" for detailed help.', ConsoleColor::CYAN); + + if ($command->description) { + $this->output->writeLine("Description: {$command->description}", ConsoleColor::GRAY); + } + + $this->output->writeLine(''); + } + + /** + * Show welcome message + */ + private function showWelcome(): void + { + $this->output->writeLine(''); + $this->output->writeLine('🤖 Console Dialog Mode', ConsoleColor::BRIGHT_CYAN); + $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); + $this->output->writeLine(''); + + $commandCount = $this->commandList->count(); + $this->output->writeLine("Available commands: {$commandCount}", ConsoleColor::GRAY); + + if ($this->readlineAvailable) { + $this->output->writeLine('✓ Readline support enabled (Tab completion, ↑/↓ history)', ConsoleColor::GREEN); + } else { + $this->output->writeLine('⚠ Readline not available (install php-readline for better experience)', ConsoleColor::YELLOW); + } + + $this->output->writeLine(''); + $this->output->writeLine('Type "help" for available commands or "exit" to quit.', ConsoleColor::GRAY); + $this->output->writeLine(''); + } + + /** + * Show help + */ + private function showHelp(): void + { + $categories = $this->groupRegistry->getOrganizedCommands(); + + $this->output->writeLine(''); + $this->output->writeLine('📚 Available Commands', ConsoleColor::BRIGHT_CYAN); + $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); + $this->output->writeLine(''); + + foreach ($categories as $category => $commands) { + $this->output->writeLine("{$category}:", ConsoleColor::BRIGHT_YELLOW); + + foreach ($commands as $command) { + $name = $command->name ?? 'unknown'; + $description = $command->description ?? 'No description'; + $this->output->writeLine(" {$name}", ConsoleColor::WHITE); + $this->output->writeLine(" {$description}", ConsoleColor::GRAY); + } + + $this->output->writeLine(''); + } + + $this->output->writeLine('💡 Tips:', ConsoleColor::BRIGHT_YELLOW); + $this->output->writeLine(' • Type a command name to execute it', ConsoleColor::GRAY); + $this->output->writeLine(' • Use Tab for command completion', ConsoleColor::GRAY); + $this->output->writeLine(' • Use ↑/↓ to navigate command history', ConsoleColor::GRAY); + $this->output->writeLine(' • Type "help " for detailed help', ConsoleColor::GRAY); + $this->output->writeLine(' • Type "exit" or "quit" to leave', ConsoleColor::GRAY); + $this->output->writeLine(''); + } + + /** + * Show detailed help for a specific command + */ + private function showCommandHelp(string $commandName): void + { + $this->output->writeLine(''); + $this->output->writeLine("📖 Command Help: {$commandName}", ConsoleColor::BRIGHT_CYAN); + $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); + $this->output->writeLine(''); + + if (! $this->commandList->has($commandName)) { + $this->output->writeError("Command '{$commandName}' not found."); + $suggestions = $this->suggestionEngine->suggestCommand($commandName); + if ($suggestions->hasSuggestions()) { + $this->output->writeLine(''); + $this->output->writeLine('Did you mean one of these?', ConsoleColor::YELLOW); + foreach ($suggestions->suggestions as $suggestion) { + $this->output->writeLine(" • {$suggestion->command->name}", ConsoleColor::CYAN); + } + } + $this->output->writeLine(''); + + return; + } + + try { + $command = $this->commandList->get($commandName); + $discoveredAttributes = $this->discoveryRegistry->attributes()->get(\App\Framework\Console\ConsoleCommand::class); + + // Find the command object + $commandObject = null; + foreach ($discoveredAttributes as $discovered) { + $attribute = $discovered->createAttributeInstance(); + if ($attribute && $attribute->name === $commandName) { + $className = $discovered->className->getFullyQualified(); + $commandObject = $this->container->get($className); + break; + } + } + + if ($commandObject) { + $help = $this->helpGenerator->generateHelp($commandObject); + $this->displayFormattedHelp($help); + } else { + // Fallback to basic info + $this->output->writeLine("Name: {$command->name}", ConsoleColor::WHITE); + if ($command->description) { + $this->output->writeLine("Description: {$command->description}", ConsoleColor::GRAY); + } + } + } catch (\Throwable $e) { + $this->output->writeError("Failed to generate help: {$e->getMessage()}"); + if ($this->output->isTerminal()) { + $this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY); + } + } + + $this->output->writeLine(''); + } + + /** + * Display formatted help + */ + private function displayFormattedHelp(CommandHelp $help): void + { + $sections = $help->formatAsColoredText(); + + foreach ($sections as $section) { + $color = match ($section['color']) { + 'BRIGHT_CYAN' => ConsoleColor::BRIGHT_CYAN, + 'BRIGHT_YELLOW' => ConsoleColor::BRIGHT_YELLOW, + 'BRIGHT_WHITE' => ConsoleColor::BRIGHT_WHITE, + 'BRIGHT_GREEN' => ConsoleColor::BRIGHT_GREEN, + 'WHITE' => ConsoleColor::WHITE, + 'GRAY' => ConsoleColor::GRAY, + 'YELLOW' => ConsoleColor::YELLOW, + 'RED' => ConsoleColor::RED, + default => ConsoleColor::WHITE + }; + + $this->output->writeLine($section['text'], $color); + } + } + + /** + * Show command history + */ + private function showHistory(): void + { + $history = $this->commandHistory->getRecentHistory(20); + + $this->output->writeLine(''); + $this->output->writeLine('📜 Command History', ConsoleColor::BRIGHT_CYAN); + $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); + $this->output->writeLine(''); + + if (empty($history)) { + $this->output->writeLine('No commands in history yet.', ConsoleColor::GRAY); + } else { + foreach ($history as $index => $entry) { + $command = $entry['command'] ?? 'unknown'; + $count = $entry['count'] ?? 1; + $timestamp = isset($entry['timestamp']) ? date('Y-m-d H:i:s', $entry['timestamp']) : 'unknown'; + + $this->output->writeLine( + sprintf('%3d. %s (used %d times, last: %s)', $index + 1, $command, $count, $timestamp), + ConsoleColor::WHITE + ); + } + } + + $this->output->writeLine(''); + } + + /** + * Clear screen + */ + private function clearScreen(): void + { + if (function_exists('shell_exec') && $this->output->isTerminal()) { + // ANSI clear screen sequence + $this->output->write("\033[2J\033[H"); + } + } + + /** + * Cleanup resources + */ + private function cleanup(): void + { + if ($this->readlineAvailable) { + // Save history + readline_write_history(null); + } + } +} + diff --git a/src/Framework/Console/Components/DialogCommandExecutor.php b/src/Framework/Console/Components/DialogCommandExecutor.php new file mode 100644 index 00000000..77ab4f0a --- /dev/null +++ b/src/Framework/Console/Components/DialogCommandExecutor.php @@ -0,0 +1,69 @@ + $arguments + */ + public function executeCommand(string $commandName, array $arguments = []): ExitCode + { + $this->commandHistory->addToHistory($commandName); + + $this->output->writeLine(''); + $this->output->writeLine("Executing: {$commandName}", ConsoleColor::CYAN); + + if (! empty($arguments)) { + $this->output->writeLine('Arguments: ' . implode(' ', $arguments), ConsoleColor::GRAY); + } + + $this->output->writeLine(str_repeat('─', 60)); + + try { + $exitCode = $this->commandRegistry->executeCommand($commandName, $arguments, $this->output); + + $this->output->writeLine(str_repeat('─', 60)); + + if ($exitCode->value === 0) { + $this->output->writeLine('✓ Command completed successfully', ConsoleColor::GREEN); + } else { + $this->output->writeLine('✗ Command failed with exit code: ' . $exitCode->value, ConsoleColor::RED); + } + + return $exitCode; + } catch (\Exception $e) { + $this->output->writeLine(str_repeat('─', 60)); + $this->output->writeLine('✗ Error: ' . $e->getMessage(), ConsoleColor::RED); + + if ($this->output->isTerminal()) { + $this->output->writeLine('Stack trace:', ConsoleColor::GRAY); + $this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY); + } + + return ExitCode::GENERAL_ERROR; + } + } +} + diff --git a/src/Framework/Console/Components/Table.php b/src/Framework/Console/Components/Table.php index eff6e342..66b130e7 100644 --- a/src/Framework/Console/Components/Table.php +++ b/src/Framework/Console/Components/Table.php @@ -25,7 +25,7 @@ final class Table private ?ConsoleStyle $headerStyle = null, private ?ConsoleStyle $rowStyle = null, private ?ConsoleStyle $borderStyle = null, - private bool $showBorders = true + private readonly bool $showBorders = true ) { $this->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD); $this->rowStyle ??= ConsoleStyle::create(); diff --git a/src/Framework/Console/Components/TreeHelper.php b/src/Framework/Console/Components/TreeHelper.php index d1a553b4..f85724c3 100644 --- a/src/Framework/Console/Components/TreeHelper.php +++ b/src/Framework/Console/Components/TreeHelper.php @@ -31,8 +31,8 @@ final class TreeHelper private array $nodes = []; public function __construct( - private string $title = '', - private ConsoleOutput $output = new ConsoleOutput(), + private string $title = '', + #private readonly ConsoleOutput $output = new ConsoleOutput(), ) { $this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD); $this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE); @@ -115,13 +115,13 @@ final class TreeHelper /** * Zeigt die vollständige Baumstruktur an. */ - public function display(): void + public function display(ConsoleOutput $output): void { if (! empty($this->title)) { - $this->output->writeLine($this->title, $this->nodeStyle); + $output->writeLine($this->title, $this->nodeStyle); } - $this->displayTree(); + $this->displayTree($output); } /** @@ -156,7 +156,7 @@ final class TreeHelper * Zeigt die Baumstruktur mit dem aktuellen Präfix an. * (Interne Methode für rekursives Rendern) */ - private function displayTree(): void + private function displayTree(ConsoleOutput $output): void { $count = count($this->nodes); @@ -172,7 +172,7 @@ final class TreeHelper $style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle; $title = $linePrefix . $item['title']; - $this->output->writeLine( + $output->writeLine( $this->lineStyle->apply($linePrefix) . $style->apply($item['title']) ); @@ -181,7 +181,7 @@ final class TreeHelper if (! $item['isLeaf'] && $item['node'] !== null) { $item['node'] ->setPrefix($nodePrefix, $isLast) - ->displayTree(); + ->displayTree($output); } } } diff --git a/src/Framework/Console/ConsoleApplication.php b/src/Framework/Console/ConsoleApplication.php index ad55ee9d..480b9998 100644 --- a/src/Framework/Console/ConsoleApplication.php +++ b/src/Framework/Console/ConsoleApplication.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace App\Framework\Console; use App\Framework\Config\AppConfig; +use App\Framework\Console\Components\ConsoleDialog; use App\Framework\Console\Components\ConsoleTUI; +use App\Framework\Console\Components\DialogCommandExecutor; use App\Framework\Console\Components\TuiCommandExecutor; use App\Framework\Console\Components\TuiInputHandler; use App\Framework\Console\Components\TuiRenderer; @@ -179,6 +181,11 @@ final class ConsoleApplication return $this->launchInteractiveTUI(); } + // Handle dialog mode launch flags + if (in_array($commandName, ['--dialog', '--chat'])) { + return $this->launchDialogMode(); + } + // Handle built-in commands if (in_array($commandName, ['help', '--help', '-h'])) { // Spezifische Command-Hilfe @@ -376,6 +383,8 @@ final class ConsoleApplication $this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW); $this->output->writeLine(" php {$this->scriptName} # Interaktive TUI starten"); $this->output->writeLine(" php {$this->scriptName} --interactive # Interaktive TUI explizit starten"); + $this->output->writeLine(" php {$this->scriptName} --dialog # Dialog-Modus starten (AI-Assistent-ähnlich)"); + $this->output->writeLine(" php {$this->scriptName} --chat # Dialog-Modus starten (Alias)"); $this->output->writeLine(" php {$this->scriptName} # Commands einer Kategorie anzeigen"); $this->output->writeLine(" php {$this->scriptName} [argumente] # Kommando direkt ausführen"); $this->output->writeLine(" php {$this->scriptName} help # Hilfe für spezifisches Kommando"); @@ -384,6 +393,7 @@ final class ConsoleApplication $this->output->writeLine("Hinweis:", ConsoleColor::CYAN); $this->output->writeLine(" Ohne Argumente wird automatisch die interaktive TUI gestartet."); $this->output->writeLine(" Die TUI bietet eine grafische Navigation durch alle verfügbaren Commands."); + $this->output->writeLine(" Der Dialog-Modus bietet eine einfache Text-Eingabe mit Tab-Completion und History."); } /** @@ -614,6 +624,63 @@ final class ConsoleApplication } } + /** + * Startet den Dialog-Modus + */ + private function launchDialogMode(): int + { + try { + // Get DiscoveryRegistry for dialog components + $discoveryRegistry = $this->container->get(DiscoveryRegistry::class); + + // Create CommandHistory + $commandHistory = new CommandHistory(); + + // Create new services + $groupRegistry = new CommandGroupRegistry($discoveryRegistry); + $commandList = $this->commandRegistry->getCommandList(); + + // Create dialog components + $commandExecutor = new DialogCommandExecutor( + $this->output, + $this->commandRegistry, + $commandHistory, + $this->scriptName + ); + + // Create dialog instance + $dialog = new ConsoleDialog( + $this->output, + $discoveryRegistry, + $commandHistory, + $groupRegistry, + $commandExecutor, + $commandList, + $this->container, + $this->scriptName . '> ' + ); + + // Start dialog + return $dialog->run()->value; + + } catch (Throwable $e) { + $this->output->writeError("Failed to launch dialog mode: " . $e->getMessage()); + + $config = $this->container->get(AppConfig::class); + if ($config->isDevelopment()) { + $this->output->writeLine("Stack trace:", ConsoleColor::RED); + $this->output->writeLine($e->getTraceAsString()); + } + + // Fallback to help + $this->output->newLine(); + $this->output->writeLine("Falling back to command-line help:", ConsoleColor::YELLOW); + $this->showHelp(); + + return ExitCode::SOFTWARE_ERROR->value; + } + } + /** * Prüft ob das Terminal für TUI kompatibel ist */ diff --git a/src/Framework/Console/Result/TableResult.php b/src/Framework/Console/Result/TableResult.php index 1d287af3..7adce036 100644 --- a/src/Framework/Console/Result/TableResult.php +++ b/src/Framework/Console/Result/TableResult.php @@ -34,10 +34,10 @@ final readonly class TableResult implements ConsoleResult * @param ExitCode $exitCode Exit code (default: SUCCESS) */ public function __construct( - public readonly array $headers, - public readonly array $rows, - public readonly ?string $title = null, - public readonly ExitCode $exitCode = ExitCode::SUCCESS, + public array $headers, + public array $rows, + public ?string $title = null, + public ExitCode $exitCode = ExitCode::SUCCESS, ) { $this->data = [ 'headers' => $this->headers, @@ -78,7 +78,7 @@ final readonly class TableResult implements ConsoleResult */ public function render(ConsoleOutputInterface $output): void { - $table = new Table($output); + $table = new Table(); if ($this->title !== null) { $table->setTitle($this->title); diff --git a/src/Framework/Console/TerminalDetector.php b/src/Framework/Console/TerminalDetector.php new file mode 100644 index 00000000..582b9351 --- /dev/null +++ b/src/Framework/Console/TerminalDetector.php @@ -0,0 +1,90 @@ +stdout; + $streamResource = $stream->getStream(); + + return posix_isatty($streamResource); + } + + /** + * Prüft ob STDOUT ein Terminal ist. + * + * @return bool True wenn STDOUT ein Terminal ist + */ + public static function isStdoutTerminal(): bool + { + return self::isTerminal(CliSapi::detect()->stdout); + } + + /** + * Prüft ob STDERR ein Terminal ist. + * + * @return bool True wenn STDERR ein Terminal ist + */ + public static function isStderrTerminal(): bool + { + return self::isTerminal(CliSapi::detect()->stderr); + } + + /** + * Prüft ob ein Stream ein Terminal ist und Farben unterstützt. + * + * Kombiniert Terminal-Detection mit Farb-Support-Prüfung. + * Terminal muss vorhanden sein und TERM-Environment-Variable sollte nicht "dumb" sein. + * + * Verwendet intern CliSapi für Default-Streams. + * + * @param TerminalStream|null $stream Terminal Stream (oder null für STDOUT) + * @return bool True wenn Terminal vorhanden und Farben unterstützt werden + */ + public static function supportsColors(?TerminalStream $stream = null): bool + { + if (!self::isTerminal($stream)) { + return false; + } + + // Prüfe TERM Environment-Variable + // "dumb" bedeutet kein Farb-Support + $term = getenv('TERM'); + if ($term !== false && strtolower($term) === 'dumb') { + return false; + } + + // Prüfe NO_COLOR Environment-Variable (standardisiert) + if (getenv('NO_COLOR') !== false) { + return false; + } + + return true; + } +} + diff --git a/src/Framework/Console/ValueObjects/TerminalStream.php b/src/Framework/Console/ValueObjects/TerminalStream.php new file mode 100644 index 00000000..8994ecf3 --- /dev/null +++ b/src/Framework/Console/ValueObjects/TerminalStream.php @@ -0,0 +1,143 @@ +stream; + } + + /** + * Prüft ob dieser Stream STDIN ist. + */ + public function isStdin(): bool + { + return defined('STDIN') && $this->stream === STDIN; + } + + /** + * Prüft ob dieser Stream STDOUT ist. + */ + public function isStdout(): bool + { + return defined('STDOUT') && $this->stream === STDOUT; + } + + /** + * Prüft ob dieser Stream STDERR ist. + */ + public function isStderr(): bool + { + return defined('STDERR') && $this->stream === STDERR; + } +} + diff --git a/src/Framework/Core/System/Ini/IniDirective.php b/src/Framework/Core/System/Ini/IniDirective.php index c23e8fb8..b33dd255 100644 --- a/src/Framework/Core/System/Ini/IniDirective.php +++ b/src/Framework/Core/System/Ini/IniDirective.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace App\Framework\Core\System\Ini; -final class IniDirective +final readonly class IniDirective implements Stringable { public function __construct( public string $name, @@ -12,9 +12,18 @@ final class IniDirective private int $accessMask, ) {} - public function getAccess(): int + public function getAccess(): Access + { + return Access::fromBitmask($this->accessMask); + } + + public function getAccessMask(): int { - $access = Access::fromBitmask($this->accessMask); return $this->accessMask; } + + public function __toString(): string + { + return $this->value; + } } diff --git a/src/Framework/Core/System/Ini/IniKey.php b/src/Framework/Core/System/Ini/IniKey.php index 024cb94e..be44c4bc 100644 --- a/src/Framework/Core/System/Ini/IniKey.php +++ b/src/Framework/Core/System/Ini/IniKey.php @@ -57,6 +57,16 @@ enum IniKey: string case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX = "opcache.enable_file_override_from_index"; case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX_IF_EXISTS = "opcache.enable_file_override_from_index_if_exists"; case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX_IF_EXISTS_IF_EMPTY = "opcache.enable_file_override_from_index_if_exists_if_empty"; - - + case MAX_EXECUTION_TIME = "max_execution_time"; + case MAX_INPUT_TIME = "max_input_time"; + case UPLOAD_MAX_FILESIZE = "upload_max_filesize"; + case POST_MAX_SIZE = "post_max_size"; + case MAX_FILE_UPLOADS = "max_file_uploads"; + case DISPLAY_ERRORS = "display_errors"; + case DISPLAY_STARTUP_ERRORS = "display_startup_errors"; + case ERROR_LOG = "error_log"; + case DATE_TIMEZONE = "date.timezone"; + case EXPOSE_PHP = "expose_php"; + case REALPATH_CACHE_SIZE = "realpath_cache_size"; + case REALPATH_CACHE_TTL = "realpath_cache_ttl"; } diff --git a/src/Framework/Core/System/Ini/IniManager.php b/src/Framework/Core/System/Ini/IniManager.php new file mode 100644 index 00000000..318d6554 --- /dev/null +++ b/src/Framework/Core/System/Ini/IniManager.php @@ -0,0 +1,158 @@ + All ini directives loaded once in constructor + */ + private array $directives; + + public function __construct() + { + $this->directives = $this->loadDirectives(); + } + + /** + * Get an ini value by key. + * Returns null if the key does not exist. + * + * @param IniKey|string $key The ini key (enum or string) + * @return IniDirective|null The ini value or null if not found + */ + public function get(IniKey|string $key): IniDirective|null + { + $keyString = $this->normalizeKey($key); + $directive = $this->directives[$keyString] ?? null; + + return $directive ?? null; + } + + /** + * Set an ini value. + * Only works for keys that are modifiable (INI_USER or INI_ALL). + * Updates the cached directive after successful set. + * + * @param IniKey $key The ini key + * @param string $value The value to set + * @return bool True on success, false on failure + */ + public function set(IniKey $key, string $value): bool + { + $result = ini_set($key->value, $value); + + if ($result === false) { + return false; + } + + // Update cached directive with new value + if (isset($this->directives[$key->value])) { + $directive = $this->directives[$key->value]; + $this->directives[$key->value] = new IniDirective( + name: $directive->name, + value: $value, + global: $directive->global, + accessMask: $directive->getAccessMask() + ); + } + + return true; + } + + /** + * Get a complete IniDirective object with access information. + * + * @param IniKey $key The ini key + * @return IniDirective|null The directive object or null if not found + */ + public function getDirective(IniKey $key): ?IniDirective + { + return $this->directives[$key->value] ?? null; + } + + /** + * Get all ini directives as IniDirective objects. + * + * @return array Array of directive name => IniDirective + */ + public function getAll(): array + { + return $this->directives; + } + + /** + * Get all ini directives filtered by access level. + * + * @param Access $access The access level to filter by + * @return array Array of directive name => IniDirective + */ + public function getAllByAccess(Access $access): array + { + $filtered = []; + + foreach ($this->directives as $name => $directive) { + if ($directive->getAccess() === $access) { + $filtered[$name] = $directive; + } + } + + return $filtered; + } + + /** + * Check if an ini directive is modifiable. + * A directive is modifiable if it has INI_USER or INI_ALL access. + * + * @param IniKey $key The ini key + * @return bool True if modifiable, false otherwise + */ + public function isModifiable(IniKey $key): bool + { + $directive = $this->getDirective($key); + + if ($directive === null) { + return false; + } + + $access = $directive->getAccess(); + + return $access === Access::USER || $access === Access::ALL; + } + + /** + * Load all ini directives from ini_get_all() and convert to IniDirective objects. + * + * @return array Array of directive name => IniDirective + */ + private function loadDirectives(): array + { + $all = ini_get_all(); + $directives = []; + + foreach ($all as $name => $directive) { + $directives[$name] = new IniDirective( + name: $name, + value: $directive['local_value'] ?? $directive['global_value'] ?? '', + global: $directive['global_value'] ?? '', + accessMask: $directive['access'] ?? INI_ALL + ); + } + + return $directives; + } + + /** + * Normalize a key to a string. + * Converts IniKey enum to its string value, or returns the string as-is. + * + * @param IniKey|string $key The key to normalize + * @return string The normalized string key + */ + private function normalizeKey(IniKey|string $key): string + { + return $key instanceof IniKey ? $key->value : $key; + } +} + diff --git a/src/Framework/Core/System/PhpIni.php b/src/Framework/Core/System/PhpIni.php deleted file mode 100644 index 789f2229..00000000 --- a/src/Framework/Core/System/PhpIni.php +++ /dev/null @@ -1,30 +0,0 @@ -path = $path; - } - - public function isLoaded(): bool - { - return $this->path !== ""; - } - - public function __toString(): string - { - return $this->path; - } -} diff --git a/src/Framework/Core/System/SystemConfig.php b/src/Framework/Core/System/SystemConfig.php new file mode 100644 index 00000000..c8252dd1 --- /dev/null +++ b/src/Framework/Core/System/SystemConfig.php @@ -0,0 +1,26 @@ +ini->get(IniKey::MEMORY_LIMIT) + * $config->env->get('APP_ENV') + * $config->env->getBool('APP_DEBUG') + */ +final readonly class SystemConfig +{ + public function __construct( + public readonly IniManager $ini, + public readonly Environment $env + ) {} +} diff --git a/src/Framework/Database/Migration/MigrationRunner.php b/src/Framework/Database/Migration/MigrationRunner.php index 0b35c737..6117d7b6 100644 --- a/src/Framework/Database/Migration/MigrationRunner.php +++ b/src/Framework/Database/Migration/MigrationRunner.php @@ -18,12 +18,12 @@ use App\Framework\DateTime\Clock; use App\Framework\Exception\Core\DatabaseErrorCode; use App\Framework\Exception\ExceptionContext; use App\Framework\Exception\FrameworkException; +use App\Framework\Id\Ulid\UlidGenerator; use App\Framework\Logging\Logger; use App\Framework\Performance\MemoryMonitor; use App\Framework\Performance\OperationTracker; use App\Framework\Performance\PerformanceReporter; use App\Framework\Performance\Repository\PerformanceMetricsRepository; -use App\Framework\Ulid\UlidGenerator; final readonly class MigrationRunner { diff --git a/src/Framework/Database/Services/EntityPersister.php b/src/Framework/Database/Services/EntityPersister.php index d8201f87..3b718ae4 100644 --- a/src/Framework/Database/Services/EntityPersister.php +++ b/src/Framework/Database/Services/EntityPersister.php @@ -264,7 +264,7 @@ final readonly class EntityPersister if ($caster !== null) { $result = $caster->toDatabase($value); // Debug logging for ULID issues - if ($valueType === 'App\Framework\Ulid\Ulid') { + if ($valueType === 'App\Framework\Id\Ulid\Ulid') { error_log("ULID converted: " . var_export($result, true) . " (length: " . strlen($result) . ")"); } diff --git a/src/Framework/Database/TypeCaster/UlidCaster.php b/src/Framework/Database/TypeCaster/UlidCaster.php index 9bd8800c..a37cd06b 100644 --- a/src/Framework/Database/TypeCaster/UlidCaster.php +++ b/src/Framework/Database/TypeCaster/UlidCaster.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Framework\Database\TypeCaster; use App\Framework\DateTime\SystemClock; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; use InvalidArgumentException; final class UlidCaster implements TypeCasterInterface diff --git a/src/Framework/ErrorAggregation/ErrorEvent.php b/src/Framework/ErrorAggregation/ErrorEvent.php index 5d187d70..b8500705 100644 --- a/src/Framework/ErrorAggregation/ErrorEvent.php +++ b/src/Framework/ErrorAggregation/ErrorEvent.php @@ -7,7 +7,7 @@ namespace App\Framework\ErrorAggregation; use App\Framework\Exception\Core\ErrorSeverity; use App\Framework\Exception\ErrorCode; use App\Framework\Exception\ErrorHandlerContext; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; /** * Represents a single error event for aggregation and analysis diff --git a/src/Framework/ErrorAggregation/ErrorPattern.php b/src/Framework/ErrorAggregation/ErrorPattern.php index f0d91728..2c375a02 100644 --- a/src/Framework/ErrorAggregation/ErrorPattern.php +++ b/src/Framework/ErrorAggregation/ErrorPattern.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Framework\ErrorAggregation; use App\Framework\Exception\Core\ErrorSeverity; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; /** * Represents a pattern of similar errors for analysis and alerting diff --git a/src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php b/src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php index ce39401a..ada6a3b2 100644 --- a/src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php +++ b/src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php @@ -9,7 +9,7 @@ use App\Framework\ErrorAggregation\ErrorEvent; use App\Framework\ErrorAggregation\ErrorPattern; use App\Framework\Exception\Core\ErrorSeverity; use App\Framework\Exception\ErrorCode; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; /** * Database-based error storage implementation @@ -25,8 +25,8 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface { $sql = " INSERT INTO error_events ( - id, service, component, operation, error_code, error_message, - severity, occurred_at, context, metadata, request_id, user_id, + id, service, component, operation, error_code, error_message, + severity, occurred_at, context, metadata, request_id, user_id, client_ip, is_security_event, stack_trace, user_agent ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "; @@ -59,8 +59,8 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface $sql = " INSERT INTO error_events ( - id, service, component, operation, error_code, error_message, - severity, occurred_at, context, metadata, request_id, user_id, + id, service, component, operation, error_code, error_message, + severity, occurred_at, context, metadata, request_id, user_id, client_ip, is_security_event, stack_trace, user_agent ) VALUES " . str_repeat('(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?),', count($events)); @@ -95,9 +95,9 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface { $sql = " INSERT INTO error_patterns ( - id, fingerprint, service, component, operation, error_code, - normalized_message, severity, occurrence_count, first_occurrence, - last_occurrence, affected_users, affected_ips, is_active, + id, fingerprint, service, component, operation, error_code, + normalized_message, severity, occurrence_count, first_occurrence, + last_occurrence, affected_users, affected_ips, is_active, is_acknowledged, acknowledged_by, acknowledged_at, resolution, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE @@ -163,8 +163,8 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface public function getActivePatterns(int $limit = 50, int $offset = 0): array { $sql = " - SELECT * FROM error_patterns - WHERE is_active = 1 + SELECT * FROM error_patterns + WHERE is_active = 1 ORDER BY last_occurrence DESC, occurrence_count DESC LIMIT ? OFFSET ? "; @@ -177,7 +177,7 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface public function getPatternsByService(string $service, int $limit = 50): array { $sql = " - SELECT * FROM error_patterns + SELECT * FROM error_patterns WHERE service = ? AND is_active = 1 ORDER BY last_occurrence DESC, occurrence_count DESC LIMIT ? @@ -209,14 +209,14 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface public function getStatistics(\DateTimeImmutable $from, \DateTimeImmutable $to): array { $sql = " - SELECT + SELECT COUNT(*) as total_events, COUNT(DISTINCT service) as services_affected, COUNT(DISTINCT user_id) as users_affected, COUNT(DISTINCT client_ip) as ips_affected, severity, COUNT(*) as severity_count - FROM error_events + FROM error_events WHERE occurred_at BETWEEN ? AND ? GROUP BY severity "; @@ -259,11 +259,11 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface }; $sql = " - SELECT + SELECT DATE_FORMAT(occurred_at, ?) as time_bucket, severity, COUNT(*) as count - FROM error_events + FROM error_events WHERE occurred_at BETWEEN ? AND ? GROUP BY time_bucket, severity ORDER BY time_bucket ASC @@ -290,7 +290,7 @@ final readonly class DatabaseErrorStorage implements ErrorStorageInterface public function getTopPatterns(int $limit = 10, ?string $service = null): array { $sql = " - SELECT * FROM error_patterns + SELECT * FROM error_patterns WHERE is_active = 1 "; $params = []; diff --git a/src/Framework/ErrorBoundaries/Middleware/ErrorBoundaryMiddleware.php b/src/Framework/ErrorBoundaries/Middleware/ErrorBoundaryMiddleware.php index 0c2e36d1..2beed201 100644 --- a/src/Framework/ErrorBoundaries/Middleware/ErrorBoundaryMiddleware.php +++ b/src/Framework/ErrorBoundaries/Middleware/ErrorBoundaryMiddleware.php @@ -56,7 +56,7 @@ final readonly class ErrorBoundaryMiddleware implements HttpMiddleware */ private function createJsonFallbackResponse($request): JsonResponse { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $errorData = [ 'error' => [ 'code' => 'SERVICE_TEMPORARILY_UNAVAILABLE', @@ -75,7 +75,7 @@ final readonly class ErrorBoundaryMiddleware implements HttpMiddleware */ private function createHtmlFallbackResponse($request, MiddlewareContext $context) { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $fallbackHtml = $this->getFallbackHtmlContent($request); return new ViewResult($fallbackHtml, [ diff --git a/src/Framework/Filesystem/Directory.php b/src/Framework/Filesystem/Directory.php index dd418462..b801a5cc 100644 --- a/src/Framework/Filesystem/Directory.php +++ b/src/Framework/Filesystem/Directory.php @@ -23,6 +23,19 @@ final readonly class Directory ) { } + /** + * Erstellt eine FilesystemFactory-Instanz mit Logger vom Storage (falls verfügbar) + */ + private function getFactory(): FilesystemFactory + { + $logger = null; + if (property_exists($this->storage, 'logger')) { + $logger = $this->storage->logger ?? null; + } + + return FilesystemFactory::create($logger); + } + /** * Get path as FilePath object */ @@ -65,9 +78,10 @@ final readonly class Directory $paths = $this->storage->listDirectory($this->getPathString()); $files = []; + $factory = $this->getFactory(); foreach ($paths as $path) { if (is_file($path)) { - $files[] = FilesystemFactory::createFile($path, $this->storage); + $files[] = $factory->createFile($path, $this->storage); } } @@ -84,9 +98,10 @@ final readonly class Directory $paths = $this->storage->listDirectory($this->getPathString()); $directories = []; + $factory = $this->getFactory(); foreach ($paths as $path) { if (is_dir($path)) { - $directories[] = FilesystemFactory::createDirectory($path, $this->storage); + $directories[] = $factory->createDirectory($path, $this->storage); } } @@ -104,11 +119,12 @@ final readonly class Directory $files = []; $directories = []; + $factory = $this->getFactory(); foreach ($paths as $path) { if (is_file($path)) { - $files[] = FilesystemFactory::createFile($path, $this->storage); + $files[] = $factory->createFile($path, $this->storage); } elseif (is_dir($path)) { - $directories[] = FilesystemFactory::createDirectory($path, $this->storage); + $directories[] = $factory->createDirectory($path, $this->storage); } } @@ -122,7 +138,7 @@ final readonly class Directory { $filePath = $this->getPath()->join($filename); - return FilesystemFactory::createFile($filePath, $this->storage); + return $this->getFactory()->createFile($filePath, $this->storage); } /** @@ -132,7 +148,7 @@ final readonly class Directory { $dirPath = $this->getPath()->join($name); - return FilesystemFactory::createDirectory($dirPath, $this->storage); + return $this->getFactory()->createDirectory($dirPath, $this->storage); } /** @@ -140,7 +156,7 @@ final readonly class Directory */ public function refresh(): Directory { - return FilesystemFactory::createDirectory($this->path, $this->storage); + return $this->getFactory()->createDirectory($this->path, $this->storage); } /** @@ -158,7 +174,7 @@ final readonly class Directory { $parentPath = $this->getPath()->getDirectory(); - return FilesystemFactory::createDirectory($parentPath, $this->storage); + return $this->getFactory()->createDirectory($parentPath, $this->storage); } /** @@ -273,10 +289,11 @@ final readonly class Directory continue; } + $factory = $this->getFactory(); if ($result['is_file']) { - $files[] = FilesystemFactory::createFile($result['path'], $this->storage); + $files[] = $factory->createFile($result['path'], $this->storage); } elseif ($result['is_dir']) { - $directories[] = FilesystemFactory::createDirectory($result['path'], $this->storage); + $directories[] = $factory->createDirectory($result['path'], $this->storage); } } diff --git a/src/Framework/Filesystem/FilesystemFactory.php b/src/Framework/Filesystem/FilesystemFactory.php index ca91b2a9..4225c5d4 100644 --- a/src/Framework/Filesystem/FilesystemFactory.php +++ b/src/Framework/Filesystem/FilesystemFactory.php @@ -5,8 +5,7 @@ declare(strict_types=1); namespace App\Framework\Filesystem; use App\Framework\Filesystem\ValueObjects\FilePath; -use App\Framework\Logging\DefaultLogger; -use App\Framework\Logging\LoggerFactory; +use App\Framework\Logging\Logger; use ReflectionClass; /** @@ -14,6 +13,22 @@ use ReflectionClass; */ final readonly class FilesystemFactory { + public function __construct( + private ?Logger $logger = null + ) { + } + + /** + * Erstellt eine neue FilesystemFactory-Instanz mit Logger. + * + * @param Logger|null $logger Optional logger instance + * @return self + */ + public static function create(?Logger $logger = null): self + { + return new self($logger); + } + /** * Erstellt ein File-Objekt mit Lazy-Loading für schwere Eigenschaften. * @@ -21,21 +36,18 @@ final readonly class FilesystemFactory * @param Storage $storage Storage-Implementierung * @param int|null $cacheTimeoutSeconds Optional, Zeit in Sekunden, nach der der Cache ungültig wird * @param bool $lazyLoad Optional, ob Lazy-Loading verwendet werden soll - * @param DefaultLogger|null $logger Optional, Logger für Debug-Informationen */ - public static function createFile( + public function createFile( FilePath|string $path, Storage $storage, ?int $cacheTimeoutSeconds = null, - bool $lazyLoad = true, - ?DefaultLogger $logger = null + bool $lazyLoad = true ): File { - $logger ??= LoggerFactory::getDefaultLogger(); $pathString = $path instanceof FilePath ? $path->toString() : $path; // Direkte Instanziierung ohne Lazy-Loading if (! $lazyLoad) { - $logger->debug("Erstelle File-Objekt ohne Lazy-Loading: {$pathString}"); + $this->logger?->debug("Erstelle File-Objekt ohne Lazy-Loading: {$pathString}"); // Nur laden wenn die Datei existiert if (! $storage->exists($pathString)) { @@ -53,21 +65,22 @@ final readonly class FilesystemFactory $reflection = new ReflectionClass(File::class); $loadTime = time(); + $logger = $this->logger; // LazyProxy verwenden für individuelle Property-Callbacks return $reflection->newLazyProxy([ // Dateiinhalt wird erst beim ersten Zugriff geladen 'contents' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) { $pathStr = $file->getPathString(); - $logger->debug("Lazy-Loading contents für {$pathStr}"); + $logger?->debug("Lazy-Loading contents für {$pathStr}"); // Cache-Invalidierung basierend auf Zeit if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) { - $logger->debug("Cache-Timeout erreicht für {$pathStr}, lade neu"); + $logger?->debug("Cache-Timeout erreicht für {$pathStr}, lade neu"); } if (! $file->exists()) { - $logger->debug("Datei existiert nicht: {$pathStr}"); + $logger?->debug("Datei existiert nicht: {$pathStr}"); return ''; } @@ -78,15 +91,15 @@ final readonly class FilesystemFactory // Dateigröße wird erst beim ersten Zugriff ermittelt 'size' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) { $pathStr = $file->getPathString(); - $logger->debug("Lazy-Loading size für {$pathStr}"); + $logger?->debug("Lazy-Loading size für {$pathStr}"); // Cache-Invalidierung basierend auf Zeit if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) { - $logger->debug("Cache-Timeout erreicht für {$pathStr}, lade neu"); + $logger?->debug("Cache-Timeout erreicht für {$pathStr}, lade neu"); } if (! $file->exists()) { - $logger->debug("Datei existiert nicht: {$pathStr}"); + $logger?->debug("Datei existiert nicht: {$pathStr}"); return 0; } @@ -97,15 +110,15 @@ final readonly class FilesystemFactory // Zeitstempel wird erst beim ersten Zugriff ermittelt 'lastModified' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) { $pathStr = $file->getPathString(); - $logger->debug("Lazy-Loading lastModified für {$pathStr}"); + $logger?->debug("Lazy-Loading lastModified für {$pathStr}"); // Cache-Invalidierung basierend auf Zeit if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) { - $logger->debug("Cache-Timeout erreicht für {$pathStr}, lade neu"); + $logger?->debug("Cache-Timeout erreicht für {$pathStr}, lade neu"); } if (! $file->exists()) { - $logger->debug("Datei existiert nicht: {$pathStr}"); + $logger?->debug("Datei existiert nicht: {$pathStr}"); return 0; } @@ -121,20 +134,17 @@ final readonly class FilesystemFactory * @param FilePath|string $path Pfad zum Verzeichnis * @param Storage $storage Storage-Implementierung * @param bool $lazyLoad Optional, ob Lazy-Loading verwendet werden soll - * @param DefaultLogger|null $logger Optional, Logger für Debug-Informationen */ - public static function createDirectory( + public function createDirectory( FilePath|string $path, Storage $storage, - bool $lazyLoad = true, - ?DefaultLogger $logger = null + bool $lazyLoad = true ): Directory { - $logger ??= LoggerFactory::getDefaultLogger(); $pathString = $path instanceof FilePath ? $path->toString() : $path; // Direkte Instanziierung ohne Lazy-Loading if (! $lazyLoad) { - $logger->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$pathString}"); + $this->logger?->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$pathString}"); $contents = []; if (is_dir($pathString)) { @@ -145,13 +155,14 @@ final readonly class FilesystemFactory } $reflection = new ReflectionClass(Directory::class); + $logger = $this->logger; // LazyGhost verwenden - alle Eigenschaften werden beim ersten Zugriff initialisiert $lazyDir = $reflection->newLazyGhost( // Initializer-Callback function (Directory $directory) use ($logger): void { $pathStr = $directory->getPathString(); - $logger->debug("Lazy-Loading Directory-Inhalt für {$pathStr}"); + $logger?->debug("Lazy-Loading Directory-Inhalt für {$pathStr}"); // Verzeichnisinhalt wird erst beim ersten Zugriff auf eine Eigenschaft geladen if ($directory->exists()) { diff --git a/src/Framework/Filesystem/InMemoryStorage.php b/src/Framework/Filesystem/InMemoryStorage.php index 81ece8bb..906175c2 100644 --- a/src/Framework/Filesystem/InMemoryStorage.php +++ b/src/Framework/Filesystem/InMemoryStorage.php @@ -210,7 +210,8 @@ final class InMemoryStorage implements Storage, StreamableStorage */ public function file(string $path): File { - return FilesystemFactory::createFile($path, $this); + $factory = FilesystemFactory::create($this->logger ?? null); + return $factory->createFile($path, $this); } /** @@ -218,7 +219,8 @@ final class InMemoryStorage implements Storage, StreamableStorage */ public function directory(string $path): Directory { - return FilesystemFactory::createDirectory($path, $this); + $factory = FilesystemFactory::create($this->logger ?? null); + return $factory->createDirectory($path, $this); } public function getMimeType(string $path): string diff --git a/src/Framework/Filesystem/LoggableStorage.php b/src/Framework/Filesystem/LoggableStorage.php index 702d9223..3689ff7e 100644 --- a/src/Framework/Filesystem/LoggableStorage.php +++ b/src/Framework/Filesystem/LoggableStorage.php @@ -4,8 +4,7 @@ declare(strict_types=1); namespace App\Framework\Filesystem; -use App\Framework\Logging\DefaultLogger; -use App\Framework\Logging\LoggerFactory; +use App\Framework\Logging\Logger; /** * Decorator für Storage-Implementierungen mit Logging-Unterstützung. @@ -18,50 +17,52 @@ final class LoggableStorage implements Storage public \App\Framework\Async\FiberManager $fiberManager { get => $this->storage->fiberManager; } + public ?Logger $logger; + public function __construct( private readonly Storage $storage, - private ?DefaultLogger $logger = null + ?Logger $logger = null ) { - $this->logger ??= LoggerFactory::getDefaultLogger(); + $this->logger = $logger; } public function get(string $path): string { - $this->logger->debug("Lese Datei: {$path}"); + $this->logger?->debug("Lese Datei: {$path}"); return $this->storage->get($path); } public function put(string $path, string $content): void { - $this->logger->debug("Schreibe Datei: {$path}"); + $this->logger?->debug("Schreibe Datei: {$path}"); $this->storage->put($path, $content); } public function exists(string $path): bool { $exists = $this->storage->exists($path); - $this->logger->debug("Prüfe Existenz: {$path} - " . ($exists ? 'existiert' : 'existiert nicht')); + $this->logger?->debug("Prüfe Existenz: {$path} - " . ($exists ? 'existiert' : 'existiert nicht')); return $exists; } public function delete(string $path): void { - $this->logger->debug("Lösche Datei: {$path}"); + $this->logger?->debug("Lösche Datei: {$path}"); $this->storage->delete($path); } public function copy(string $source, string $destination): void { - $this->logger->debug("Kopiere Datei: {$source} -> {$destination}"); + $this->logger?->debug("Kopiere Datei: {$source} -> {$destination}"); $this->storage->copy($source, $destination); } public function size(string $path): int { $size = $this->storage->size($path); - $this->logger->debug("Dateigröße: {$path} - {$size} Bytes"); + $this->logger?->debug("Dateigröße: {$path} - {$size} Bytes"); return $size; } @@ -69,7 +70,7 @@ final class LoggableStorage implements Storage public function lastModified(string $path): int { $lastModified = $this->storage->lastModified($path); - $this->logger->debug("Letzte Änderung: {$path} - " . date('Y-m-d H:i:s', $lastModified)); + $this->logger?->debug("Letzte Änderung: {$path} - " . date('Y-m-d H:i:s', $lastModified)); return $lastModified; } @@ -77,7 +78,7 @@ final class LoggableStorage implements Storage public function getMimeType(string $path): string { $mimeType = $this->storage->getMimeType($path); - $this->logger->debug("MIME-Typ: {$path} - {$mimeType}"); + $this->logger?->debug("MIME-Typ: {$path} - {$mimeType}"); return $mimeType; } @@ -85,7 +86,7 @@ final class LoggableStorage implements Storage public function isReadable(string $path): bool { $isReadable = $this->storage->isReadable($path); - $this->logger->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein')); + $this->logger?->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein')); return $isReadable; } @@ -93,70 +94,70 @@ final class LoggableStorage implements Storage public function isWritable(string $path): bool { $isWritable = $this->storage->isWritable($path); - $this->logger->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein')); + $this->logger?->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein')); return $isWritable; } public function listDirectory(string $directory): array { - $this->logger->debug("Liste Verzeichnis: {$directory}"); + $this->logger?->debug("Liste Verzeichnis: {$directory}"); $files = $this->storage->listDirectory($directory); - $this->logger->debug("Gefundene Dateien: " . count($files)); + $this->logger?->debug("Gefundene Dateien: " . count($files)); return $files; } public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void { - $this->logger->debug("Erstelle Verzeichnis: {$path}"); + $this->logger?->debug("Erstelle Verzeichnis: {$path}"); $this->storage->createDirectory($path, $permissions, $recursive); } public function file(string $path): File { - $this->logger->debug("Erstelle File-Objekt: {$path}"); + $this->logger?->debug("Erstelle File-Objekt: {$path}"); return $this->storage->file($path); } public function directory(string $path): Directory { - $this->logger->debug("Erstelle Directory-Objekt: {$path}"); + $this->logger?->debug("Erstelle Directory-Objekt: {$path}"); return $this->storage->directory($path); } public function batch(array $operations): array { - $this->logger->debug("Führe Batch-Operation aus mit " . count($operations) . " Operationen"); + $this->logger?->debug("Führe Batch-Operation aus mit " . count($operations) . " Operationen"); $results = $this->storage->batch($operations); - $this->logger->debug("Batch-Operation abgeschlossen"); + $this->logger?->debug("Batch-Operation abgeschlossen"); return $results; } public function getMultiple(array $paths): array { - $this->logger->debug("Lese " . count($paths) . " Dateien parallel"); + $this->logger?->debug("Lese " . count($paths) . " Dateien parallel"); $results = $this->storage->getMultiple($paths); - $this->logger->debug("Parallel-Lesen abgeschlossen"); + $this->logger?->debug("Parallel-Lesen abgeschlossen"); return $results; } public function putMultiple(array $files): void { - $this->logger->debug("Schreibe " . count($files) . " Dateien parallel"); + $this->logger?->debug("Schreibe " . count($files) . " Dateien parallel"); $this->storage->putMultiple($files); - $this->logger->debug("Parallel-Schreiben abgeschlossen"); + $this->logger?->debug("Parallel-Schreiben abgeschlossen"); } public function getMetadataMultiple(array $paths): array { - $this->logger->debug("Lade Metadaten für " . count($paths) . " Dateien parallel"); + $this->logger?->debug("Lade Metadaten für " . count($paths) . " Dateien parallel"); $results = $this->storage->getMetadataMultiple($paths); - $this->logger->debug("Metadaten-Laden abgeschlossen"); + $this->logger?->debug("Metadaten-Laden abgeschlossen"); return $results; } diff --git a/src/Framework/Filesystem/StorageTrait.php b/src/Framework/Filesystem/StorageTrait.php index d83a9c71..e5ca36c6 100644 --- a/src/Framework/Filesystem/StorageTrait.php +++ b/src/Framework/Filesystem/StorageTrait.php @@ -14,7 +14,8 @@ trait StorageTrait */ public function file(string $path): File { - return FilesystemFactory::createFile($path, $this); + $factory = FilesystemFactory::create($this->logger ?? null); + return $factory->createFile($path, $this); } /** @@ -22,6 +23,7 @@ trait StorageTrait */ public function directory(string $path): Directory { - return FilesystemFactory::createDirectory($path, $this); + $factory = FilesystemFactory::create($this->logger ?? null); + return $factory->createDirectory($path, $this); } } diff --git a/src/Framework/Filesystem/Traits/AtomicStorageTrait.php b/src/Framework/Filesystem/Traits/AtomicStorageTrait.php index 7c5e71a1..e1c647eb 100644 --- a/src/Framework/Filesystem/Traits/AtomicStorageTrait.php +++ b/src/Framework/Filesystem/Traits/AtomicStorageTrait.php @@ -16,7 +16,7 @@ trait AtomicStorageTrait { public function putAtomic(string $path, string $content): void { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $tempPath = $path . '.tmp.' . $generator->generate(); $this->put($tempPath, $content); diff --git a/src/Framework/Filesystem/ValueObjects/FilePath.php b/src/Framework/Filesystem/ValueObjects/FilePath.php index d11bedc3..faa18149 100644 --- a/src/Framework/Filesystem/ValueObjects/FilePath.php +++ b/src/Framework/Filesystem/ValueObjects/FilePath.php @@ -79,7 +79,7 @@ final readonly class FilePath implements Stringable */ public static function temp(?string $filename = null): self { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $filename ??= 'tmp_' . $generator->generate(); return self::tempDir()->join($filename); diff --git a/src/Framework/Id/Contracts/IdGeneratorInterface.php b/src/Framework/Id/Contracts/IdGeneratorInterface.php new file mode 100644 index 00000000..5f16825b --- /dev/null +++ b/src/Framework/Id/Contracts/IdGeneratorInterface.php @@ -0,0 +1,31 @@ + + */ + public function generateBatch(int $count): array; + + /** + * Validate if a string is a valid ID for this generator + */ + public function isValid(string $value): bool; +} diff --git a/src/Framework/Id/Contracts/IdInterface.php b/src/Framework/Id/Contracts/IdInterface.php new file mode 100644 index 00000000..9b75e3a9 --- /dev/null +++ b/src/Framework/Id/Contracts/IdInterface.php @@ -0,0 +1,26 @@ +value === $other->value; } diff --git a/src/Framework/Cuid/CuidGenerator.php b/src/Framework/Id/Cuid/CuidGenerator.php similarity index 98% rename from src/Framework/Cuid/CuidGenerator.php rename to src/Framework/Id/Cuid/CuidGenerator.php index 731bbaa3..b99af101 100644 --- a/src/Framework/Cuid/CuidGenerator.php +++ b/src/Framework/Id/Cuid/CuidGenerator.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\Framework\Cuid; +namespace App\Framework\Id\Cuid; +use App\Framework\Id\Contracts\IdGeneratorInterface; use App\Framework\Random\RandomGenerator; use InvalidArgumentException; @@ -12,7 +13,7 @@ use InvalidArgumentException; * * Generates Collision-resistant Unique Identifiers with machine fingerprinting. */ -final class CuidGenerator +final class CuidGenerator implements IdGeneratorInterface { private int $counter = 0; diff --git a/src/Framework/Id/IdGeneratorFactory.php b/src/Framework/Id/IdGeneratorFactory.php new file mode 100644 index 00000000..fb46645f --- /dev/null +++ b/src/Framework/Id/IdGeneratorFactory.php @@ -0,0 +1,91 @@ + new CuidGenerator($this->randomGenerator), + IdType::KSUID => new KsuidGenerator($this->randomGenerator), + IdType::NANOID => new NanoIdGenerator($this->randomGenerator), + IdType::ULID => new UlidGenerator($this->clock ?? new SystemClock()), + }; + } + + /** + * Create a CUID generator + */ + public function createCuid(?string $customFingerprint = null): CuidGenerator + { + return new CuidGenerator($this->randomGenerator, $customFingerprint); + } + + /** + * Create a KSUID generator + */ + public function createKsuid(): KsuidGenerator + { + return new KsuidGenerator($this->randomGenerator); + } + + /** + * Create a NanoId generator + */ + public function createNanoId(int $defaultSize = NanoId::DEFAULT_SIZE, string $defaultAlphabet = NanoId::DEFAULT_ALPHABET): NanoIdGenerator + { + return new NanoIdGenerator($this->randomGenerator, $defaultSize, $defaultAlphabet); + } + + /** + * Create a ULID generator + */ + public function createUlid(?Clock $clock = null): UlidGenerator + { + return new UlidGenerator($clock ?? $this->clock ?? new SystemClock()); + } + + /** + * Create a factory with default dependencies + */ + public static function createDefault(): self + { + return new self( + new SecureRandomGenerator(), + new SystemClock() + ); + } +} diff --git a/src/Framework/Id/IdType.php b/src/Framework/Id/IdType.php new file mode 100644 index 00000000..bd011916 --- /dev/null +++ b/src/Framework/Id/IdType.php @@ -0,0 +1,56 @@ + + */ + public static function all(): array + { + return array_column(self::cases(), 'value'); + } + + /** + * Check if a string value is a valid ID type + */ + public static function isValid(string $value): bool + { + foreach (self::cases() as $case) { + if ($case->value === strtolower($value)) { + return true; + } + } + + return false; + } + + /** + * Create from string value + */ + public static function fromString(string $value): self + { + $value = strtolower($value); + + foreach (self::cases() as $case) { + if ($case->value === $value) { + return $case; + } + } + + throw new \InvalidArgumentException("Invalid ID type: {$value}. Valid types are: " . implode(', ', self::all())); + } +} diff --git a/src/Framework/Ksuid/Ksuid.php b/src/Framework/Id/Ksuid/Ksuid.php similarity index 96% rename from src/Framework/Ksuid/Ksuid.php rename to src/Framework/Id/Ksuid/Ksuid.php index e15a5de6..fb0bd562 100644 --- a/src/Framework/Ksuid/Ksuid.php +++ b/src/Framework/Id/Ksuid/Ksuid.php @@ -2,11 +2,13 @@ declare(strict_types=1); -namespace App\Framework\Ksuid; +namespace App\Framework\Id\Ksuid; +use App\Framework\Id\Contracts\IdInterface; use BcMath\Number; use DateTimeImmutable; use InvalidArgumentException; +use Stringable; /** * KSUID Value Object @@ -17,7 +19,7 @@ use InvalidArgumentException; * - Lexicographically sortable by creation time * - URL-safe, case-sensitive */ -final readonly class Ksuid +final readonly class Ksuid implements IdInterface, Stringable { public const int ENCODED_LENGTH = 27; public const int TIMESTAMP_BYTES = 4; @@ -159,8 +161,12 @@ final readonly class Ksuid /** * Check equality with another KSUID */ - public function equals(self $other): bool + public function equals(IdInterface $other): bool { + if (! $other instanceof self) { + return false; + } + return $this->value === $other->value; } diff --git a/src/Framework/Ksuid/KsuidGenerator.php b/src/Framework/Id/Ksuid/KsuidGenerator.php similarity index 95% rename from src/Framework/Ksuid/KsuidGenerator.php rename to src/Framework/Id/Ksuid/KsuidGenerator.php index 05a72553..479a24c7 100644 --- a/src/Framework/Ksuid/KsuidGenerator.php +++ b/src/Framework/Id/Ksuid/KsuidGenerator.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\Framework\Ksuid; +namespace App\Framework\Id\Ksuid; +use App\Framework\Id\Contracts\IdGeneratorInterface; use App\Framework\Random\RandomGenerator; use DateTimeImmutable; use InvalidArgumentException; @@ -13,7 +14,7 @@ use InvalidArgumentException; * * Generates K-Sortable Unique Identifiers with timestamp ordering. */ -final readonly class KsuidGenerator +final readonly class KsuidGenerator implements IdGeneratorInterface { public function __construct( private RandomGenerator $randomGenerator @@ -183,7 +184,7 @@ final readonly class KsuidGenerator return Ksuid::fromTimestampAndPayload($timestamp, $payload); } - /**@return array{min: \App\Framework\Ksuid\Ksuid, max: \App\Framework\Ksuid\Ksuid} + /**@return array{min: \App\Framework\Id\Ksuid\Ksuid, max: \App\Framework\Id\Ksuid\Ksuid} * Generate KSUIDs for a time range (useful for queries) */ public function generateTimeRange(int $startTimestamp, int $endTimestamp): array diff --git a/src/Framework/NanoId/NanoId.php b/src/Framework/Id/NanoId/NanoId.php similarity index 93% rename from src/Framework/NanoId/NanoId.php rename to src/Framework/Id/NanoId/NanoId.php index e1c50c28..a6f4498b 100644 --- a/src/Framework/NanoId/NanoId.php +++ b/src/Framework/Id/NanoId/NanoId.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace App\Framework\NanoId; +namespace App\Framework\Id\NanoId; +use App\Framework\Id\Contracts\IdInterface; use InvalidArgumentException; -use Stringable; /** * NanoId Value Object @@ -14,7 +14,7 @@ use Stringable; * Default alphabet: A-Za-z0-9_- * Default size: 21 characters */ -final readonly class NanoId implements Stringable +final readonly class NanoId implements IdInterface { public const int DEFAULT_SIZE = 21; public const string DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-'; @@ -123,8 +123,12 @@ final readonly class NanoId implements Stringable /** * Check equality with another NanoId */ - public function equals(self $other): bool + public function equals(IdInterface $other): bool { + if (! $other instanceof self) { + return false; + } + return $this->value === $other->value; } diff --git a/src/Framework/NanoId/NanoIdGenerator.php b/src/Framework/Id/NanoId/NanoIdGenerator.php similarity index 97% rename from src/Framework/NanoId/NanoIdGenerator.php rename to src/Framework/Id/NanoId/NanoIdGenerator.php index 414cc1c4..7bb6fa4a 100644 --- a/src/Framework/NanoId/NanoIdGenerator.php +++ b/src/Framework/Id/NanoId/NanoIdGenerator.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\Framework\NanoId; +namespace App\Framework\Id\NanoId; +use App\Framework\Id\Contracts\IdGeneratorInterface; use App\Framework\Random\RandomGenerator; use InvalidArgumentException; @@ -12,7 +13,7 @@ use InvalidArgumentException; * * Provides flexible NanoId generation with various presets and configurations. */ -final readonly class NanoIdGenerator +final readonly class NanoIdGenerator implements IdGeneratorInterface { private int $defaultSize; diff --git a/src/Framework/Ulid/StringConverter.php b/src/Framework/Id/Ulid/StringConverter.php similarity index 97% rename from src/Framework/Ulid/StringConverter.php rename to src/Framework/Id/Ulid/StringConverter.php index 7bb22104..23c22309 100644 --- a/src/Framework/Ulid/StringConverter.php +++ b/src/Framework/Id/Ulid/StringConverter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Framework\Ulid; +namespace App\Framework\Id\Ulid; use App\Framework\Core\Encoding\Base32Alphabet; use App\Framework\Core\Encoding\Base32Encoder; diff --git a/src/Framework/Ulid/Ulid.php b/src/Framework/Id/Ulid/Ulid.php similarity index 78% rename from src/Framework/Ulid/Ulid.php rename to src/Framework/Id/Ulid/Ulid.php index 9da9a41a..77dc4c8c 100644 --- a/src/Framework/Ulid/Ulid.php +++ b/src/Framework/Id/Ulid/Ulid.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace App\Framework\Ulid; +namespace App\Framework\Id\Ulid; use App\Framework\DateTime\Clock; +use App\Framework\Id\Contracts\IdInterface; use DateTimeImmutable; use InvalidArgumentException; use JsonSerializable; @@ -12,7 +13,7 @@ use JsonSerializable; /** * Objekt-Wrapper für ULIDs mit String-/JSON-API. */ -final readonly class Ulid implements JsonSerializable +final readonly class Ulid implements IdInterface, JsonSerializable { private string $ulid; @@ -62,6 +63,34 @@ final readonly class Ulid implements JsonSerializable return $this->ulid; } + /** + * @inheritDoc + */ + public function toString(): string + { + return $this->ulid; + } + + /** + * @inheritDoc + */ + public function getValue(): string + { + return $this->ulid; + } + + /** + * @inheritDoc + */ + public function equals(IdInterface $other): bool + { + if (! $other instanceof self) { + return false; + } + + return $this->ulid === $other->ulid; + } + public function jsonSerialize(): string { return $this->ulid; diff --git a/src/Framework/Ulid/UlidGenerator.php b/src/Framework/Id/Ulid/UlidGenerator.php similarity index 69% rename from src/Framework/Ulid/UlidGenerator.php rename to src/Framework/Id/Ulid/UlidGenerator.php index db991889..01081fb4 100644 --- a/src/Framework/Ulid/UlidGenerator.php +++ b/src/Framework/Id/Ulid/UlidGenerator.php @@ -2,10 +2,11 @@ declare(strict_types=1); -namespace App\Framework\Ulid; +namespace App\Framework\Id\Ulid; use App\Framework\DateTime\Clock; use App\Framework\DateTime\SystemClock; +use App\Framework\Id\Contracts\IdGeneratorInterface; /** * ULID Generator - Universally Unique Lexicographically Sortable Identifier @@ -17,7 +18,7 @@ use App\Framework\DateTime\SystemClock; * - Production: new UlidGenerator() - uses SystemClock automatically * - Testing: new UlidGenerator($mockClock) - inject mock for deterministic tests */ -final readonly class UlidGenerator +final readonly class UlidGenerator implements IdGeneratorInterface { public function __construct( private ?Clock $clock = null @@ -61,4 +62,34 @@ final readonly class UlidGenerator { return $prefix . '_' . $this->generate(); } + + /** + * @inheritDoc + */ + public function generateBatch(int $count): array + { + if ($count <= 0) { + throw new \InvalidArgumentException('Count must be positive'); + } + + if ($count > 10000) { + throw new \InvalidArgumentException('Batch size cannot exceed 10000'); + } + + $ids = []; + + for ($i = 0; $i < $count; $i++) { + $ids[] = $this->generate(); + } + + return $ids; + } + + /** + * @inheritDoc + */ + public function isValid(string $value): bool + { + return \App\Framework\Id\Ulid\Ulid::isValid($value); + } } diff --git a/src/Framework/Ulid/UlidParser.php b/src/Framework/Id/Ulid/UlidParser.php similarity index 98% rename from src/Framework/Ulid/UlidParser.php rename to src/Framework/Id/Ulid/UlidParser.php index a0019b92..dd746ff3 100644 --- a/src/Framework/Ulid/UlidParser.php +++ b/src/Framework/Id/Ulid/UlidParser.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Framework\Ulid; +namespace App\Framework\Id\Ulid; use App\Framework\DateTime\Clock; use DateTimeImmutable; diff --git a/src/Framework/Ulid/UlidValidator.php b/src/Framework/Id/Ulid/UlidValidator.php similarity index 91% rename from src/Framework/Ulid/UlidValidator.php rename to src/Framework/Id/Ulid/UlidValidator.php index 1324d2a7..b79514cb 100644 --- a/src/Framework/Ulid/UlidValidator.php +++ b/src/Framework/Id/Ulid/UlidValidator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Framework\Ulid; +namespace App\Framework\Id\Ulid; /** * Validiert ULID-Strings. diff --git a/src/Framework/Id/UnifiedIdGenerator.php b/src/Framework/Id/UnifiedIdGenerator.php new file mode 100644 index 00000000..ed65e4ff --- /dev/null +++ b/src/Framework/Id/UnifiedIdGenerator.php @@ -0,0 +1,121 @@ +generateWithType($this->defaultType); + } + + /** + * Generate a new ID of the specified type + * + * @param IdType|string $type The ID type to generate + * @return IdInterface|string + */ + public function generateWithType(IdType|string $type): IdInterface|string + { + $generator = $this->factory->create($type); + + return $generator->generate(); + } + + /** + * Generate a batch of IDs using the default type + * + * @param int $count Number of IDs to generate + * @return array + */ + public function generateBatch(int $count): array + { + return $this->generateBatchWithType($count, $this->defaultType); + } + + /** + * Generate a batch of IDs of the specified type + * + * @param int $count Number of IDs to generate + * @param IdType|string $type The ID type to generate + * @return array + */ + public function generateBatchWithType(int $count, IdType|string $type): array + { + $generator = $this->factory->create($type); + + return $generator->generateBatch($count); + } + + /** + * Validate if a string is a valid ID for the default type + */ + public function isValid(string $value): bool + { + return $this->isValidForType($value, $this->defaultType); + } + + /** + * Validate if a string is a valid ID for the specified type + * + * @param string $value The ID string to validate + * @param IdType|string $type The ID type to validate against + */ + public function isValidForType(string $value, IdType|string $type): bool + { + $generator = $this->factory->create($type); + + return $generator->isValid($value); + } + + /** + * Get the default ID type + */ + public function getDefaultType(): IdType + { + return $this->defaultType; + } + + /** + * Create a unified generator with default settings + */ + public static function createDefault(?IdType $defaultType = null): self + { + return new self( + IdGeneratorFactory::createDefault(), + $defaultType ?? IdType::ULID + ); + } + + /** + * Create a unified generator with a custom factory + */ + public static function create(IdGeneratorFactory $factory, ?IdType $defaultType = null): self + { + return new self( + $factory, + $defaultType ?? IdType::ULID + ); + } +} diff --git a/src/Framework/Logging/Formatter/DevelopmentFormatter.php b/src/Framework/Logging/Formatter/DevelopmentFormatter.php index 2953e18a..a346571e 100644 --- a/src/Framework/Logging/Formatter/DevelopmentFormatter.php +++ b/src/Framework/Logging/Formatter/DevelopmentFormatter.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace App\Framework\Logging\Formatter; +use App\Framework\Console\CliSapi; +use App\Framework\Console\ConsoleStyle; +use App\Framework\Console\TerminalDetector; use App\Framework\Logging\LogLevel; use App\Framework\Logging\LogRecord; @@ -17,6 +20,21 @@ final readonly class DevelopmentFormatter implements LogFormatter private bool $colorOutput = true ) { } + + /** + * Prüft ob Farben verwendet werden sollen (berücksichtigt Terminal-Detection) + */ + private function shouldUseColors(): bool + { + // Wenn colorOutput explizit false, keine Farben + if (!$this->colorOutput) { + return false; + } + + // Prüfe Terminal-Detection mit CliSapi + $cliSapi = CliSapi::detect(); + return $cliSapi->supportsColors($cliSapi->stdout); + } public function __invoke(LogRecord $record): string { @@ -26,7 +44,7 @@ final readonly class DevelopmentFormatter implements LogFormatter $message = $record->getMessage(); // Color coding for levels - $levelString = $this->colorOutput ? $this->colorizeLevel($level) : $level->getName(); + $levelString = $this->shouldUseColors() ? $this->colorizeLevel($level) : $level->getName(); $output = sprintf( "%s [%s] %s.%s: %s\n", @@ -54,20 +72,11 @@ final readonly class DevelopmentFormatter implements LogFormatter private function colorizeLevel(LogLevel $level): string { - if (! $this->colorOutput) { - return $level->getName(); - } - - return match($level) { - LogLevel::DEBUG => "\033[36m" . $level->getName() . "\033[0m", // Cyan - LogLevel::INFO => "\033[32m" . $level->getName() . "\033[0m", // Green - LogLevel::NOTICE => "\033[34m" . $level->getName() . "\033[0m", // Blue - LogLevel::WARNING => "\033[33m" . $level->getName() . "\033[0m", // Yellow - LogLevel::ERROR => "\033[31m" . $level->getName() . "\033[0m", // Red - LogLevel::CRITICAL => "\033[35m" . $level->getName() . "\033[0m", // Magenta - LogLevel::ALERT => "\033[41m" . $level->getName() . "\033[0m", // Red background - LogLevel::EMERGENCY => "\033[41;37m" . $level->getName() . "\033[0m", // Red bg, white text - }; + // Verwende LogLevel::getConsoleColor() und ConsoleStyle statt hardcoded ANSI-Codes + $consoleColor = $level->getConsoleColor(); + $style = ConsoleStyle::create(color: $consoleColor); + + return $style->apply($level->getName()); } private function formatContext(array $context): string diff --git a/src/Framework/Logging/Formatter/LineFormatter.php b/src/Framework/Logging/Formatter/LineFormatter.php index aeb8a0f9..ac392071 100644 --- a/src/Framework/Logging/Formatter/LineFormatter.php +++ b/src/Framework/Logging/Formatter/LineFormatter.php @@ -4,15 +4,34 @@ declare(strict_types=1); namespace App\Framework\Logging\Formatter; +use App\Framework\Console\CliSapi; +use App\Framework\Console\ConsoleStyle; +use App\Framework\Console\TerminalDetector; use App\Framework\Logging\LogRecord; final readonly class LineFormatter implements LogFormatter { public function __construct( private string $format = "[{timestamp}] {channel}.{level}: {message} {context}", - private string $timestampFormat = 'Y-m-d H:i:s' + private string $timestampFormat = 'Y-m-d H:i:s', + private bool $colorOutput = false ) { } + + /** + * Prüft ob Farben verwendet werden sollen (berücksichtigt Terminal-Detection) + */ + private function shouldUseColors(): bool + { + // Wenn colorOutput explizit false, keine Farben + if (!$this->colorOutput) { + return false; + } + + // Prüfe Terminal-Detection mit CliSapi (für stderr, da Web-Requests auf stderr loggen) + $cliSapi = CliSapi::detect(); + return $cliSapi->supportsColors($cliSapi->stderr); + } public function __invoke(LogRecord $record): string { @@ -33,11 +52,17 @@ final readonly class LineFormatter implements LogFormatter ? "[{$record->getChannel()}] " : ''; + // Level-String mit optionalen Farben + $levelName = $record->level->getName(); + $coloredLevel = $this->shouldUseColors() + ? $this->colorizeLevel($record->level) + : $levelName; + $replacements = [ '{timestamp}' => $record->getFormattedTimestamp($this->timestampFormat), '{channel}' => $record->channel ?? 'app', - '{level}' => $record->level->getName(), - '{level_name}' => $record->level->getName(), + '{level}' => $coloredLevel, + '{level_name}' => $coloredLevel, '{message}' => $record->message, '{context}' => $contextString, '{request_id}' => $requestId, @@ -45,4 +70,16 @@ final readonly class LineFormatter implements LogFormatter return strtr($this->format, $replacements); } + + /** + * Färbt den Level-String basierend auf LogLevel + */ + private function colorizeLevel(\App\Framework\Logging\LogLevel $level): string + { + // Verwende LogLevel::getConsoleColor() und ConsoleStyle statt hardcoded ANSI-Codes + $consoleColor = $level->getConsoleColor(); + $style = ConsoleStyle::create(color: $consoleColor); + + return $style->apply($level->getName()); + } } diff --git a/src/Framework/Logging/HandlerFactory.php b/src/Framework/Logging/HandlerFactory.php new file mode 100644 index 00000000..c1b90814 --- /dev/null +++ b/src/Framework/Logging/HandlerFactory.php @@ -0,0 +1,190 @@ + + */ + public function createHandlers( + TypedConfiguration $config, + Environment $env, + LogConfig $logConfig, + PathProvider $pathProvider, + LogLevel $minLevel, + Queue $queue + ): array { + $handlers = []; + + // Console/Docker Logging Handler - für CLI und Web-Requests + if (PHP_SAPI === 'cli') { + // CLI: Docker JSON oder Console Handler + $handlers[] = $this->createCliHandler($config, $env, $minLevel); + } else { + // Web-Requests: Console Handler auf stderr mit intelligenter Formatter-Auswahl + $cliSapi = CliSapi::detect(); + $colorOutput = $this->determineColorOutput($env, $cliSapi->stderr); + $handlers[] = $this->createWebHandler($minLevel, $colorOutput); + } + + // MultiFileHandler für automatisches Channel-Routing + $multiFileFormatter = new LineFormatter(); + + $handlers[] = new MultiFileHandler( + $logConfig, + $pathProvider, + $multiFileFormatter, + $minLevel, + 0644 + ); + + // Fallback FileHandler für Kompatibilität (nur für 'app' Channel ohne Channel-Info) + $fileFormatter = new LineFormatter( + format: 'Line Formatter: [{timestamp}] [{level_name}] {request_id}{channel}{message}', + timestampFormat: 'Y-m-d H:i:s' + ); + $handlers[] = new FileHandler( + $fileFormatter, + $logConfig->getLogPath('app'), + $minLevel, + 0644, + null, + $pathProvider + ); + + return $handlers; + } + + /** + * Erstellt den CLI-Handler (Docker JSON oder Console) + */ + private function createCliHandler( + TypedConfiguration $config, + Environment $env, + LogLevel $minLevel + ): LogHandler { + // Prüfe ob wir in Docker laufen (für strukturierte JSON-Logs) + $inDocker = file_exists('/.dockerenv') || getenv('DOCKER_CONTAINER') === 'true'; + + if ($inDocker) { + if ($config->app->isProduction()) { + // Production Docker: Compact JSON für Log-Aggregatoren mit Redaction + return new DockerJsonHandler( + env: $env, + minLevel: $minLevel, + redactSensitiveData: true // Auto-redact in Production + ); + } else { + // Development Docker: Pretty JSON für bessere Lesbarkeit + return new DockerJsonHandler( + env: $env, + serviceName: $config->app->name ?? 'app', + minLevel: $minLevel, + prettyPrint: true, // Pretty-print für Development + redactSensitiveData: false // Keine Redaction in Development für Debugging + ); + } + } else { + // Lokale Entwicklung: Console Handler mit intelligenter Formatter-Auswahl + $cliSapi = CliSapi::detect(); + $colorOutput = $this->determineColorOutput($env, $cliSapi->stdout); + return $this->createCliConsoleHandler($minLevel, $colorOutput); + } + } + + /** + * Erstellt einen Web-Request ConsoleHandler mit intelligenter Formatter-Auswahl + */ + private function createWebHandler(LogLevel $minLevel, bool $colorOutput): ConsoleHandler + { + $developmentFormatter = new DevelopmentFormatter( + includeStackTrace: true, + colorOutput: $colorOutput + ); + + $lineFormatter = new LineFormatter( + colorOutput: $colorOutput + ); + + // Intelligente Formatter-Auswahl: Array mit beiden Formattern + return new ConsoleHandler( + [ + 'development' => $developmentFormatter, + 'line' => $lineFormatter, + ], + $minLevel, + debugOnly: false + ); + } + + /** + * Erstellt einen CLI ConsoleHandler mit intelligenter Formatter-Auswahl + */ + private function createCliConsoleHandler(LogLevel $minLevel, bool $colorOutput): ConsoleHandler + { + $developmentFormatter = new DevelopmentFormatter( + includeStackTrace: true, + colorOutput: $colorOutput + ); + + $lineFormatter = new LineFormatter( + colorOutput: $colorOutput + ); + + // Intelligente Formatter-Auswahl: Array mit beiden Formattern + return new ConsoleHandler( + [ + 'development' => $developmentFormatter, + 'line' => $lineFormatter, + ], + $minLevel + ); + } + + /** + * Bestimmt ob Farb-Output aktiviert werden soll + * + * @param Environment $env Environment für Konfiguration + * @param \App\Framework\Console\ValueObjects\TerminalStream $stream Terminal Stream für Terminal-Detection + * @return bool True wenn Farben aktiviert werden sollen + */ + private function determineColorOutput(Environment $env, \App\Framework\Console\ValueObjects\TerminalStream $stream): bool + { + $colorConfig = $env->get(EnvKey::LOG_COLOR_OUTPUT, 'auto'); + + return match (strtolower((string) $colorConfig)) { + 'true', '1', 'yes', 'on' => true, + 'false', '0', 'no', 'off' => false, + default => TerminalDetector::supportsColors($stream), // 'auto' oder default + }; + } +} + diff --git a/src/Framework/Logging/Handlers/ConsoleHandler.php b/src/Framework/Logging/Handlers/ConsoleHandler.php index a34f4ae4..2693e4bf 100644 --- a/src/Framework/Logging/Handlers/ConsoleHandler.php +++ b/src/Framework/Logging/Handlers/ConsoleHandler.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Framework\Logging\Handlers; use App\Framework\Logging\LogHandler; +use App\Framework\Logging\Formatter\DevelopmentFormatter; +use App\Framework\Logging\Formatter\LineFormatter; use App\Framework\Logging\Formatter\LogFormatter; use App\Framework\Logging\LogLevel; use App\Framework\Logging\LogRecord; @@ -12,6 +14,10 @@ use App\Framework\Logging\LogRecord; /** * Handler für die Ausgabe von Log-Einträgen in der Konsole. * + * Unterstützt intelligente Formatter-Auswahl basierend auf LogLevel und Context: + * - ERROR/CRITICAL/ALERT/EMERGENCY oder Exception → DevelopmentFormatter (detailliert) + * - DEBUG/INFO/NOTICE/WARNING → LineFormatter (kompakt) + * * Bei CLI: Nutzt stderr für WARNING+ und stdout für niedrigere Levels. * Bei Web-Requests: Alle Logs gehen auf stderr (POSIX-konform, Docker-kompatibel). */ @@ -28,26 +34,53 @@ class ConsoleHandler implements LogHandler private bool $debugOnly; /** - * @var LogFormatter Formatter für die Log-Ausgabe + * @var LogFormatter|null Formatter für die Log-Ausgabe (bei einfachem Modus) */ - private LogFormatter $formatter; + private ?LogFormatter $formatter = null; + + /** + * @var DevelopmentFormatter|null Formatter für detaillierte Ausgabe (bei intelligentem Modus) + */ + private ?DevelopmentFormatter $developmentFormatter = null; + + /** + * @var LineFormatter|null Formatter für kompakte Ausgabe (bei intelligentem Modus) + */ + private ?LineFormatter $lineFormatter = null; + + /** + * @var bool Ob intelligente Formatter-Auswahl aktiviert ist + */ + private bool $intelligentSelection; /** * Erstellt einen neuen ConsoleHandler * - * @param LogFormatter $formatter Formatter für die Log-Ausgabe + * @param LogFormatter|array{development: DevelopmentFormatter, line: LineFormatter}|null $formatter + * Einzelner Formatter (rückwärtskompatibel) oder Array mit 'development' und 'line' Formattern für intelligente Auswahl * @param LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird * @param bool $debugOnly Ob der Handler nur im Debug-Modus aktiv ist + * @param LogLevel $stderrLevel Level ab dem stderr verwendet wird (default: WARNING) */ public function __construct( - LogFormatter $formatter, + LogFormatter|array|null $formatter = null, LogLevel|int $minLevel = LogLevel::DEBUG, bool $debugOnly = true, private readonly LogLevel $stderrLevel = LogLevel::WARNING, ) { - $this->formatter = $formatter; $this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel); $this->debugOnly = $debugOnly; + + // Intelligente Formatter-Auswahl wenn Array übergeben wird + if (is_array($formatter)) { + $this->intelligentSelection = true; + $this->developmentFormatter = $formatter['development'] ?? null; + $this->lineFormatter = $formatter['line'] ?? null; + } else { + // Rückwärtskompatibel: Einzelner Formatter + $this->intelligentSelection = false; + $this->formatter = $formatter; + } } /** @@ -68,8 +101,11 @@ class ConsoleHandler implements LogHandler */ public function handle(LogRecord $record): void { + // Formatter auswählen (intelligent oder einfach) + $formatter = $this->selectFormatter($record); + // Formatter verwenden für Formatierung - $formatted = ($this->formatter)($record); + $formatted = $formatter($record); // Formatter gibt immer string zurück für Console $output = is_string($formatted) @@ -96,9 +132,57 @@ class ConsoleHandler implements LogHandler } /** - * Gibt den Formatter zurück + * Wählt den passenden Formatter basierend auf LogLevel und Context */ - public function getFormatter(): LogFormatter + private function selectFormatter(LogRecord $record): LogFormatter + { + // Wenn kein intelligenter Modus, verwende den einzelnen Formatter + if (!$this->intelligentSelection) { + if ($this->formatter === null) { + throw new \RuntimeException('ConsoleHandler: No formatter configured'); + } + return $this->formatter; + } + + // Prüfe ob Exception vorhanden ist + if ($this->hasException($record)) { + return $this->developmentFormatter ?? $this->lineFormatter ?? throw new \RuntimeException('ConsoleHandler: No formatter configured'); + } + + // Für ERROR, CRITICAL, ALERT, EMERGENCY: DevelopmentFormatter (detailliert) + if ($record->level->value >= LogLevel::ERROR->value) { + return $this->developmentFormatter ?? $this->lineFormatter ?? throw new \RuntimeException('ConsoleHandler: No formatter configured'); + } + + // Für DEBUG, INFO, NOTICE, WARNING: LineFormatter (kompakt) + return $this->lineFormatter ?? $this->developmentFormatter ?? throw new \RuntimeException('ConsoleHandler: No formatter configured'); + } + + /** + * Prüft ob der LogRecord eine Exception enthält + */ + private function hasException(LogRecord $record): bool + { + // Prüfe in extras + if ($record->hasExtra('exception_class') || $record->hasExtra('exception')) { + return true; + } + + // Prüfe in context + $context = $record->getContext(); + if (isset($context['exception']) || isset($context['exception_class'])) { + return true; + } + + return false; + } + + /** + * Gibt den Formatter zurück (rückwärtskompatibel) + * + * @deprecated Verwende selectFormatter() für intelligente Auswahl + */ + public function getFormatter(): ?LogFormatter { return $this->formatter; } diff --git a/src/Framework/Logging/LoggerFactory.php b/src/Framework/Logging/LoggerFactory.php deleted file mode 100644 index affa559f..00000000 --- a/src/Framework/Logging/LoggerFactory.php +++ /dev/null @@ -1,61 +0,0 @@ -determineMinLogLevel($config); $logConfig = $this->initializeLogConfig($pathProvider); - $queue = $this->createQueue($pathProvider); - $handlers = $this->createHandlers($config, $env, $logConfig, $pathProvider, $minLevel, $queue); + + $queueFactory = new QueueFactory(); + $queue = $queueFactory->createQueue($pathProvider, $env); + + $handlerFactory = new HandlerFactory(); + $handlers = $handlerFactory->createHandlers($config, $env, $logConfig, $pathProvider, $minLevel, $queue); + $contextManager = $container->get(LogContextManager::class); $clock = $container->get(Clock::class); @@ -113,117 +108,4 @@ final readonly class LoggerInitializer return $logConfig; } - /** - * Erstellt die Queue für asynchrones Logging - */ - private function createQueue(PathProvider $pathProvider): Queue - { - #$redisConfig = RedisConfig::fromEnvironment($env); - #$redisConnection = new RedisConnection($redisConfig, 'queue'); - - #return new RedisQueue($redisConnection, 'commands'); - - $queuePath = $pathProvider->resolvePath('storage/queue'); - return new FileQueue($queuePath); - } - - /** - * Erstellt alle Handler basierend auf der Umgebung - * - * @param TypedConfiguration $config - * @param Environment $env - * @param LogConfig $logConfig - * @param PathProvider $pathProvider - * @param LogLevel $minLevel - * @param Queue $queue - * @return array - */ - private function createHandlers( - TypedConfiguration $config, - Environment $env, - LogConfig $logConfig, - PathProvider $pathProvider, - LogLevel $minLevel, - Queue $queue - ): array { - $handlers = []; - - // Console/Docker Logging Handler - für CLI und Web-Requests - if (PHP_SAPI === 'cli') { - // CLI: Docker JSON oder Console Handler - $handlers[] = $this->createCliHandler($config, $env, $minLevel); - } else { - // Web-Requests: Console Handler auf stderr - $webFormatter = new LineFormatter(); - $handlers[] = new ConsoleHandler($webFormatter, $minLevel, debugOnly: false); - } - - //$handlers[] = new QueuedLogHandler($queue); - - // MultiFileHandler für automatisches Channel-Routing - $multiFileFormatter = new LineFormatter(); - - $handlers[] = new MultiFileHandler( - $logConfig, - $pathProvider, - $multiFileFormatter, - $minLevel, - 0644 - ); - - // Fallback FileHandler für Kompatibilität (nur für 'app' Channel ohne Channel-Info) - $fileFormatter = new LineFormatter( - format: 'Line Formatter: [{timestamp}] [{level_name}] {request_id}{channel}{message}', - timestampFormat: 'Y-m-d H:i:s' - ); - $handlers[] = new FileHandler( - $fileFormatter, - $logConfig->getLogPath('app'), - $minLevel, - 0644, - null, - $pathProvider - ); - - return $handlers; - } - - /** - * Erstellt den CLI-Handler (Docker JSON oder Console) - */ - private function createCliHandler( - TypedConfiguration $config, - Environment $env, - LogLevel $minLevel - ): LogHandler { - // Prüfe ob wir in Docker laufen (für strukturierte JSON-Logs) - $inDocker = file_exists('/.dockerenv') || getenv('DOCKER_CONTAINER') === 'true'; - - if ($inDocker) { - if ($config->app->isProduction()) { - // Production Docker: Compact JSON für Log-Aggregatoren mit Redaction - return new DockerJsonHandler( - env: $env, - minLevel: $minLevel, - redactSensitiveData: true // Auto-redact in Production - ); - } else { - // Development Docker: Pretty JSON für bessere Lesbarkeit - return new DockerJsonHandler( - env: $env, - serviceName: $config->app->name ?? 'app', - minLevel: $minLevel, - prettyPrint: true, // Pretty-print für Development - redactSensitiveData: false // Keine Redaction in Development für Debugging - ); - } - } else { - // Lokale Entwicklung: Farbige Console-Logs - $consoleFormatter = new DevelopmentFormatter( - includeStackTrace: true, - colorOutput: true - ); - return new ConsoleHandler($consoleFormatter, $minLevel); - } - } } diff --git a/src/Framework/Logging/QueueFactory.php b/src/Framework/Logging/QueueFactory.php new file mode 100644 index 00000000..3e4b1364 --- /dev/null +++ b/src/Framework/Logging/QueueFactory.php @@ -0,0 +1,39 @@ +shouldUseRedisQueue($env)) { + // $redisConfig = RedisConfig::fromEnvironment($env); + // $redisConnection = new RedisConnection($redisConfig, 'queue'); + // return new RedisQueue($redisConnection, 'commands'); + // } + + $queuePath = $pathProvider->resolvePath('storage/queue'); + return new FileQueue($queuePath); + } +} + diff --git a/src/Framework/Logging/ValueObjects/CorrelationId.php b/src/Framework/Logging/ValueObjects/CorrelationId.php index 6cab7d1a..b7412907 100644 --- a/src/Framework/Logging/ValueObjects/CorrelationId.php +++ b/src/Framework/Logging/ValueObjects/CorrelationId.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Framework\Logging\ValueObjects; use App\Framework\Http\ServerRequest; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; use Ramsey\Uuid\Uuid; /** diff --git a/src/Framework/MachineLearning/ModelManagement/CachePerformanceStorage.php b/src/Framework/MachineLearning/ModelManagement/CachePerformanceStorage.php index 2846da18..0d2f88cd 100644 --- a/src/Framework/MachineLearning/ModelManagement/CachePerformanceStorage.php +++ b/src/Framework/MachineLearning/ModelManagement/CachePerformanceStorage.php @@ -7,10 +7,9 @@ namespace App\Framework\MachineLearning\ModelManagement; use App\Framework\Cache\Cache; use App\Framework\Cache\CacheItem; use App\Framework\Cache\CacheKey; -use App\Framework\Core\ValueObjects\Version; use App\Framework\Core\ValueObjects\Duration; use App\Framework\Core\ValueObjects\Timestamp; -use App\Framework\Ulid\UlidGenerator; +use App\Framework\Core\ValueObjects\Version; /** * Cache-based Performance Storage diff --git a/src/Framework/MagicLinks/Services/CacheMagicLinkService.php b/src/Framework/MagicLinks/Services/CacheMagicLinkService.php index 6f8901c0..cedf51cf 100644 --- a/src/Framework/MagicLinks/Services/CacheMagicLinkService.php +++ b/src/Framework/MagicLinks/Services/CacheMagicLinkService.php @@ -8,12 +8,12 @@ use App\Framework\Cache\Cache; use App\Framework\Cache\CacheKey; use App\Framework\Core\ValueObjects\Duration; use App\Framework\DateTime\Clock; +use App\Framework\Id\Ulid\UlidGenerator; use App\Framework\MagicLinks\MagicLinkData; use App\Framework\MagicLinks\MagicLinkToken; use App\Framework\MagicLinks\TokenAction; use App\Framework\MagicLinks\TokenConfig; use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload; -use App\Framework\Ulid\UlidGenerator; use DateTimeImmutable; final readonly class CacheMagicLinkService implements MagicLinkService diff --git a/src/Framework/MagicLinks/Services/InMemoryMagicLinkService.php b/src/Framework/MagicLinks/Services/InMemoryMagicLinkService.php index 5feff15d..4400a1a0 100644 --- a/src/Framework/MagicLinks/Services/InMemoryMagicLinkService.php +++ b/src/Framework/MagicLinks/Services/InMemoryMagicLinkService.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace App\Framework\MagicLinks\Services; use App\Framework\DateTime\Clock; +use App\Framework\Id\Ulid\UlidGenerator; use App\Framework\MagicLinks\MagicLinkData; use App\Framework\MagicLinks\MagicLinkToken; use App\Framework\MagicLinks\TokenAction; use App\Framework\MagicLinks\TokenConfig; use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload; -use App\Framework\Ulid\UlidGenerator; final class InMemoryMagicLinkService implements MagicLinkService { diff --git a/src/Framework/Mail/SmtpTransport.php b/src/Framework/Mail/SmtpTransport.php index 0213bf8a..6520a438 100644 --- a/src/Framework/Mail/SmtpTransport.php +++ b/src/Framework/Mail/SmtpTransport.php @@ -260,7 +260,7 @@ final class SmtpTransport implements TransportInterface private function buildMultipartAlternativeMessage(Message $message, array $lines): string { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $boundary = 'alt_' . $generator->generate(); $lines[] = 'MIME-Version: 1.0'; @@ -292,7 +292,7 @@ final class SmtpTransport implements TransportInterface private function buildMultipartMixedMessage(Message $message, array $lines): string { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $boundary = 'mixed_' . $generator->generate(); $lines[] = 'MIME-Version: 1.0'; @@ -377,7 +377,7 @@ final class SmtpTransport implements TransportInterface private function generateMessageId(): string { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); return $generator->generate() . '.' . time() . '@' . gethostname(); } @@ -415,7 +415,7 @@ final class SmtpTransport implements TransportInterface } // Fallback to generated ID - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); return $generator->generate() . '@' . gethostname(); } diff --git a/src/Framework/Mail/Testing/MockTransport.php b/src/Framework/Mail/Testing/MockTransport.php index 0d522781..8dcb7280 100644 --- a/src/Framework/Mail/Testing/MockTransport.php +++ b/src/Framework/Mail/Testing/MockTransport.php @@ -27,7 +27,7 @@ final class MockTransport implements TransportInterface ); } - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $messageId = 'mock_' . $generator->generate(); $this->sentMessages[] = [ 'message' => $message, diff --git a/src/Framework/Notification/Templates/TemplateId.php b/src/Framework/Notification/Templates/TemplateId.php index 4300dced..769e06b2 100644 --- a/src/Framework/Notification/Templates/TemplateId.php +++ b/src/Framework/Notification/Templates/TemplateId.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Framework\Notification\Templates; -use App\Framework\Ulid\UlidGenerator; +use App\Framework\Id\Ulid\UlidGenerator; /** * Template Identifier Value Object diff --git a/src/Framework/Notification/ValueObjects/NotificationId.php b/src/Framework/Notification/ValueObjects/NotificationId.php index bb8902ed..30a04d81 100644 --- a/src/Framework/Notification/ValueObjects/NotificationId.php +++ b/src/Framework/Notification/ValueObjects/NotificationId.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace App\Framework\Notification\ValueObjects; use App\Framework\DateTime\SystemClock; -use App\Framework\Ulid\UlidGenerator; -use App\Framework\Ulid\UlidValidator; +use App\Framework\Id\Ulid\UlidGenerator; +use App\Framework\Id\Ulid\UlidValidator; /** * Unique identifier for notifications diff --git a/src/Framework/Queue/Entities/DeadLetterJob.php b/src/Framework/Queue/Entities/DeadLetterJob.php index fbf7c8d8..c2181098 100644 --- a/src/Framework/Queue/Entities/DeadLetterJob.php +++ b/src/Framework/Queue/Entities/DeadLetterJob.php @@ -7,11 +7,11 @@ namespace App\Framework\Queue\Entities; use App\Framework\Database\Attributes\Column; use App\Framework\Database\Attributes\Entity; use App\Framework\Database\Attributes\Id; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Queue\ValueObjects\DeadLetterQueueName; use App\Framework\Queue\ValueObjects\FailureReason; use App\Framework\Queue\ValueObjects\JobPayload; use App\Framework\Queue\ValueObjects\QueueName; -use App\Framework\Ulid\Ulid; /** * Entity representing a job that failed and was moved to the dead letter queue diff --git a/src/Framework/Queue/Entities/JobChainEntry.php b/src/Framework/Queue/Entities/JobChainEntry.php index c33b098b..43ec9b07 100644 --- a/src/Framework/Queue/Entities/JobChainEntry.php +++ b/src/Framework/Queue/Entities/JobChainEntry.php @@ -7,9 +7,9 @@ namespace App\Framework\Queue\Entities; use App\Framework\Database\Attributes\Column; use App\Framework\Database\Attributes\Entity; use App\Framework\Database\Attributes\Id; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Queue\ValueObjects\ChainExecutionMode; use App\Framework\Queue\ValueObjects\JobChain; -use App\Framework\Ulid\Ulid; /** * Entity representing a job chain entry in the database diff --git a/src/Framework/Queue/Entities/JobDependencyEntry.php b/src/Framework/Queue/Entities/JobDependencyEntry.php index 93f47d06..50eaee93 100644 --- a/src/Framework/Queue/Entities/JobDependencyEntry.php +++ b/src/Framework/Queue/Entities/JobDependencyEntry.php @@ -7,9 +7,9 @@ namespace App\Framework\Queue\Entities; use App\Framework\Database\Attributes\Column; use App\Framework\Database\Attributes\Entity; use App\Framework\Database\Attributes\Id; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Queue\ValueObjects\DependencyType; use App\Framework\Queue\ValueObjects\JobDependency; -use App\Framework\Ulid\Ulid; /** * Entity representing a job dependency entry in the database diff --git a/src/Framework/Queue/Entities/JobMetricsEntry.php b/src/Framework/Queue/Entities/JobMetricsEntry.php index a4ef924a..1063e636 100644 --- a/src/Framework/Queue/Entities/JobMetricsEntry.php +++ b/src/Framework/Queue/Entities/JobMetricsEntry.php @@ -7,8 +7,8 @@ namespace App\Framework\Queue\Entities; use App\Framework\Database\Attributes\Column; use App\Framework\Database\Attributes\Entity; use App\Framework\Database\Attributes\Id; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Queue\ValueObjects\JobMetrics; -use App\Framework\Ulid\Ulid; #[Entity(table: 'job_metrics')] final readonly class JobMetricsEntry diff --git a/src/Framework/Queue/Entities/JobProgressEntry.php b/src/Framework/Queue/Entities/JobProgressEntry.php index 882eecc4..6e7d616c 100644 --- a/src/Framework/Queue/Entities/JobProgressEntry.php +++ b/src/Framework/Queue/Entities/JobProgressEntry.php @@ -8,8 +8,8 @@ use App\Framework\Core\ValueObjects\Percentage; use App\Framework\Database\Attributes\Column; use App\Framework\Database\Attributes\Entity; use App\Framework\Database\Attributes\Id; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Queue\ValueObjects\JobProgress; -use App\Framework\Ulid\Ulid; /** * Entity representing a job progress tracking entry diff --git a/src/Framework/Queue/FileQueue.php b/src/Framework/Queue/FileQueue.php index d2fadcba..c65cfcff 100644 --- a/src/Framework/Queue/FileQueue.php +++ b/src/Framework/Queue/FileQueue.php @@ -339,7 +339,7 @@ final readonly class FileQueue implements Queue private function generatePriorityFilename(float $score): string { $scoreStr = str_pad((string) (int) ($score * 1000000), 15, '0', STR_PAD_LEFT); - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); return "job_{$scoreStr}_" . $generator->generate() . '.json'; } @@ -349,7 +349,7 @@ final readonly class FileQueue implements Queue */ private function generateDelayedFilename(int $availableTime): string { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); return "delayed_{$availableTime}_" . $generator->generate() . '.json'; } diff --git a/src/Framework/Queue/MachineLearning/QueueJobFeatureExtractor.php b/src/Framework/Queue/MachineLearning/QueueJobFeatureExtractor.php index 12d3e12f..37f58252 100644 --- a/src/Framework/Queue/MachineLearning/QueueJobFeatureExtractor.php +++ b/src/Framework/Queue/MachineLearning/QueueJobFeatureExtractor.php @@ -218,7 +218,7 @@ final readonly class QueueJobFeatureExtractor foreach ($metricsHistory as $metrics) { // Create minimal metadata from metrics $metadata = new JobMetadata( - id: new \App\Framework\Ulid\Ulid(new \App\Framework\DateTime\SystemClock()), + id: new \App\Framework\Id\Ulid\Ulid(new \App\Framework\DateTime\SystemClock()), class: \App\Framework\Core\ValueObjects\ClassName::create($metrics->queueName), type: 'job', queuedAt: \App\Framework\Core\ValueObjects\Timestamp::now(), diff --git a/src/Framework/Queue/Services/DatabaseJobBatchManager.php b/src/Framework/Queue/Services/DatabaseJobBatchManager.php index bdbb056a..dc5f3188 100644 --- a/src/Framework/Queue/Services/DatabaseJobBatchManager.php +++ b/src/Framework/Queue/Services/DatabaseJobBatchManager.php @@ -245,7 +245,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface private function generateBatchId(): string { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); return 'batch_' . $generator->generate(); } } diff --git a/src/Framework/Queue/ValueObjects/JobId.php b/src/Framework/Queue/ValueObjects/JobId.php index cc4ff441..44d5de9c 100644 --- a/src/Framework/Queue/ValueObjects/JobId.php +++ b/src/Framework/Queue/ValueObjects/JobId.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Framework\Queue\ValueObjects; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; /** * Value Object representing a unique Job identifier diff --git a/src/Framework/Queue/ValueObjects/JobMetadata.php b/src/Framework/Queue/ValueObjects/JobMetadata.php index fd6f0cfd..1463938f 100644 --- a/src/Framework/Queue/ValueObjects/JobMetadata.php +++ b/src/Framework/Queue/ValueObjects/JobMetadata.php @@ -8,7 +8,7 @@ use App\Framework\Core\ValueObjects\ClassName; use App\Framework\Core\ValueObjects\Duration; use App\Framework\Core\ValueObjects\Timestamp; use App\Framework\DateTime\SystemClock; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; /** * Job Metadata Value Object diff --git a/src/Framework/Queue/ValueObjects/WorkerId.php b/src/Framework/Queue/ValueObjects/WorkerId.php index e9b8d54e..0340aa69 100644 --- a/src/Framework/Queue/ValueObjects/WorkerId.php +++ b/src/Framework/Queue/ValueObjects/WorkerId.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Framework\Queue\ValueObjects; -use App\Framework\Ulid\UlidGenerator; +use App\Framework\Id\Ulid\UlidGenerator; /** * Value Object representing a unique Worker identifier diff --git a/src/Framework/Tracing/Exporters/ConsoleTraceExporter.php b/src/Framework/Tracing/Exporters/ConsoleTraceExporter.php index 872c3db2..ff219e22 100644 --- a/src/Framework/Tracing/Exporters/ConsoleTraceExporter.php +++ b/src/Framework/Tracing/Exporters/ConsoleTraceExporter.php @@ -57,7 +57,7 @@ final readonly class ConsoleTraceExporter implements TraceExporter $spanMap = []; $rootSpans = []; - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); // First, create a map of all spans foreach ($spans as $span) { $spanId = $span['spanId'] ?? $generator->generate(); diff --git a/src/Framework/Tracing/Exporters/DatabaseTraceExporter.php b/src/Framework/Tracing/Exporters/DatabaseTraceExporter.php index 0d8d87db..10c1f576 100644 --- a/src/Framework/Tracing/Exporters/DatabaseTraceExporter.php +++ b/src/Framework/Tracing/Exporters/DatabaseTraceExporter.php @@ -68,7 +68,7 @@ final readonly class DatabaseTraceExporter implements TraceExporter ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) "; - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $traceId = $traceData['traceId'] ?? $generator->generate(); $startTime = $traceData['startTime'] ?? microtime(true); $endTime = $traceData['endTime'] ?? ($startTime + ($traceData['duration'] ?? 0)); @@ -120,7 +120,7 @@ final readonly class DatabaseTraceExporter implements TraceExporter // Remove trailing comma $sql = rtrim($sql, ','); - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $values = []; foreach ($spans as $span) { $spanStartTime = $span['startTime'] ?? microtime(true); @@ -210,8 +210,8 @@ final readonly class DatabaseTraceExporter implements TraceExporter } $spansSql = " - SELECT * FROM {$this->spansTable} - WHERE trace_id = ? + SELECT * FROM {$this->spansTable} + WHERE trace_id = ? ORDER BY start_time ASC "; $spans = $this->connection->fetchAll($spansSql, [$trace['id']]); @@ -278,9 +278,9 @@ final readonly class DatabaseTraceExporter implements TraceExporter $sql = " SELECT trace_id, start_time, end_time, duration, span_count, error_count, status - FROM {$this->tracesTable} + FROM {$this->tracesTable} {$whereClause} - ORDER BY start_time DESC + ORDER BY start_time DESC LIMIT ? OFFSET ? "; diff --git a/src/Framework/Tracing/Exporters/JaegerExporter.php b/src/Framework/Tracing/Exporters/JaegerExporter.php index 087c7c15..e66fdf03 100644 --- a/src/Framework/Tracing/Exporters/JaegerExporter.php +++ b/src/Framework/Tracing/Exporters/JaegerExporter.php @@ -46,7 +46,7 @@ final readonly class JaegerExporter implements TraceExporter private function convertToJaegerFormat(array $traceData): array { - $generator = new \App\Framework\Ulid\UlidGenerator(); + $generator = new \App\Framework\Id\Ulid\UlidGenerator(); $traceId = $traceData['traceId'] ?? $generator->generate(); $spans = []; diff --git a/src/Framework/TypeCaster/Casters/UlidCaster.php b/src/Framework/TypeCaster/Casters/UlidCaster.php index 9470cbe8..527cd33d 100644 --- a/src/Framework/TypeCaster/Casters/UlidCaster.php +++ b/src/Framework/TypeCaster/Casters/UlidCaster.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace App\Framework\TypeCaster\Casters; use App\Framework\DateTime\SystemClock; +use App\Framework\Id\Ulid\Ulid; use App\Framework\TypeCaster\TypeCasterInterface; -use App\Framework\Ulid\Ulid; use InvalidArgumentException; final readonly class UlidCaster implements TypeCasterInterface diff --git a/src/Framework/Validation/Rules/Ulid.php b/src/Framework/Validation/Rules/Ulid.php index 59dc18a3..89834275 100644 --- a/src/Framework/Validation/Rules/Ulid.php +++ b/src/Framework/Validation/Rules/Ulid.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Framework\Validation\Rules; -use App\Framework\Ulid\UlidValidator; +use App\Framework\Id\Ulid\UlidValidator; use App\Framework\Validation\ValidationRule; use Attribute; diff --git a/src/Framework/Vault/DatabaseVault.php b/src/Framework/Vault/DatabaseVault.php index 235a568e..12161d78 100644 --- a/src/Framework/Vault/DatabaseVault.php +++ b/src/Framework/Vault/DatabaseVault.php @@ -8,7 +8,7 @@ use App\Framework\Database\ConnectionInterface; use App\Framework\Database\ValueObjects\SqlQuery; use App\Framework\DateTime\Clock; use App\Framework\Http\IpAddress; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; use App\Framework\UserAgent\UserAgent; use App\Framework\Vault\Exceptions\VaultException; use App\Framework\Vault\Exceptions\VaultKeyNotFoundException; diff --git a/tests/Feature/ImageApiControllerTest.php b/tests/Feature/ImageApiControllerTest.php index 2b7ef92a..340add04 100644 --- a/tests/Feature/ImageApiControllerTest.php +++ b/tests/Feature/ImageApiControllerTest.php @@ -12,7 +12,7 @@ use App\Framework\Http\Exception\NotFound; use App\Framework\Http\HttpRequest; use App\Framework\Http\MimeType; use App\Framework\Http\Responses\JsonResponse; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; beforeEach(function () { $this->imageRepository = Mockery::mock(ImageRepository::class); diff --git a/tests/Feature/ImageSystemTest.php b/tests/Feature/ImageSystemTest.php index 6b2f5ec3..004394c0 100644 --- a/tests/Feature/ImageSystemTest.php +++ b/tests/Feature/ImageSystemTest.php @@ -8,7 +8,7 @@ use App\Framework\Core\ValueObjects\Hash; use App\Framework\DateTime\SystemClock; use App\Framework\Filesystem\ValueObjects\FilePath; use App\Framework\Http\MimeType; -use App\Framework\Ulid\Ulid; +use App\Framework\Id\Ulid\Ulid; beforeEach(function () { // Create test directory structure diff --git a/tests/Feature/ShowImageControllerTest.php b/tests/Feature/ShowImageControllerTest.php index 7def863c..e09a66df 100644 --- a/tests/Feature/ShowImageControllerTest.php +++ b/tests/Feature/ShowImageControllerTest.php @@ -13,8 +13,8 @@ use App\Framework\Exception\FrameworkException; use App\Framework\Filesystem\ValueObjects\FilePath; use App\Framework\Http\HttpRequest; use App\Framework\Http\MimeType; +use App\Framework\Id\Ulid\Ulid; use App\Framework\Router\Result\FileResult; -use App\Framework\Ulid\Ulid; beforeEach(function () { // Create test directory and file diff --git a/tests/Framework/Queue/ValueObjects/JobIdTest.php b/tests/Framework/Queue/ValueObjects/JobIdTest.php index b8dfd9ff..f5600729 100644 --- a/tests/Framework/Queue/ValueObjects/JobIdTest.php +++ b/tests/Framework/Queue/ValueObjects/JobIdTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); +use App\Framework\Id\Ulid\Ulid; use App\Framework\Queue\ValueObjects\JobId; -use App\Framework\Ulid\Ulid; describe('JobId Value Object', function () { diff --git a/tests/Unit/Framework/Core/System/Ini/AccessTest.php b/tests/Unit/Framework/Core/System/Ini/AccessTest.php new file mode 100644 index 00000000..e1b68974 --- /dev/null +++ b/tests/Unit/Framework/Core/System/Ini/AccessTest.php @@ -0,0 +1,44 @@ +toBe(Access::USER); + }); + + it('converts INI_PERDIR bitmask to PERDIR enum', function () { + $access = Access::fromBitmask(INI_PERDIR); + + expect($access)->toBe(Access::PERDIR); + }); + + it('converts INI_SYSTEM bitmask to SYSTEM enum', function () { + $access = Access::fromBitmask(INI_SYSTEM); + + expect($access)->toBe(Access::SYSTEM); + }); + + it('converts INI_ALL bitmask to ALL enum', function () { + $access = Access::fromBitmask(INI_ALL); + + expect($access)->toBe(Access::ALL); + }); + + it('throws exception for invalid bitmask', function () { + expect(fn () => Access::fromBitmask(999)) + ->toThrow(InvalidArgumentException::class, 'Invalid bitmask value: 999'); + }); + + it('has correct string values', function () { + expect(Access::USER->value)->toBe('USER'); + expect(Access::PERDIR->value)->toBe('Per Directory'); + expect(Access::SYSTEM->value)->toBe('System'); + expect(Access::ALL->value)->toBe('All'); + }); +}); + diff --git a/tests/Unit/Framework/Core/System/Ini/IniDirectiveTest.php b/tests/Unit/Framework/Core/System/Ini/IniDirectiveTest.php new file mode 100644 index 00000000..a00fddc1 --- /dev/null +++ b/tests/Unit/Framework/Core/System/Ini/IniDirectiveTest.php @@ -0,0 +1,95 @@ +getAccess(); + + expect($access)->toBeInstanceOf(Access::class); + expect($access)->toBe(Access::USER); + }); + + it('returns access mask from getAccessMask()', function () { + $directive = new IniDirective( + name: 'test_directive', + value: 'test_value', + global: 'global_value', + accessMask: INI_USER + ); + + $mask = $directive->getAccessMask(); + + expect($mask)->toBeInt(); + expect($mask)->toBe(INI_USER); + }); + + it('correctly converts INI_USER bitmask', function () { + $directive = new IniDirective( + name: 'test_directive', + value: 'test_value', + global: 'global_value', + accessMask: INI_USER + ); + + expect($directive->getAccess())->toBe(Access::USER); + }); + + it('correctly converts INI_PERDIR bitmask', function () { + $directive = new IniDirective( + name: 'test_directive', + value: 'test_value', + global: 'global_value', + accessMask: INI_PERDIR + ); + + expect($directive->getAccess())->toBe(Access::PERDIR); + }); + + it('correctly converts INI_SYSTEM bitmask', function () { + $directive = new IniDirective( + name: 'test_directive', + value: 'test_value', + global: 'global_value', + accessMask: INI_SYSTEM + ); + + expect($directive->getAccess())->toBe(Access::SYSTEM); + }); + + it('correctly converts INI_ALL bitmask', function () { + $directive = new IniDirective( + name: 'test_directive', + value: 'test_value', + global: 'global_value', + accessMask: INI_ALL + ); + + expect($directive->getAccess())->toBe(Access::ALL); + }); + + it('stores and returns all properties correctly', function () { + $directive = new IniDirective( + name: 'my_directive', + value: 'my_value', + global: 'global_value', + accessMask: INI_USER + ); + + expect($directive->name)->toBe('my_directive'); + expect($directive->value)->toBe('my_value'); + expect($directive->global)->toBe('global_value'); + expect($directive->getAccessMask())->toBe(INI_USER); + }); +}); + diff --git a/tests/Unit/Framework/Core/System/Ini/IniKeyTest.php b/tests/Unit/Framework/Core/System/Ini/IniKeyTest.php new file mode 100644 index 00000000..5cffdff8 --- /dev/null +++ b/tests/Unit/Framework/Core/System/Ini/IniKeyTest.php @@ -0,0 +1,70 @@ +value)->toBe('memory_limit'); + }); + + it('has correct string value for MAX_EXECUTION_TIME', function () { + expect(IniKey::MAX_EXECUTION_TIME->value)->toBe('max_execution_time'); + }); + + it('has correct string value for UPLOAD_MAX_FILESIZE', function () { + expect(IniKey::UPLOAD_MAX_FILESIZE->value)->toBe('upload_max_filesize'); + }); + + it('has correct string value for POST_MAX_SIZE', function () { + expect(IniKey::POST_MAX_SIZE->value)->toBe('post_max_size'); + }); + + it('has correct string value for DISPLAY_ERRORS', function () { + expect(IniKey::DISPLAY_ERRORS->value)->toBe('display_errors'); + }); + + it('has correct string value for LOG_ERRORS', function () { + expect(IniKey::LOG_ERRORS->value)->toBe('log_errors'); + }); + + it('has correct string value for DATE_TIMEZONE', function () { + expect(IniKey::DATE_TIMEZONE->value)->toBe('date.timezone'); + }); + + it('has correct string value for ERROR_REPORTING', function () { + expect(IniKey::ERROR_REPORTING->value)->toBe('error_reporting'); + }); + + it('has correct string value for ERROR_LOG', function () { + expect(IniKey::ERROR_LOG->value)->toBe('error_log'); + }); + + it('has all expected commonly used keys', function () { + $expectedKeys = [ + 'memory_limit', + 'max_execution_time', + 'max_input_time', + 'upload_max_filesize', + 'post_max_size', + 'max_file_uploads', + 'display_errors', + 'display_startup_errors', + 'error_log', + 'date.timezone', + 'log_errors', + 'error_reporting', + ]; + + $actualValues = array_map( + fn (IniKey $key) => $key->value, + IniKey::cases() + ); + + foreach ($expectedKeys as $expectedKey) { + expect($actualValues)->toContain($expectedKey); + } + }); +}); + diff --git a/tests/Unit/Framework/Core/System/Ini/IniManagerTest.php b/tests/Unit/Framework/Core/System/Ini/IniManagerTest.php new file mode 100644 index 00000000..a46f1528 --- /dev/null +++ b/tests/Unit/Framework/Core/System/Ini/IniManagerTest.php @@ -0,0 +1,165 @@ +get(IniKey::MEMORY_LIMIT); + + expect($value)->not->toBeNull(); + expect($value)->toBeString(); + }); + + it('gets ini value by string key', function () { + $manager = new IniManager(); + $value = $manager->get('memory_limit'); + + expect($value)->not->toBeNull(); + expect($value)->toBeString(); + }); + + it('returns null for non-existent key', function () { + $manager = new IniManager(); + $value = $manager->get('non_existent_ini_key_12345'); + + expect($value)->toBeNull(); + }); + + it('sets ini value for modifiable key', function () { + $manager = new IniManager(); + + // Get original value + $original = $manager->get(IniKey::DISPLAY_ERRORS); + + // Set a new value + $result = $manager->set(IniKey::DISPLAY_ERRORS, '1'); + + // Restore original value + if ($original !== null) { + $manager->set(IniKey::DISPLAY_ERRORS, $original); + } + + expect($result)->toBeTrue(); + }); + + it('gets directive object with access information', function () { + $manager = new IniManager(); + $directive = $manager->getDirective(IniKey::MEMORY_LIMIT); + + expect($directive)->toBeInstanceOf(IniDirective::class); + expect($directive->name)->toBe('memory_limit'); + expect($directive->value)->not->toBeEmpty(); + expect($directive->getAccess())->toBeInstanceOf(Access::class); + }); + + it('returns null for non-existent directive', function () { + $manager = new IniManager(); + // Using a key that likely doesn't exist + $directive = $manager->getDirective(IniKey::ALLOW_URL_INCLUDE); + + // This might return null if the directive is not available + // or return an IniDirective if it exists + if ($directive === null) { + expect($directive)->toBeNull(); + } else { + expect($directive)->toBeInstanceOf(IniDirective::class); + } + }); + + it('gets all ini directives', function () { + $manager = new IniManager(); + $all = $manager->getAll(); + + expect($all)->toBeArray(); + expect($all)->not->toBeEmpty(); + + // Check that all values are IniDirective objects + foreach ($all as $name => $directive) { + expect($name)->toBeString(); + expect($directive)->toBeInstanceOf(IniDirective::class); + } + }); + + it('filters directives by access level', function () { + $manager = new IniManager(); + $userDirectives = $manager->getAllByAccess(Access::USER); + + expect($userDirectives)->toBeArray(); + + // Verify all returned directives have the correct access level + foreach ($userDirectives as $directive) { + expect($directive->getAccess())->toBe(Access::USER); + } + }); + + it('checks if directive is modifiable', function () { + $manager = new IniManager(); + + // DISPLAY_ERRORS is typically INI_USER and should be modifiable + $isModifiable = $manager->isModifiable(IniKey::DISPLAY_ERRORS); + + expect($isModifiable)->toBeBool(); + }); + + it('returns false for non-modifiable directive', function () { + $manager = new IniManager(); + + // Some directives like ENGINE are typically INI_SYSTEM and not modifiable + // MEMORY_LIMIT might be INI_SYSTEM depending on configuration + $directive = $manager->getDirective(IniKey::MEMORY_LIMIT); + + if ($directive !== null) { + $isModifiable = $manager->isModifiable(IniKey::MEMORY_LIMIT); + + // If it's SYSTEM or PERDIR, it should not be modifiable + if ($directive->getAccess() === Access::SYSTEM || $directive->getAccess() === Access::PERDIR) { + expect($isModifiable)->toBeFalse(); + } + } + }); + + it('handles enum and string keys consistently', function () { + $manager = new IniManager(); + + $valueFromEnum = $manager->get(IniKey::MEMORY_LIMIT); + $valueFromString = $manager->get('memory_limit'); + + expect($valueFromEnum)->toBe($valueFromString); + }); + + it('updates cached directive after set', function () { + $manager = new IniManager(); + + // Get original value + $original = $manager->get(IniKey::DISPLAY_ERRORS); + $originalDirective = $manager->getDirective(IniKey::DISPLAY_ERRORS); + + if ($originalDirective === null || $original === null) { + return; // Skip if directive doesn't exist + } + + // Set a new value + $newValue = '1'; + $result = $manager->set(IniKey::DISPLAY_ERRORS, $newValue); + + expect($result)->toBeTrue(); + + // Verify cached value is updated + $updatedValue = $manager->get(IniKey::DISPLAY_ERRORS); + $updatedDirective = $manager->getDirective(IniKey::DISPLAY_ERRORS); + + expect($updatedValue)->toBe($newValue); + expect($updatedDirective)->not->toBeNull(); + expect($updatedDirective->value)->toBe($newValue); + + // Restore original value + $manager->set(IniKey::DISPLAY_ERRORS, $original); + }); +}); + diff --git a/tests/Unit/Framework/Core/System/SystemConfigTest.php b/tests/Unit/Framework/Core/System/SystemConfigTest.php new file mode 100644 index 00000000..ebcf58cd --- /dev/null +++ b/tests/Unit/Framework/Core/System/SystemConfigTest.php @@ -0,0 +1,113 @@ +ini)->toBeInstanceOf(IniManager::class); + expect($config->ini)->toBe($iniManager); + }); + + it('provides access to environment via property', function () { + $iniManager = new IniManager(); + $environment = new Environment(); + $config = new SystemConfig($iniManager, $environment); + + expect($config->environment)->toBeInstanceOf(Environment::class); + expect($config->environment)->toBe($environment); + }); + + it('allows accessing ini values through ini property', function () { + $iniManager = new IniManager(); + $environment = new Environment(); + $config = new SystemConfig($iniManager, $environment); + + $value = $config->ini->get(IniKey::MEMORY_LIMIT); + + expect($value)->not->toBeNull(); + expect($value)->toBeString(); + }); + + it('allows accessing ini values by string key', function () { + $iniManager = new IniManager(); + $environment = new Environment(); + $config = new SystemConfig($iniManager, $environment); + + $value = $config->ini->get('memory_limit'); + + expect($value)->not->toBeNull(); + expect($value)->toBeString(); + }); + + it('allows accessing environment variables through environment property', function () { + $iniManager = new IniManager(); + $environment = new Environment(['TEST_KEY' => 'test_value']); + $config = new SystemConfig($iniManager, $environment); + + $value = $config->environment->get('TEST_KEY'); + + expect($value)->toBe('test_value'); + }); + + it('allows accessing all ini manager methods', function () { + $iniManager = new IniManager(); + $environment = new Environment(); + $config = new SystemConfig($iniManager, $environment); + + // Test getDirective + $directive = $config->ini->getDirective(IniKey::MEMORY_LIMIT); + expect($directive)->not->toBeNull(); + + // Test getAll + $all = $config->ini->getAll(); + expect($all)->toBeArray(); + expect($all)->not->toBeEmpty(); + + // Test isModifiable + $isModifiable = $config->ini->isModifiable(IniKey::DISPLAY_ERRORS); + expect($isModifiable)->toBeBool(); + }); + + it('allows accessing all environment methods', function () { + $iniManager = new IniManager(); + $environment = new Environment([ + 'TEST_STRING' => 'hello', + 'TEST_INT' => '42', + 'TEST_BOOL' => 'true', + ]); + $config = new SystemConfig($iniManager, $environment); + + // Test getString + expect($config->environment->getString('TEST_STRING'))->toBe('hello'); + + // Test getInt + expect($config->environment->getInt('TEST_INT'))->toBe(42); + + // Test getBool + expect($config->environment->getBool('TEST_BOOL'))->toBeTrue(); + + // Test has + expect($config->environment->has('TEST_STRING'))->toBeTrue(); + expect($config->environment->has('NON_EXISTENT'))->toBeFalse(); + }); + + it('properties are readonly', function () { + $iniManager = new IniManager(); + $environment = new Environment(); + $config = new SystemConfig($iniManager, $environment); + + // Properties should be accessible but not modifiable (readonly class) + expect($config->ini)->toBe($iniManager); + expect($config->environment)->toBe($environment); + }); +}); + diff --git a/tests/Unit/Framework/Cuid/CuidGeneratorTest.php b/tests/Unit/Framework/Cuid/CuidGeneratorTest.php index 975873d1..5c32d0ba 100644 --- a/tests/Unit/Framework/Cuid/CuidGeneratorTest.php +++ b/tests/Unit/Framework/Cuid/CuidGeneratorTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use App\Framework\Cuid\Cuid; -use App\Framework\Cuid\CuidGenerator; +use App\Framework\Id\Cuid\Cuid; +use App\Framework\Id\Cuid\CuidGenerator; use App\Framework\Random\SecureRandomGenerator; it('generates Cuid with current timestamp', function () { diff --git a/tests/Unit/Framework/Cuid/CuidTest.php b/tests/Unit/Framework/Cuid/CuidTest.php index d3e1a37d..14e369f5 100644 --- a/tests/Unit/Framework/Cuid/CuidTest.php +++ b/tests/Unit/Framework/Cuid/CuidTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use App\Framework\Cuid\Cuid; +use App\Framework\Id\Cuid\Cuid; it('creates Cuid from string', function () { $value = 'cjld2cjxh0000qzrmn831i7rn'; diff --git a/tests/Unit/Framework/Ksuid/KsuidGeneratorTest.php b/tests/Unit/Framework/Ksuid/KsuidGeneratorTest.php index 101882c1..f01d81d6 100644 --- a/tests/Unit/Framework/Ksuid/KsuidGeneratorTest.php +++ b/tests/Unit/Framework/Ksuid/KsuidGeneratorTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use App\Framework\Ksuid\Ksuid; -use App\Framework\Ksuid\KsuidGenerator; +use App\Framework\Id\Ksuid\Ksuid; +use App\Framework\Id\Ksuid\KsuidGenerator; use App\Framework\Random\SecureRandomGenerator; it('generates KSUID with current timestamp', function () { diff --git a/tests/Unit/Framework/Ksuid/KsuidTest.php b/tests/Unit/Framework/Ksuid/KsuidTest.php index 071a0b72..3bc4c32b 100644 --- a/tests/Unit/Framework/Ksuid/KsuidTest.php +++ b/tests/Unit/Framework/Ksuid/KsuidTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use App\Framework\Ksuid\Ksuid; +use App\Framework\Id\Ksuid\Ksuid; it('creates KSUID from string', function () { $value = '2SwcbqZrBNGd67ZJYmPKx42wKZj'; diff --git a/tests/Unit/Framework/Logging/CorrelationIdTest.php b/tests/Unit/Framework/Logging/CorrelationIdTest.php index b8abf27d..4669829e 100644 --- a/tests/Unit/Framework/Logging/CorrelationIdTest.php +++ b/tests/Unit/Framework/Logging/CorrelationIdTest.php @@ -159,7 +159,7 @@ final class CorrelationIdTest extends TestCase public function test_creates_from_ulid(): void { - $ulid = \App\Framework\Ulid\Ulid::generate(); + $ulid = \App\Framework\Id\Ulid\Ulid::generate(); $id = CorrelationId::fromUlid($ulid); $this->assertEquals((string) $ulid, $id->toString()); diff --git a/tests/Unit/Framework/MagicLinks/Services/InMemoryMagicLinkServiceTest.php b/tests/Unit/Framework/MagicLinks/Services/InMemoryMagicLinkServiceTest.php index af0b4f47..47f5d9b2 100644 --- a/tests/Unit/Framework/MagicLinks/Services/InMemoryMagicLinkServiceTest.php +++ b/tests/Unit/Framework/MagicLinks/Services/InMemoryMagicLinkServiceTest.php @@ -3,11 +3,11 @@ declare(strict_types=1); use App\Framework\DateTime\SystemClock; +use App\Framework\Id\Ulid\UlidGenerator; use App\Framework\MagicLinks\MagicLinkToken; use App\Framework\MagicLinks\Services\InMemoryMagicLinkService; use App\Framework\MagicLinks\TokenAction; use App\Framework\MagicLinks\TokenConfig; -use App\Framework\Ulid\UlidGenerator; beforeEach(function () { $this->clock = new SystemClock(); diff --git a/tests/Unit/Framework/NanoId/NanoIdGeneratorTest.php b/tests/Unit/Framework/NanoId/NanoIdGeneratorTest.php index e7d6d82e..259157b5 100644 --- a/tests/Unit/Framework/NanoId/NanoIdGeneratorTest.php +++ b/tests/Unit/Framework/NanoId/NanoIdGeneratorTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use App\Framework\NanoId\NanoId; -use App\Framework\NanoId\NanoIdGenerator; +use App\Framework\Id\NanoId\NanoId; +use App\Framework\Id\NanoId\NanoIdGenerator; use App\Framework\Random\SecureRandomGenerator; it('creates generator with default settings', function () { diff --git a/tests/Unit/Framework/NanoId/NanoIdTest.php b/tests/Unit/Framework/NanoId/NanoIdTest.php index dae3b347..0b31aa26 100644 --- a/tests/Unit/Framework/NanoId/NanoIdTest.php +++ b/tests/Unit/Framework/NanoId/NanoIdTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use App\Framework\NanoId\NanoId; +use App\Framework\Id\NanoId\NanoId; it('creates NanoId from string', function () { $value = 'test123ABC'; diff --git a/tests/debug/test-aggregator-internals.php b/tests/debug/test-aggregator-internals.php index 3d3fbb79..607b501b 100644 --- a/tests/debug/test-aggregator-internals.php +++ b/tests/debug/test-aggregator-internals.php @@ -4,19 +4,17 @@ declare(strict_types=1); require __DIR__ . '/../../vendor/autoload.php'; +use App\Framework\Core\ValueObjects\Timestamp; +use App\Framework\DateTime\Clock; use App\Framework\ErrorAggregation\ErrorEvent; use App\Framework\ErrorAggregation\ErrorPattern; use App\Framework\ErrorAggregation\Storage\InMemoryErrorStorage; -use App\Framework\Core\ValueObjects\Timestamp; -use App\Framework\DateTime\Clock; use App\Framework\Exception\Core\DatabaseErrorCode; -use App\Framework\Exception\Core\ErrorSeverity; -use App\Framework\Exception\FrameworkException; +use App\Framework\Exception\ErrorHandlerContext; use App\Framework\Exception\ExceptionContext; +use App\Framework\Exception\FrameworkException; use App\Framework\Exception\RequestContext; use App\Framework\Exception\SystemContext; -use App\Framework\Exception\ErrorHandlerContext; -use App\Framework\Ulid\Ulid; // Create test clock $clock = new class implements Clock { diff --git a/tests/debug/test-queue-anomaly-integration.php b/tests/debug/test-queue-anomaly-integration.php index c3c38ba2..9e95583e 100644 --- a/tests/debug/test-queue-anomaly-integration.php +++ b/tests/debug/test-queue-anomaly-integration.php @@ -15,17 +15,17 @@ declare(strict_types=1); require_once __DIR__ . '/../../vendor/autoload.php'; use App\Framework\Core\AppBootstrapper; -use App\Framework\Queue\MachineLearning\JobAnomalyDetector; -use App\Framework\Queue\MachineLearning\QueueJobFeatureExtractor; -use App\Framework\Queue\MachineLearning\QueueAnomalyMonitor; -use App\Framework\Queue\Services\JobMetricsManager; -use App\Framework\Queue\ValueObjects\JobMetrics; -use App\Framework\Queue\ValueObjects\JobMetadata; use App\Framework\Core\ValueObjects\ClassName; -use App\Framework\Core\ValueObjects\Timestamp; use App\Framework\Core\ValueObjects\Score; -use App\Framework\Ulid\Ulid; +use App\Framework\Core\ValueObjects\Timestamp; use App\Framework\DateTime\SystemClock; +use App\Framework\Id\Ulid\Ulid; +use App\Framework\Queue\MachineLearning\JobAnomalyDetector; +use App\Framework\Queue\MachineLearning\QueueAnomalyMonitor; +use App\Framework\Queue\MachineLearning\QueueJobFeatureExtractor; +use App\Framework\Queue\Services\JobMetricsManager; +use App\Framework\Queue\ValueObjects\JobMetadata; +use App\Framework\Queue\ValueObjects\JobMetrics; echo "=== Queue Anomaly Detection Integration Test ===\n\n";