refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives
This commit is contained in:
@@ -14426,7 +14426,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
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
|
identifier: method.notFound
|
||||||
count: 3
|
count: 3
|
||||||
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
||||||
@@ -14618,7 +14618,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
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
|
identifier: method.notFound
|
||||||
count: 4
|
count: 4
|
||||||
path: src/Framework/ErrorAggregation/Alerting/EmailAlertChannel.php
|
path: src/Framework/ErrorAggregation/Alerting/EmailAlertChannel.php
|
||||||
@@ -14799,7 +14799,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
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
|
identifier: method.notFound
|
||||||
count: 5
|
count: 5
|
||||||
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
||||||
@@ -14930,13 +14930,13 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
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
|
identifier: method.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
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
|
identifier: staticMethod.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
||||||
@@ -15001,19 +15001,19 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
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
|
identifier: arguments.count
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
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
|
identifier: method.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
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
|
identifier: staticMethod.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
||||||
@@ -15054,13 +15054,13 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
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
|
identifier: arguments.count
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
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
|
identifier: method.notFound
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
||||||
@@ -15149,7 +15149,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
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
|
identifier: arguments.count
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
||||||
@@ -26703,43 +26703,43 @@ parameters:
|
|||||||
path: src/Framework/Ulid/StringConverter.php
|
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
|
identifier: class.nameCase
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: class.nameCase
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: class.nameCase
|
||||||
count: 4
|
count: 4
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: return.unusedType
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: missingType.iterableValue
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: class.nameCase
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/Ulid/UlidParser.php
|
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
|
identifier: return.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/UlidParser.php
|
path: src/Framework/Ulid/UlidParser.php
|
||||||
|
|||||||
@@ -9775,7 +9775,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
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
|
identifier: method.notFound
|
||||||
count: 3
|
count: 3
|
||||||
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
||||||
@@ -9943,7 +9943,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/Alerting/AlertManager.php
|
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
|
identifier: method.notFound
|
||||||
count: 4
|
count: 4
|
||||||
path: src/Framework/ErrorAggregation/Alerting/EmailAlertChannel.php
|
path: src/Framework/ErrorAggregation/Alerting/EmailAlertChannel.php
|
||||||
@@ -10124,7 +10124,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
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
|
identifier: method.notFound
|
||||||
count: 5
|
count: 5
|
||||||
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
||||||
@@ -10237,13 +10237,13 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorAggregator.php
|
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
|
identifier: method.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
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
|
identifier: staticMethod.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
||||||
@@ -10302,19 +10302,19 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
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
|
identifier: arguments.count
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorEvent.php
|
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
|
identifier: method.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
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
|
identifier: staticMethod.notFound
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
||||||
@@ -10355,13 +10355,13 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
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
|
identifier: arguments.count
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/ErrorAggregation/ErrorPattern.php
|
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
|
identifier: method.notFound
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
||||||
@@ -10450,7 +10450,7 @@ parameters:
|
|||||||
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
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
|
identifier: arguments.count
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
path: src/Framework/ErrorAggregation/Storage/DatabaseErrorStorage.php
|
||||||
@@ -18231,43 +18231,43 @@ parameters:
|
|||||||
path: src/Framework/Ulid/StringConverter.php
|
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
|
identifier: class.nameCase
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: class.nameCase
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: class.nameCase
|
||||||
count: 4
|
count: 4
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: return.unusedType
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: missingType.iterableValue
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/Ulid.php
|
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
|
identifier: class.nameCase
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Framework/Ulid/UlidParser.php
|
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
|
identifier: return.type
|
||||||
count: 1
|
count: 1
|
||||||
path: src/Framework/Ulid/UlidParser.php
|
path: src/Framework/Ulid/UlidParser.php
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -11,15 +11,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once 'vendor/autoload.php';
|
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\DateTime\SystemClock;
|
||||||
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
|
|
||||||
class ImageMigrationScript
|
class ImageMigrationScript
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ use App\Framework\Http\Method;
|
|||||||
use App\Framework\Http\Request;
|
use App\Framework\Http\Request;
|
||||||
use App\Framework\Http\Session\FormIdGenerator;
|
use App\Framework\Http\Session\FormIdGenerator;
|
||||||
use App\Framework\Http\UploadedFile;
|
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\Meta\MetaData;
|
||||||
use App\Framework\Router\Result\ViewResult;
|
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\FormBuilder;
|
||||||
use App\Framework\View\RawHtml;
|
use App\Framework\View\RawHtml;
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ use App\Framework\Http\Exception\NotFound;
|
|||||||
use App\Framework\Http\HttpRequest;
|
use App\Framework\Http\HttpRequest;
|
||||||
use App\Framework\Http\Method;
|
use App\Framework\Http\Method;
|
||||||
use App\Framework\Http\Responses\JsonResponse;
|
use App\Framework\Http\Responses\JsonResponse;
|
||||||
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
use App\Framework\Router\Result\FileResult;
|
use App\Framework\Router\Result\FileResult;
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
|
||||||
|
|
||||||
final readonly class ImageApiController
|
final readonly class ImageApiController
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use App\Framework\Http\Request;
|
|||||||
use App\Framework\Http\Responses\JsonResponse;
|
use App\Framework\Http\Responses\JsonResponse;
|
||||||
use App\Framework\Http\Status;
|
use App\Framework\Http\Status;
|
||||||
use App\Framework\Http\UploadedFile;
|
use App\Framework\Http\UploadedFile;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
final readonly class UploadImageController
|
final readonly class UploadImageController
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use App\Framework\Database\Attributes\Entity;
|
|||||||
use App\Framework\Database\Attributes\Type;
|
use App\Framework\Database\Attributes\Type;
|
||||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||||
use App\Framework\Http\MimeType;
|
use App\Framework\Http\MimeType;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
#[Entity(tableName: 'images', idColumn: 'ulid')]
|
#[Entity(tableName: 'images', idColumn: 'ulid')]
|
||||||
final readonly class Image
|
final readonly class Image
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Domain\Media;
|
namespace App\Domain\Media;
|
||||||
|
|
||||||
use App\Framework\Ulid\StringConverter;
|
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
final readonly class ImageResizer
|
final readonly class ImageResizer
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Domain\SmartLink\ValueObjects;
|
namespace App\Domain\SmartLink\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
final readonly class ClickId
|
final readonly class ClickId
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Domain\SmartLink\ValueObjects;
|
namespace App\Domain\SmartLink\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
final readonly class GeoRuleId
|
final readonly class GeoRuleId
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Domain\SmartLink\ValueObjects;
|
namespace App\Domain\SmartLink\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
final readonly class SmartLinkId
|
final readonly class SmartLinkId
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ final class PerformanceBasedAnalyticsStorage implements AnalyticsStorage
|
|||||||
return;
|
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();
|
$filename = $this->dataPath . '/raw_' . date('Y-m-d_H-i-s') . '_' . $generator->generate() . '.' . $this->serializer->getFileExtension();
|
||||||
$content = $this->serializer->serialize($this->rawDataBuffer);
|
$content = $this->serializer->serialize($this->rawDataBuffer);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Audit\ValueObjects;
|
namespace App\Framework\Audit\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audit Entry ID value object (ULID-based)
|
* Audit Entry ID value object (ULID-based)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum EnvKey: string
|
|||||||
// Feature Flags
|
// Feature Flags
|
||||||
case ENABLE_CONTEXT_AWARE_INITIALIZERS = 'ENABLE_CONTEXT_AWARE_INITIALIZERS';
|
case ENABLE_CONTEXT_AWARE_INITIALIZERS = 'ENABLE_CONTEXT_AWARE_INITIALIZERS';
|
||||||
case MCP_SERVER_MODE = 'MCP_SERVER_MODE';
|
case MCP_SERVER_MODE = 'MCP_SERVER_MODE';
|
||||||
|
case LOG_COLOR_OUTPUT = 'LOG_COLOR_OUTPUT';
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
case DB_DRIVER = 'DB_DRIVER';
|
case DB_DRIVER = 'DB_DRIVER';
|
||||||
|
|||||||
149
src/Framework/Console/CliSapi.php
Normal file
149
src/Framework/Console/CliSapi.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Console;
|
||||||
|
|
||||||
|
use App\Framework\Console\ValueObjects\TerminalStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object für CLI SAPI mit allen Standard-Streams.
|
||||||
|
*
|
||||||
|
* Zentrale Abstraktion für CLI I/O Operationen.
|
||||||
|
* Bietet alle drei Streams (stdin, stdout, stderr) als Properties
|
||||||
|
* und delegiert Terminal-Detection an TerminalDetector.
|
||||||
|
*/
|
||||||
|
final readonly class CliSapi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param TerminalStream $stdin Standard-Input Stream
|
||||||
|
* @param TerminalStream $stdout Standard-Output Stream
|
||||||
|
* @param TerminalStream $stderr Standard-Error Stream
|
||||||
|
* @param bool $isCli Ob aktueller Kontext CLI ist
|
||||||
|
*/
|
||||||
|
private function __construct(
|
||||||
|
public TerminalStream $stdin,
|
||||||
|
public TerminalStream $stdout,
|
||||||
|
public TerminalStream $stderr,
|
||||||
|
private bool $isCli
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt CliSapi automatisch basierend auf SAPI-Detection.
|
||||||
|
*
|
||||||
|
* Erkennt automatisch ob CLI oder Web-Kontext.
|
||||||
|
*/
|
||||||
|
public static function detect(): self
|
||||||
|
{
|
||||||
|
$isCli = PHP_SAPI === 'cli';
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
stdin: TerminalStream::stdin(),
|
||||||
|
stdout: TerminalStream::stdout(),
|
||||||
|
stderr: TerminalStream::stderr(),
|
||||||
|
isCli: $isCli
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt CliSapi explizit für CLI-Kontext.
|
||||||
|
*/
|
||||||
|
public static function forCli(): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
stdin: TerminalStream::stdin(),
|
||||||
|
stdout: TerminalStream::stdout(),
|
||||||
|
stderr: TerminalStream::stderr(),
|
||||||
|
isCli: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt CliSapi explizit für Web-Kontext.
|
||||||
|
*/
|
||||||
|
public static function forWeb(): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
stdin: TerminalStream::stdin(),
|
||||||
|
stdout: TerminalStream::stdout(),
|
||||||
|
stderr: TerminalStream::stderr(),
|
||||||
|
isCli: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob aktueller Kontext CLI ist.
|
||||||
|
*/
|
||||||
|
public function isCli(): bool
|
||||||
|
{
|
||||||
|
return $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
548
src/Framework/Console/Components/ConsoleDialog.php
Normal file
548
src/Framework/Console/Components/ConsoleDialog.php
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Console\Components;
|
||||||
|
|
||||||
|
use App\Framework\Console\CommandGroupRegistry;
|
||||||
|
use App\Framework\Console\CommandHelp;
|
||||||
|
use App\Framework\Console\CommandHelpGenerator;
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\CommandList;
|
||||||
|
use App\Framework\Console\ConsoleColor;
|
||||||
|
use App\Framework\Console\ConsoleOutputInterface;
|
||||||
|
use App\Framework\Console\ErrorRecovery\CommandSuggestionEngine;
|
||||||
|
use App\Framework\Console\ExitCode;
|
||||||
|
use App\Framework\Console\ParameterInspector;
|
||||||
|
use App\Framework\DI\Container;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog mode orchestrator - simple text-based interactive console
|
||||||
|
* Similar to AI assistant chat interface
|
||||||
|
*/
|
||||||
|
final readonly class ConsoleDialog
|
||||||
|
{
|
||||||
|
private bool $readlineAvailable = false;
|
||||||
|
|
||||||
|
private CommandSuggestionEngine $suggestionEngine;
|
||||||
|
|
||||||
|
private CommandHelpGenerator $helpGenerator;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ConsoleOutputInterface $output,
|
||||||
|
private DiscoveryRegistry $discoveryRegistry,
|
||||||
|
private CommandHistory $commandHistory,
|
||||||
|
private CommandGroupRegistry $groupRegistry,
|
||||||
|
private DialogCommandExecutor $commandExecutor,
|
||||||
|
private CommandList $commandList,
|
||||||
|
private Container $container,
|
||||||
|
private string $prompt = 'console> '
|
||||||
|
) {
|
||||||
|
$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<int, string>
|
||||||
|
*/
|
||||||
|
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<int, string>
|
||||||
|
*/
|
||||||
|
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 <command> 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<int, string>}
|
||||||
|
*/
|
||||||
|
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 <command>" 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
69
src/Framework/Console/Components/DialogCommandExecutor.php
Normal file
69
src/Framework/Console/Components/DialogCommandExecutor.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Console\Components;
|
||||||
|
|
||||||
|
use App\Framework\Console\CommandHistory;
|
||||||
|
use App\Framework\Console\CommandRegistry;
|
||||||
|
use App\Framework\Console\ConsoleColor;
|
||||||
|
use App\Framework\Console\ConsoleOutputInterface;
|
||||||
|
use App\Framework\Console\ExitCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles command execution for dialog mode
|
||||||
|
*/
|
||||||
|
final readonly class DialogCommandExecutor
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ConsoleOutputInterface $output,
|
||||||
|
private CommandRegistry $commandRegistry,
|
||||||
|
private CommandHistory $commandHistory,
|
||||||
|
private string $scriptName = 'console'
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command by name with arguments
|
||||||
|
*
|
||||||
|
* @param array<int, string> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ final class Table
|
|||||||
private ?ConsoleStyle $headerStyle = null,
|
private ?ConsoleStyle $headerStyle = null,
|
||||||
private ?ConsoleStyle $rowStyle = null,
|
private ?ConsoleStyle $rowStyle = null,
|
||||||
private ?ConsoleStyle $borderStyle = 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->headerStyle ??= ConsoleStyle::create(color: ConsoleColor::BRIGHT_WHITE, format: ConsoleFormat::BOLD);
|
||||||
$this->rowStyle ??= ConsoleStyle::create();
|
$this->rowStyle ??= ConsoleStyle::create();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class TreeHelper
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $title = '',
|
private string $title = '',
|
||||||
private ConsoleOutput $output = new ConsoleOutput(),
|
#private readonly ConsoleOutput $output = new ConsoleOutput(),
|
||||||
) {
|
) {
|
||||||
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
|
$this->nodeStyle = ConsoleStyle::create(color: ConsoleColor::BRIGHT_YELLOW, format: ConsoleFormat::BOLD);
|
||||||
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
|
$this->leafStyle = ConsoleStyle::create(color: ConsoleColor::WHITE);
|
||||||
@@ -115,13 +115,13 @@ final class TreeHelper
|
|||||||
/**
|
/**
|
||||||
* Zeigt die vollständige Baumstruktur an.
|
* Zeigt die vollständige Baumstruktur an.
|
||||||
*/
|
*/
|
||||||
public function display(): void
|
public function display(ConsoleOutput $output): void
|
||||||
{
|
{
|
||||||
if (! empty($this->title)) {
|
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.
|
* Zeigt die Baumstruktur mit dem aktuellen Präfix an.
|
||||||
* (Interne Methode für rekursives Rendern)
|
* (Interne Methode für rekursives Rendern)
|
||||||
*/
|
*/
|
||||||
private function displayTree(): void
|
private function displayTree(ConsoleOutput $output): void
|
||||||
{
|
{
|
||||||
$count = count($this->nodes);
|
$count = count($this->nodes);
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ final class TreeHelper
|
|||||||
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
|
$style = $item['isLeaf'] ? $this->leafStyle : $this->nodeStyle;
|
||||||
$title = $linePrefix . $item['title'];
|
$title = $linePrefix . $item['title'];
|
||||||
|
|
||||||
$this->output->writeLine(
|
$output->writeLine(
|
||||||
$this->lineStyle->apply($linePrefix) .
|
$this->lineStyle->apply($linePrefix) .
|
||||||
$style->apply($item['title'])
|
$style->apply($item['title'])
|
||||||
);
|
);
|
||||||
@@ -181,7 +181,7 @@ final class TreeHelper
|
|||||||
if (! $item['isLeaf'] && $item['node'] !== null) {
|
if (! $item['isLeaf'] && $item['node'] !== null) {
|
||||||
$item['node']
|
$item['node']
|
||||||
->setPrefix($nodePrefix, $isLast)
|
->setPrefix($nodePrefix, $isLast)
|
||||||
->displayTree();
|
->displayTree($output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Console;
|
namespace App\Framework\Console;
|
||||||
|
|
||||||
use App\Framework\Config\AppConfig;
|
use App\Framework\Config\AppConfig;
|
||||||
|
use App\Framework\Console\Components\ConsoleDialog;
|
||||||
use App\Framework\Console\Components\ConsoleTUI;
|
use App\Framework\Console\Components\ConsoleTUI;
|
||||||
|
use App\Framework\Console\Components\DialogCommandExecutor;
|
||||||
use App\Framework\Console\Components\TuiCommandExecutor;
|
use App\Framework\Console\Components\TuiCommandExecutor;
|
||||||
use App\Framework\Console\Components\TuiInputHandler;
|
use App\Framework\Console\Components\TuiInputHandler;
|
||||||
use App\Framework\Console\Components\TuiRenderer;
|
use App\Framework\Console\Components\TuiRenderer;
|
||||||
@@ -179,6 +181,11 @@ final class ConsoleApplication
|
|||||||
return $this->launchInteractiveTUI();
|
return $this->launchInteractiveTUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle dialog mode launch flags
|
||||||
|
if (in_array($commandName, ['--dialog', '--chat'])) {
|
||||||
|
return $this->launchDialogMode();
|
||||||
|
}
|
||||||
|
|
||||||
// Handle built-in commands
|
// Handle built-in commands
|
||||||
if (in_array($commandName, ['help', '--help', '-h'])) {
|
if (in_array($commandName, ['help', '--help', '-h'])) {
|
||||||
// Spezifische Command-Hilfe
|
// Spezifische Command-Hilfe
|
||||||
@@ -376,6 +383,8 @@ final class ConsoleApplication
|
|||||||
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
|
$this->output->writeLine("Verwendung:", ConsoleColor::BRIGHT_YELLOW);
|
||||||
$this->output->writeLine(" php {$this->scriptName} # Interaktive TUI starten");
|
$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} --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} <kategorie> # Commands einer Kategorie anzeigen");
|
$this->output->writeLine(" php {$this->scriptName} <kategorie> # Commands einer Kategorie anzeigen");
|
||||||
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente] # Kommando direkt ausführen");
|
$this->output->writeLine(" php {$this->scriptName} <kommando> [argumente] # Kommando direkt ausführen");
|
||||||
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Hilfe für spezifisches Kommando");
|
$this->output->writeLine(" php {$this->scriptName} help <kommando> # Hilfe für spezifisches Kommando");
|
||||||
@@ -384,6 +393,7 @@ final class ConsoleApplication
|
|||||||
$this->output->writeLine("Hinweis:", ConsoleColor::CYAN);
|
$this->output->writeLine("Hinweis:", ConsoleColor::CYAN);
|
||||||
$this->output->writeLine(" Ohne Argumente wird automatisch die interaktive TUI gestartet.");
|
$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(" 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
|
* Prüft ob das Terminal für TUI kompatibel ist
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ final readonly class TableResult implements ConsoleResult
|
|||||||
* @param ExitCode $exitCode Exit code (default: SUCCESS)
|
* @param ExitCode $exitCode Exit code (default: SUCCESS)
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly array $headers,
|
public array $headers,
|
||||||
public readonly array $rows,
|
public array $rows,
|
||||||
public readonly ?string $title = null,
|
public ?string $title = null,
|
||||||
public readonly ExitCode $exitCode = ExitCode::SUCCESS,
|
public ExitCode $exitCode = ExitCode::SUCCESS,
|
||||||
) {
|
) {
|
||||||
$this->data = [
|
$this->data = [
|
||||||
'headers' => $this->headers,
|
'headers' => $this->headers,
|
||||||
@@ -78,7 +78,7 @@ final readonly class TableResult implements ConsoleResult
|
|||||||
*/
|
*/
|
||||||
public function render(ConsoleOutputInterface $output): void
|
public function render(ConsoleOutputInterface $output): void
|
||||||
{
|
{
|
||||||
$table = new Table($output);
|
$table = new Table();
|
||||||
|
|
||||||
if ($this->title !== null) {
|
if ($this->title !== null) {
|
||||||
$table->setTitle($this->title);
|
$table->setTitle($this->title);
|
||||||
|
|||||||
90
src/Framework/Console/TerminalDetector.php
Normal file
90
src/Framework/Console/TerminalDetector.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Console;
|
||||||
|
|
||||||
|
use App\Framework\Console\ValueObjects\TerminalStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility-Klasse für Terminal-Detection und Farb-Support-Prüfung.
|
||||||
|
*
|
||||||
|
* Kann von Console-Modul und Logging-Modul verwendet werden.
|
||||||
|
*
|
||||||
|
* Verwendet intern CliSapi für Default-Streams.
|
||||||
|
*/
|
||||||
|
final readonly class TerminalDetector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prüft ob ein Stream ein Terminal ist.
|
||||||
|
*
|
||||||
|
* Verwendet intern CliSapi für Default-Streams.
|
||||||
|
*
|
||||||
|
* @param TerminalStream|null $stream Terminal Stream (oder null für STDOUT)
|
||||||
|
* @return bool True wenn Stream ein Terminal ist
|
||||||
|
*/
|
||||||
|
public static function isTerminal(?TerminalStream $stream = null): bool
|
||||||
|
{
|
||||||
|
if (!function_exists('posix_isatty')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream ??= CliSapi::detect()->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
143
src/Framework/Console/ValueObjects/TerminalStream.php
Normal file
143
src/Framework/Console/ValueObjects/TerminalStream.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Console\ValueObjects;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object für Terminal Streams (STDIN/STDOUT/STDERR).
|
||||||
|
*
|
||||||
|
* Wrappt Streams type-safe und ermöglicht Terminal-Detection
|
||||||
|
* ohne direkte resource Type-Hints.
|
||||||
|
*
|
||||||
|
* Unterstützt sowohl CLI-Kontext (mit STDOUT/STDERR/STDIN Konstanten)
|
||||||
|
* als auch Web-Kontext (mit php:// Streams).
|
||||||
|
*/
|
||||||
|
final readonly class TerminalStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $stream Stream-Resource (STDIN, STDOUT, STDERR, etc.)
|
||||||
|
*/
|
||||||
|
private function __construct(
|
||||||
|
private mixed $stream
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein TerminalStream für STDIN.
|
||||||
|
*
|
||||||
|
* Kompatibel mit CLI und Web-Kontext:
|
||||||
|
* - CLI: Verwendet STDIN-Konstante falls verfügbar
|
||||||
|
* - Web: Öffnet php://stdin Stream
|
||||||
|
*/
|
||||||
|
public static function stdin(): self
|
||||||
|
{
|
||||||
|
if (defined('STDIN') && STDIN !== null) {
|
||||||
|
return new self(STDIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web-Kontext: php://stdin öffnen
|
||||||
|
$stream = fopen('php://stdin', 'r');
|
||||||
|
if ($stream === false) {
|
||||||
|
throw new InvalidArgumentException('Failed to open php://stdin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein TerminalStream für STDOUT.
|
||||||
|
*
|
||||||
|
* Kompatibel mit CLI und Web-Kontext:
|
||||||
|
* - CLI: Verwendet STDOUT-Konstante falls verfügbar
|
||||||
|
* - Web: Öffnet php://stdout Stream
|
||||||
|
*/
|
||||||
|
public static function stdout(): self
|
||||||
|
{
|
||||||
|
if (defined('STDOUT') && STDOUT !== null) {
|
||||||
|
return new self(STDOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web-Kontext: php://stdout öffnen
|
||||||
|
$stream = fopen('php://stdout', 'w');
|
||||||
|
if ($stream === false) {
|
||||||
|
throw new InvalidArgumentException('Failed to open php://stdout');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein TerminalStream für STDERR.
|
||||||
|
*
|
||||||
|
* Kompatibel mit CLI und Web-Kontext:
|
||||||
|
* - CLI: Verwendet STDERR-Konstante falls verfügbar
|
||||||
|
* - Web: Öffnet php://stderr Stream
|
||||||
|
*/
|
||||||
|
public static function stderr(): self
|
||||||
|
{
|
||||||
|
if (defined('STDERR') && STDERR !== null) {
|
||||||
|
return new self(STDERR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web-Kontext: php://stderr öffnen
|
||||||
|
$stream = fopen('php://stderr', 'w');
|
||||||
|
if ($stream === false) {
|
||||||
|
throw new InvalidArgumentException('Failed to open php://stderr');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein TerminalStream aus einem beliebigen Stream.
|
||||||
|
*
|
||||||
|
* @param mixed $stream Stream-Resource
|
||||||
|
* @throws InvalidArgumentException Wenn Stream nicht gültig ist
|
||||||
|
*/
|
||||||
|
public static function fromStream(mixed $stream): self
|
||||||
|
{
|
||||||
|
if ($stream === null) {
|
||||||
|
throw new InvalidArgumentException('Stream cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den wrapped Stream zurück.
|
||||||
|
*
|
||||||
|
* @return mixed Der Stream
|
||||||
|
*/
|
||||||
|
public function getStream(): mixed
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Core\System\Ini;
|
namespace App\Framework\Core\System\Ini;
|
||||||
|
|
||||||
final class IniDirective
|
final readonly class IniDirective implements Stringable
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $name,
|
public string $name,
|
||||||
@@ -12,9 +12,18 @@ final class IniDirective
|
|||||||
private int $accessMask,
|
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;
|
return $this->accessMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = "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 = "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 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";
|
||||||
}
|
}
|
||||||
|
|||||||
158
src/Framework/Core/System/Ini/IniManager.php
Normal file
158
src/Framework/Core/System/Ini/IniManager.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Core\System\Ini;
|
||||||
|
|
||||||
|
final class IniManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, IniDirective> 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<string, IniDirective> 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<string, IniDirective> 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<string, IniDirective> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Framework\Core\System;
|
|
||||||
|
|
||||||
use Stringable;
|
|
||||||
|
|
||||||
final readonly class PhpIni implements Stringable
|
|
||||||
{
|
|
||||||
public string $path;
|
|
||||||
public function __construct(
|
|
||||||
) {
|
|
||||||
$path = php_ini_loaded_file();
|
|
||||||
if($path === false) {
|
|
||||||
$path = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->path = $path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isLoaded(): bool
|
|
||||||
{
|
|
||||||
return $this->path !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
return $this->path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
src/Framework/Core/System/SystemConfig.php
Normal file
26
src/Framework/Core/System/SystemConfig.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Core\System;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
use App\Framework\Core\System\Ini\IniManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemConfig provides unified access to system configuration sources.
|
||||||
|
*
|
||||||
|
* This wrapper class exposes IniManager and Environment as public properties,
|
||||||
|
* allowing direct access to all methods of both services.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* $config->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
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -18,12 +18,12 @@ use App\Framework\DateTime\Clock;
|
|||||||
use App\Framework\Exception\Core\DatabaseErrorCode;
|
use App\Framework\Exception\Core\DatabaseErrorCode;
|
||||||
use App\Framework\Exception\ExceptionContext;
|
use App\Framework\Exception\ExceptionContext;
|
||||||
use App\Framework\Exception\FrameworkException;
|
use App\Framework\Exception\FrameworkException;
|
||||||
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
use App\Framework\Logging\Logger;
|
use App\Framework\Logging\Logger;
|
||||||
use App\Framework\Performance\MemoryMonitor;
|
use App\Framework\Performance\MemoryMonitor;
|
||||||
use App\Framework\Performance\OperationTracker;
|
use App\Framework\Performance\OperationTracker;
|
||||||
use App\Framework\Performance\PerformanceReporter;
|
use App\Framework\Performance\PerformanceReporter;
|
||||||
use App\Framework\Performance\Repository\PerformanceMetricsRepository;
|
use App\Framework\Performance\Repository\PerformanceMetricsRepository;
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
|
||||||
|
|
||||||
final readonly class MigrationRunner
|
final readonly class MigrationRunner
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ final readonly class EntityPersister
|
|||||||
if ($caster !== null) {
|
if ($caster !== null) {
|
||||||
$result = $caster->toDatabase($value);
|
$result = $caster->toDatabase($value);
|
||||||
// Debug logging for ULID issues
|
// 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) . ")");
|
error_log("ULID converted: " . var_export($result, true) . " (length: " . strlen($result) . ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Database\TypeCaster;
|
namespace App\Framework\Database\TypeCaster;
|
||||||
|
|
||||||
use App\Framework\DateTime\SystemClock;
|
use App\Framework\DateTime\SystemClock;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
final class UlidCaster implements TypeCasterInterface
|
final class UlidCaster implements TypeCasterInterface
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace App\Framework\ErrorAggregation;
|
|||||||
use App\Framework\Exception\Core\ErrorSeverity;
|
use App\Framework\Exception\Core\ErrorSeverity;
|
||||||
use App\Framework\Exception\ErrorCode;
|
use App\Framework\Exception\ErrorCode;
|
||||||
use App\Framework\Exception\ErrorHandlerContext;
|
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
|
* Represents a single error event for aggregation and analysis
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\ErrorAggregation;
|
namespace App\Framework\ErrorAggregation;
|
||||||
|
|
||||||
use App\Framework\Exception\Core\ErrorSeverity;
|
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
|
* Represents a pattern of similar errors for analysis and alerting
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use App\Framework\ErrorAggregation\ErrorEvent;
|
|||||||
use App\Framework\ErrorAggregation\ErrorPattern;
|
use App\Framework\ErrorAggregation\ErrorPattern;
|
||||||
use App\Framework\Exception\Core\ErrorSeverity;
|
use App\Framework\Exception\Core\ErrorSeverity;
|
||||||
use App\Framework\Exception\ErrorCode;
|
use App\Framework\Exception\ErrorCode;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database-based error storage implementation
|
* Database-based error storage implementation
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ final readonly class ErrorBoundaryMiddleware implements HttpMiddleware
|
|||||||
*/
|
*/
|
||||||
private function createJsonFallbackResponse($request): JsonResponse
|
private function createJsonFallbackResponse($request): JsonResponse
|
||||||
{
|
{
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
$errorData = [
|
$errorData = [
|
||||||
'error' => [
|
'error' => [
|
||||||
'code' => 'SERVICE_TEMPORARILY_UNAVAILABLE',
|
'code' => 'SERVICE_TEMPORARILY_UNAVAILABLE',
|
||||||
@@ -75,7 +75,7 @@ final readonly class ErrorBoundaryMiddleware implements HttpMiddleware
|
|||||||
*/
|
*/
|
||||||
private function createHtmlFallbackResponse($request, MiddlewareContext $context)
|
private function createHtmlFallbackResponse($request, MiddlewareContext $context)
|
||||||
{
|
{
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
$fallbackHtml = $this->getFallbackHtmlContent($request);
|
$fallbackHtml = $this->getFallbackHtmlContent($request);
|
||||||
|
|
||||||
return new ViewResult($fallbackHtml, [
|
return new ViewResult($fallbackHtml, [
|
||||||
|
|||||||
@@ -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
|
* Get path as FilePath object
|
||||||
*/
|
*/
|
||||||
@@ -65,9 +78,10 @@ final readonly class Directory
|
|||||||
$paths = $this->storage->listDirectory($this->getPathString());
|
$paths = $this->storage->listDirectory($this->getPathString());
|
||||||
$files = [];
|
$files = [];
|
||||||
|
|
||||||
|
$factory = $this->getFactory();
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
if (is_file($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());
|
$paths = $this->storage->listDirectory($this->getPathString());
|
||||||
$directories = [];
|
$directories = [];
|
||||||
|
|
||||||
|
$factory = $this->getFactory();
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
if (is_dir($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 = [];
|
$files = [];
|
||||||
$directories = [];
|
$directories = [];
|
||||||
|
|
||||||
|
$factory = $this->getFactory();
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
if (is_file($path)) {
|
if (is_file($path)) {
|
||||||
$files[] = FilesystemFactory::createFile($path, $this->storage);
|
$files[] = $factory->createFile($path, $this->storage);
|
||||||
} elseif (is_dir($path)) {
|
} 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);
|
$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);
|
$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
|
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();
|
$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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$factory = $this->getFactory();
|
||||||
if ($result['is_file']) {
|
if ($result['is_file']) {
|
||||||
$files[] = FilesystemFactory::createFile($result['path'], $this->storage);
|
$files[] = $factory->createFile($result['path'], $this->storage);
|
||||||
} elseif ($result['is_dir']) {
|
} elseif ($result['is_dir']) {
|
||||||
$directories[] = FilesystemFactory::createDirectory($result['path'], $this->storage);
|
$directories[] = $factory->createDirectory($result['path'], $this->storage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Filesystem;
|
namespace App\Framework\Filesystem;
|
||||||
|
|
||||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||||
use App\Framework\Logging\DefaultLogger;
|
use App\Framework\Logging\Logger;
|
||||||
use App\Framework\Logging\LoggerFactory;
|
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +13,22 @@ use ReflectionClass;
|
|||||||
*/
|
*/
|
||||||
final readonly class FilesystemFactory
|
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.
|
* 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 Storage $storage Storage-Implementierung
|
||||||
* @param int|null $cacheTimeoutSeconds Optional, Zeit in Sekunden, nach der der Cache ungültig wird
|
* @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 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,
|
FilePath|string $path,
|
||||||
Storage $storage,
|
Storage $storage,
|
||||||
?int $cacheTimeoutSeconds = null,
|
?int $cacheTimeoutSeconds = null,
|
||||||
bool $lazyLoad = true,
|
bool $lazyLoad = true
|
||||||
?DefaultLogger $logger = null
|
|
||||||
): File {
|
): File {
|
||||||
$logger ??= LoggerFactory::getDefaultLogger();
|
|
||||||
$pathString = $path instanceof FilePath ? $path->toString() : $path;
|
$pathString = $path instanceof FilePath ? $path->toString() : $path;
|
||||||
|
|
||||||
// Direkte Instanziierung ohne Lazy-Loading
|
// Direkte Instanziierung ohne Lazy-Loading
|
||||||
if (! $lazyLoad) {
|
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
|
// Nur laden wenn die Datei existiert
|
||||||
if (! $storage->exists($pathString)) {
|
if (! $storage->exists($pathString)) {
|
||||||
@@ -53,21 +65,22 @@ final readonly class FilesystemFactory
|
|||||||
|
|
||||||
$reflection = new ReflectionClass(File::class);
|
$reflection = new ReflectionClass(File::class);
|
||||||
$loadTime = time();
|
$loadTime = time();
|
||||||
|
$logger = $this->logger;
|
||||||
|
|
||||||
// LazyProxy verwenden für individuelle Property-Callbacks
|
// LazyProxy verwenden für individuelle Property-Callbacks
|
||||||
return $reflection->newLazyProxy([
|
return $reflection->newLazyProxy([
|
||||||
// Dateiinhalt wird erst beim ersten Zugriff geladen
|
// Dateiinhalt wird erst beim ersten Zugriff geladen
|
||||||
'contents' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
|
'contents' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
|
||||||
$pathStr = $file->getPathString();
|
$pathStr = $file->getPathString();
|
||||||
$logger->debug("Lazy-Loading contents für {$pathStr}");
|
$logger?->debug("Lazy-Loading contents für {$pathStr}");
|
||||||
|
|
||||||
// Cache-Invalidierung basierend auf Zeit
|
// Cache-Invalidierung basierend auf Zeit
|
||||||
if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) {
|
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()) {
|
if (! $file->exists()) {
|
||||||
$logger->debug("Datei existiert nicht: {$pathStr}");
|
$logger?->debug("Datei existiert nicht: {$pathStr}");
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -78,15 +91,15 @@ final readonly class FilesystemFactory
|
|||||||
// Dateigröße wird erst beim ersten Zugriff ermittelt
|
// Dateigröße wird erst beim ersten Zugriff ermittelt
|
||||||
'size' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
|
'size' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
|
||||||
$pathStr = $file->getPathString();
|
$pathStr = $file->getPathString();
|
||||||
$logger->debug("Lazy-Loading size für {$pathStr}");
|
$logger?->debug("Lazy-Loading size für {$pathStr}");
|
||||||
|
|
||||||
// Cache-Invalidierung basierend auf Zeit
|
// Cache-Invalidierung basierend auf Zeit
|
||||||
if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) {
|
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()) {
|
if (! $file->exists()) {
|
||||||
$logger->debug("Datei existiert nicht: {$pathStr}");
|
$logger?->debug("Datei existiert nicht: {$pathStr}");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -97,15 +110,15 @@ final readonly class FilesystemFactory
|
|||||||
// Zeitstempel wird erst beim ersten Zugriff ermittelt
|
// Zeitstempel wird erst beim ersten Zugriff ermittelt
|
||||||
'lastModified' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
|
'lastModified' => function (File $file) use ($loadTime, $cacheTimeoutSeconds, $logger) {
|
||||||
$pathStr = $file->getPathString();
|
$pathStr = $file->getPathString();
|
||||||
$logger->debug("Lazy-Loading lastModified für {$pathStr}");
|
$logger?->debug("Lazy-Loading lastModified für {$pathStr}");
|
||||||
|
|
||||||
// Cache-Invalidierung basierend auf Zeit
|
// Cache-Invalidierung basierend auf Zeit
|
||||||
if ($cacheTimeoutSeconds !== null && time() - $loadTime > $cacheTimeoutSeconds) {
|
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()) {
|
if (! $file->exists()) {
|
||||||
$logger->debug("Datei existiert nicht: {$pathStr}");
|
$logger?->debug("Datei existiert nicht: {$pathStr}");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -121,20 +134,17 @@ final readonly class FilesystemFactory
|
|||||||
* @param FilePath|string $path Pfad zum Verzeichnis
|
* @param FilePath|string $path Pfad zum Verzeichnis
|
||||||
* @param Storage $storage Storage-Implementierung
|
* @param Storage $storage Storage-Implementierung
|
||||||
* @param bool $lazyLoad Optional, ob Lazy-Loading verwendet werden soll
|
* @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,
|
FilePath|string $path,
|
||||||
Storage $storage,
|
Storage $storage,
|
||||||
bool $lazyLoad = true,
|
bool $lazyLoad = true
|
||||||
?DefaultLogger $logger = null
|
|
||||||
): Directory {
|
): Directory {
|
||||||
$logger ??= LoggerFactory::getDefaultLogger();
|
|
||||||
$pathString = $path instanceof FilePath ? $path->toString() : $path;
|
$pathString = $path instanceof FilePath ? $path->toString() : $path;
|
||||||
|
|
||||||
// Direkte Instanziierung ohne Lazy-Loading
|
// Direkte Instanziierung ohne Lazy-Loading
|
||||||
if (! $lazyLoad) {
|
if (! $lazyLoad) {
|
||||||
$logger->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$pathString}");
|
$this->logger?->debug("Erstelle Directory-Objekt ohne Lazy-Loading: {$pathString}");
|
||||||
|
|
||||||
$contents = [];
|
$contents = [];
|
||||||
if (is_dir($pathString)) {
|
if (is_dir($pathString)) {
|
||||||
@@ -145,13 +155,14 @@ final readonly class FilesystemFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
$reflection = new ReflectionClass(Directory::class);
|
$reflection = new ReflectionClass(Directory::class);
|
||||||
|
$logger = $this->logger;
|
||||||
|
|
||||||
// LazyGhost verwenden - alle Eigenschaften werden beim ersten Zugriff initialisiert
|
// LazyGhost verwenden - alle Eigenschaften werden beim ersten Zugriff initialisiert
|
||||||
$lazyDir = $reflection->newLazyGhost(
|
$lazyDir = $reflection->newLazyGhost(
|
||||||
// Initializer-Callback
|
// Initializer-Callback
|
||||||
function (Directory $directory) use ($logger): void {
|
function (Directory $directory) use ($logger): void {
|
||||||
$pathStr = $directory->getPathString();
|
$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
|
// Verzeichnisinhalt wird erst beim ersten Zugriff auf eine Eigenschaft geladen
|
||||||
if ($directory->exists()) {
|
if ($directory->exists()) {
|
||||||
|
|||||||
@@ -210,7 +210,8 @@ final class InMemoryStorage implements Storage, StreamableStorage
|
|||||||
*/
|
*/
|
||||||
public function file(string $path): File
|
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
|
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
|
public function getMimeType(string $path): string
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Filesystem;
|
namespace App\Framework\Filesystem;
|
||||||
|
|
||||||
use App\Framework\Logging\DefaultLogger;
|
use App\Framework\Logging\Logger;
|
||||||
use App\Framework\Logging\LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorator für Storage-Implementierungen mit Logging-Unterstützung.
|
* 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 \App\Framework\Async\FiberManager $fiberManager { get => $this->storage->fiberManager; }
|
||||||
|
|
||||||
|
public ?Logger $logger;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Storage $storage,
|
private readonly Storage $storage,
|
||||||
private ?DefaultLogger $logger = null
|
?Logger $logger = null
|
||||||
) {
|
) {
|
||||||
$this->logger ??= LoggerFactory::getDefaultLogger();
|
$this->logger = $logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $path): string
|
public function get(string $path): string
|
||||||
{
|
{
|
||||||
$this->logger->debug("Lese Datei: {$path}");
|
$this->logger?->debug("Lese Datei: {$path}");
|
||||||
|
|
||||||
return $this->storage->get($path);
|
return $this->storage->get($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function put(string $path, string $content): void
|
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);
|
$this->storage->put($path, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exists(string $path): bool
|
public function exists(string $path): bool
|
||||||
{
|
{
|
||||||
$exists = $this->storage->exists($path);
|
$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;
|
return $exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(string $path): void
|
public function delete(string $path): void
|
||||||
{
|
{
|
||||||
$this->logger->debug("Lösche Datei: {$path}");
|
$this->logger?->debug("Lösche Datei: {$path}");
|
||||||
$this->storage->delete($path);
|
$this->storage->delete($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function copy(string $source, string $destination): void
|
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);
|
$this->storage->copy($source, $destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function size(string $path): int
|
public function size(string $path): int
|
||||||
{
|
{
|
||||||
$size = $this->storage->size($path);
|
$size = $this->storage->size($path);
|
||||||
$this->logger->debug("Dateigröße: {$path} - {$size} Bytes");
|
$this->logger?->debug("Dateigröße: {$path} - {$size} Bytes");
|
||||||
|
|
||||||
return $size;
|
return $size;
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,7 @@ final class LoggableStorage implements Storage
|
|||||||
public function lastModified(string $path): int
|
public function lastModified(string $path): int
|
||||||
{
|
{
|
||||||
$lastModified = $this->storage->lastModified($path);
|
$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;
|
return $lastModified;
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ final class LoggableStorage implements Storage
|
|||||||
public function getMimeType(string $path): string
|
public function getMimeType(string $path): string
|
||||||
{
|
{
|
||||||
$mimeType = $this->storage->getMimeType($path);
|
$mimeType = $this->storage->getMimeType($path);
|
||||||
$this->logger->debug("MIME-Typ: {$path} - {$mimeType}");
|
$this->logger?->debug("MIME-Typ: {$path} - {$mimeType}");
|
||||||
|
|
||||||
return $mimeType;
|
return $mimeType;
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,7 @@ final class LoggableStorage implements Storage
|
|||||||
public function isReadable(string $path): bool
|
public function isReadable(string $path): bool
|
||||||
{
|
{
|
||||||
$isReadable = $this->storage->isReadable($path);
|
$isReadable = $this->storage->isReadable($path);
|
||||||
$this->logger->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein'));
|
$this->logger?->debug("Lesbar: {$path} - " . ($isReadable ? 'ja' : 'nein'));
|
||||||
|
|
||||||
return $isReadable;
|
return $isReadable;
|
||||||
}
|
}
|
||||||
@@ -93,70 +94,70 @@ final class LoggableStorage implements Storage
|
|||||||
public function isWritable(string $path): bool
|
public function isWritable(string $path): bool
|
||||||
{
|
{
|
||||||
$isWritable = $this->storage->isWritable($path);
|
$isWritable = $this->storage->isWritable($path);
|
||||||
$this->logger->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein'));
|
$this->logger?->debug("Schreibbar: {$path} - " . ($isWritable ? 'ja' : 'nein'));
|
||||||
|
|
||||||
return $isWritable;
|
return $isWritable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listDirectory(string $directory): array
|
public function listDirectory(string $directory): array
|
||||||
{
|
{
|
||||||
$this->logger->debug("Liste Verzeichnis: {$directory}");
|
$this->logger?->debug("Liste Verzeichnis: {$directory}");
|
||||||
$files = $this->storage->listDirectory($directory);
|
$files = $this->storage->listDirectory($directory);
|
||||||
$this->logger->debug("Gefundene Dateien: " . count($files));
|
$this->logger?->debug("Gefundene Dateien: " . count($files));
|
||||||
|
|
||||||
return $files;
|
return $files;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createDirectory(string $path, int $permissions = 0755, bool $recursive = true): void
|
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);
|
$this->storage->createDirectory($path, $permissions, $recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function file(string $path): File
|
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);
|
return $this->storage->file($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function directory(string $path): Directory
|
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);
|
return $this->storage->directory($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function batch(array $operations): array
|
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);
|
$results = $this->storage->batch($operations);
|
||||||
$this->logger->debug("Batch-Operation abgeschlossen");
|
$this->logger?->debug("Batch-Operation abgeschlossen");
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMultiple(array $paths): array
|
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);
|
$results = $this->storage->getMultiple($paths);
|
||||||
$this->logger->debug("Parallel-Lesen abgeschlossen");
|
$this->logger?->debug("Parallel-Lesen abgeschlossen");
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function putMultiple(array $files): void
|
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->storage->putMultiple($files);
|
||||||
$this->logger->debug("Parallel-Schreiben abgeschlossen");
|
$this->logger?->debug("Parallel-Schreiben abgeschlossen");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMetadataMultiple(array $paths): array
|
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);
|
$results = $this->storage->getMetadataMultiple($paths);
|
||||||
$this->logger->debug("Metadaten-Laden abgeschlossen");
|
$this->logger?->debug("Metadaten-Laden abgeschlossen");
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ trait StorageTrait
|
|||||||
*/
|
*/
|
||||||
public function file(string $path): File
|
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
|
public function directory(string $path): Directory
|
||||||
{
|
{
|
||||||
return FilesystemFactory::createDirectory($path, $this);
|
$factory = FilesystemFactory::create($this->logger ?? null);
|
||||||
|
return $factory->createDirectory($path, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ trait AtomicStorageTrait
|
|||||||
{
|
{
|
||||||
public function putAtomic(string $path, string $content): void
|
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();
|
$tempPath = $path . '.tmp.' . $generator->generate();
|
||||||
$this->put($tempPath, $content);
|
$this->put($tempPath, $content);
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ final readonly class FilePath implements Stringable
|
|||||||
*/
|
*/
|
||||||
public static function temp(?string $filename = null): self
|
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();
|
$filename ??= 'tmp_' . $generator->generate();
|
||||||
|
|
||||||
return self::tempDir()->join($filename);
|
return self::tempDir()->join($filename);
|
||||||
|
|||||||
31
src/Framework/Id/Contracts/IdGeneratorInterface.php
Normal file
31
src/Framework/Id/Contracts/IdGeneratorInterface.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Id\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interface for all ID generators
|
||||||
|
*/
|
||||||
|
interface IdGeneratorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate a new ID
|
||||||
|
*
|
||||||
|
* @return IdInterface|string Returns either an IdInterface or string representation
|
||||||
|
*/
|
||||||
|
public function generate(): IdInterface|string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a batch of IDs
|
||||||
|
*
|
||||||
|
* @param int $count Number of IDs to generate
|
||||||
|
* @return array<int, IdInterface|string>
|
||||||
|
*/
|
||||||
|
public function generateBatch(int $count): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if a string is a valid ID for this generator
|
||||||
|
*/
|
||||||
|
public function isValid(string $value): bool;
|
||||||
|
}
|
||||||
26
src/Framework/Id/Contracts/IdInterface.php
Normal file
26
src/Framework/Id/Contracts/IdInterface.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Id\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interface for all ID value objects
|
||||||
|
*/
|
||||||
|
interface IdInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the string representation of the ID
|
||||||
|
*/
|
||||||
|
public function toString(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the string value
|
||||||
|
*/
|
||||||
|
public function getValue(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check equality with another ID
|
||||||
|
*/
|
||||||
|
public function equals(self $other): bool;
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Cuid;
|
namespace App\Framework\Id\Cuid;
|
||||||
|
|
||||||
|
use App\Framework\Id\Contracts\IdInterface;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cuid Value Object
|
* Cuid Value Object
|
||||||
@@ -16,7 +18,7 @@ use InvalidArgumentException;
|
|||||||
* - Optimized for horizontal scaling and collision resistance
|
* - Optimized for horizontal scaling and collision resistance
|
||||||
* - Always starts with 'c' for collision-resistant
|
* - Always starts with 'c' for collision-resistant
|
||||||
*/
|
*/
|
||||||
final readonly class Cuid implements Stringable
|
final readonly class Cuid implements IdInterface, Stringable
|
||||||
{
|
{
|
||||||
public const int LENGTH = 25;
|
public const int LENGTH = 25;
|
||||||
public const string PREFIX = 'c';
|
public const string PREFIX = 'c';
|
||||||
@@ -167,8 +169,12 @@ final readonly class Cuid implements Stringable
|
|||||||
/**
|
/**
|
||||||
* Check equality with another Cuid
|
* Check equality with another Cuid
|
||||||
*/
|
*/
|
||||||
public function equals(self $other): bool
|
public function equals(IdInterface $other): bool
|
||||||
{
|
{
|
||||||
|
if (! $other instanceof self) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->value === $other->value;
|
return $this->value === $other->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
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 App\Framework\Random\RandomGenerator;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ use InvalidArgumentException;
|
|||||||
*
|
*
|
||||||
* Generates Collision-resistant Unique Identifiers with machine fingerprinting.
|
* Generates Collision-resistant Unique Identifiers with machine fingerprinting.
|
||||||
*/
|
*/
|
||||||
final class CuidGenerator
|
final class CuidGenerator implements IdGeneratorInterface
|
||||||
{
|
{
|
||||||
private int $counter = 0;
|
private int $counter = 0;
|
||||||
|
|
||||||
91
src/Framework/Id/IdGeneratorFactory.php
Normal file
91
src/Framework/Id/IdGeneratorFactory.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Id;
|
||||||
|
|
||||||
|
use App\Framework\DateTime\Clock;
|
||||||
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\Id\Contracts\IdGeneratorInterface;
|
||||||
|
use App\Framework\Id\Cuid\CuidGenerator;
|
||||||
|
use App\Framework\Id\Ksuid\KsuidGenerator;
|
||||||
|
use App\Framework\Id\NanoId\NanoId;
|
||||||
|
use App\Framework\Id\NanoId\NanoIdGenerator;
|
||||||
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
|
use App\Framework\Random\RandomGenerator;
|
||||||
|
use App\Framework\Random\SecureRandomGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for creating ID generators
|
||||||
|
*/
|
||||||
|
final readonly class IdGeneratorFactory
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private RandomGenerator $randomGenerator,
|
||||||
|
private ?Clock $clock = null
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a generator for the specified ID type
|
||||||
|
*
|
||||||
|
* @param IdType|string $type The ID type to create a generator for
|
||||||
|
* @return IdGeneratorInterface
|
||||||
|
*/
|
||||||
|
public function create(IdType|string $type): IdGeneratorInterface
|
||||||
|
{
|
||||||
|
if (is_string($type)) {
|
||||||
|
$type = IdType::fromString($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
IdType::CUID => 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/Framework/Id/IdType.php
Normal file
56
src/Framework/Id/IdType.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for ID types supported by the framework
|
||||||
|
*/
|
||||||
|
enum IdType: string
|
||||||
|
{
|
||||||
|
case CUID = 'cuid';
|
||||||
|
case KSUID = 'ksuid';
|
||||||
|
case NANOID = 'nanoid';
|
||||||
|
case ULID = 'ulid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available ID types
|
||||||
|
*
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Ksuid;
|
namespace App\Framework\Id\Ksuid;
|
||||||
|
|
||||||
|
use App\Framework\Id\Contracts\IdInterface;
|
||||||
use BcMath\Number;
|
use BcMath\Number;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KSUID Value Object
|
* KSUID Value Object
|
||||||
@@ -17,7 +19,7 @@ use InvalidArgumentException;
|
|||||||
* - Lexicographically sortable by creation time
|
* - Lexicographically sortable by creation time
|
||||||
* - URL-safe, case-sensitive
|
* - URL-safe, case-sensitive
|
||||||
*/
|
*/
|
||||||
final readonly class Ksuid
|
final readonly class Ksuid implements IdInterface, Stringable
|
||||||
{
|
{
|
||||||
public const int ENCODED_LENGTH = 27;
|
public const int ENCODED_LENGTH = 27;
|
||||||
public const int TIMESTAMP_BYTES = 4;
|
public const int TIMESTAMP_BYTES = 4;
|
||||||
@@ -159,8 +161,12 @@ final readonly class Ksuid
|
|||||||
/**
|
/**
|
||||||
* Check equality with another 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;
|
return $this->value === $other->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
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 App\Framework\Random\RandomGenerator;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@@ -13,7 +14,7 @@ use InvalidArgumentException;
|
|||||||
*
|
*
|
||||||
* Generates K-Sortable Unique Identifiers with timestamp ordering.
|
* Generates K-Sortable Unique Identifiers with timestamp ordering.
|
||||||
*/
|
*/
|
||||||
final readonly class KsuidGenerator
|
final readonly class KsuidGenerator implements IdGeneratorInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RandomGenerator $randomGenerator
|
private RandomGenerator $randomGenerator
|
||||||
@@ -183,7 +184,7 @@ final readonly class KsuidGenerator
|
|||||||
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
|
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)
|
* Generate KSUIDs for a time range (useful for queries)
|
||||||
*/
|
*/
|
||||||
public function generateTimeRange(int $startTimestamp, int $endTimestamp): array
|
public function generateTimeRange(int $startTimestamp, int $endTimestamp): array
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\NanoId;
|
namespace App\Framework\Id\NanoId;
|
||||||
|
|
||||||
|
use App\Framework\Id\Contracts\IdInterface;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Stringable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NanoId Value Object
|
* NanoId Value Object
|
||||||
@@ -14,7 +14,7 @@ use Stringable;
|
|||||||
* Default alphabet: A-Za-z0-9_-
|
* Default alphabet: A-Za-z0-9_-
|
||||||
* Default size: 21 characters
|
* Default size: 21 characters
|
||||||
*/
|
*/
|
||||||
final readonly class NanoId implements Stringable
|
final readonly class NanoId implements IdInterface
|
||||||
{
|
{
|
||||||
public const int DEFAULT_SIZE = 21;
|
public const int DEFAULT_SIZE = 21;
|
||||||
public const string DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-';
|
public const string DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-';
|
||||||
@@ -123,8 +123,12 @@ final readonly class NanoId implements Stringable
|
|||||||
/**
|
/**
|
||||||
* Check equality with another NanoId
|
* 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;
|
return $this->value === $other->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
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 App\Framework\Random\RandomGenerator;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ use InvalidArgumentException;
|
|||||||
*
|
*
|
||||||
* Provides flexible NanoId generation with various presets and configurations.
|
* Provides flexible NanoId generation with various presets and configurations.
|
||||||
*/
|
*/
|
||||||
final readonly class NanoIdGenerator
|
final readonly class NanoIdGenerator implements IdGeneratorInterface
|
||||||
{
|
{
|
||||||
private int $defaultSize;
|
private int $defaultSize;
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Ulid;
|
namespace App\Framework\Id\Ulid;
|
||||||
|
|
||||||
use App\Framework\Core\Encoding\Base32Alphabet;
|
use App\Framework\Core\Encoding\Base32Alphabet;
|
||||||
use App\Framework\Core\Encoding\Base32Encoder;
|
use App\Framework\Core\Encoding\Base32Encoder;
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Ulid;
|
namespace App\Framework\Id\Ulid;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
|
use App\Framework\Id\Contracts\IdInterface;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
@@ -12,7 +13,7 @@ use JsonSerializable;
|
|||||||
/**
|
/**
|
||||||
* Objekt-Wrapper für ULIDs mit String-/JSON-API.
|
* 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;
|
private string $ulid;
|
||||||
|
|
||||||
@@ -62,6 +63,34 @@ final readonly class Ulid implements JsonSerializable
|
|||||||
return $this->ulid;
|
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
|
public function jsonSerialize(): string
|
||||||
{
|
{
|
||||||
return $this->ulid;
|
return $this->ulid;
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Ulid;
|
namespace App\Framework\Id\Ulid;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\DateTime\SystemClock;
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\Id\Contracts\IdGeneratorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ULID Generator - Universally Unique Lexicographically Sortable Identifier
|
* ULID Generator - Universally Unique Lexicographically Sortable Identifier
|
||||||
@@ -17,7 +18,7 @@ use App\Framework\DateTime\SystemClock;
|
|||||||
* - Production: new UlidGenerator() - uses SystemClock automatically
|
* - Production: new UlidGenerator() - uses SystemClock automatically
|
||||||
* - Testing: new UlidGenerator($mockClock) - inject mock for deterministic tests
|
* - Testing: new UlidGenerator($mockClock) - inject mock for deterministic tests
|
||||||
*/
|
*/
|
||||||
final readonly class UlidGenerator
|
final readonly class UlidGenerator implements IdGeneratorInterface
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ?Clock $clock = null
|
private ?Clock $clock = null
|
||||||
@@ -61,4 +62,34 @@ final readonly class UlidGenerator
|
|||||||
{
|
{
|
||||||
return $prefix . '_' . $this->generate();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Ulid;
|
namespace App\Framework\Id\Ulid;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Framework\Ulid;
|
namespace App\Framework\Id\Ulid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validiert ULID-Strings.
|
* Validiert ULID-Strings.
|
||||||
121
src/Framework/Id/UnifiedIdGenerator.php
Normal file
121
src/Framework/Id/UnifiedIdGenerator.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Id;
|
||||||
|
|
||||||
|
use App\Framework\Id\Contracts\IdGeneratorInterface;
|
||||||
|
use App\Framework\Id\Contracts\IdInterface;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified ID Generator
|
||||||
|
*
|
||||||
|
* A wrapper that can generate any type of ID based on configuration.
|
||||||
|
* Provides a single interface for generating different ID formats.
|
||||||
|
*/
|
||||||
|
final readonly class UnifiedIdGenerator implements IdGeneratorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private IdGeneratorFactory $factory,
|
||||||
|
private IdType $defaultType = IdType::ULID
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new ID using the default type
|
||||||
|
*/
|
||||||
|
public function generate(): IdInterface|string
|
||||||
|
{
|
||||||
|
return $this->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<int, IdInterface|string>
|
||||||
|
*/
|
||||||
|
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<int, IdInterface|string>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Logging\Formatter;
|
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\LogLevel;
|
||||||
use App\Framework\Logging\LogRecord;
|
use App\Framework\Logging\LogRecord;
|
||||||
|
|
||||||
@@ -18,6 +21,21 @@ final readonly class DevelopmentFormatter implements LogFormatter
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
public function __invoke(LogRecord $record): string
|
||||||
{
|
{
|
||||||
$level = $record->getLevel();
|
$level = $record->getLevel();
|
||||||
@@ -26,7 +44,7 @@ final readonly class DevelopmentFormatter implements LogFormatter
|
|||||||
$message = $record->getMessage();
|
$message = $record->getMessage();
|
||||||
|
|
||||||
// Color coding for levels
|
// Color coding for levels
|
||||||
$levelString = $this->colorOutput ? $this->colorizeLevel($level) : $level->getName();
|
$levelString = $this->shouldUseColors() ? $this->colorizeLevel($level) : $level->getName();
|
||||||
|
|
||||||
$output = sprintf(
|
$output = sprintf(
|
||||||
"%s [%s] %s.%s: %s\n",
|
"%s [%s] %s.%s: %s\n",
|
||||||
@@ -54,20 +72,11 @@ final readonly class DevelopmentFormatter implements LogFormatter
|
|||||||
|
|
||||||
private function colorizeLevel(LogLevel $level): string
|
private function colorizeLevel(LogLevel $level): string
|
||||||
{
|
{
|
||||||
if (! $this->colorOutput) {
|
// Verwende LogLevel::getConsoleColor() und ConsoleStyle statt hardcoded ANSI-Codes
|
||||||
return $level->getName();
|
$consoleColor = $level->getConsoleColor();
|
||||||
}
|
$style = ConsoleStyle::create(color: $consoleColor);
|
||||||
|
|
||||||
return match($level) {
|
return $style->apply($level->getName());
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatContext(array $context): string
|
private function formatContext(array $context): string
|
||||||
|
|||||||
@@ -4,16 +4,35 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Logging\Formatter;
|
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;
|
use App\Framework\Logging\LogRecord;
|
||||||
|
|
||||||
final readonly class LineFormatter implements LogFormatter
|
final readonly class LineFormatter implements LogFormatter
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $format = "[{timestamp}] {channel}.{level}: {message} {context}",
|
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
|
public function __invoke(LogRecord $record): string
|
||||||
{
|
{
|
||||||
$context = $record->getContext();
|
$context = $record->getContext();
|
||||||
@@ -33,11 +52,17 @@ final readonly class LineFormatter implements LogFormatter
|
|||||||
? "[{$record->getChannel()}] "
|
? "[{$record->getChannel()}] "
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// Level-String mit optionalen Farben
|
||||||
|
$levelName = $record->level->getName();
|
||||||
|
$coloredLevel = $this->shouldUseColors()
|
||||||
|
? $this->colorizeLevel($record->level)
|
||||||
|
: $levelName;
|
||||||
|
|
||||||
$replacements = [
|
$replacements = [
|
||||||
'{timestamp}' => $record->getFormattedTimestamp($this->timestampFormat),
|
'{timestamp}' => $record->getFormattedTimestamp($this->timestampFormat),
|
||||||
'{channel}' => $record->channel ?? 'app',
|
'{channel}' => $record->channel ?? 'app',
|
||||||
'{level}' => $record->level->getName(),
|
'{level}' => $coloredLevel,
|
||||||
'{level_name}' => $record->level->getName(),
|
'{level_name}' => $coloredLevel,
|
||||||
'{message}' => $record->message,
|
'{message}' => $record->message,
|
||||||
'{context}' => $contextString,
|
'{context}' => $contextString,
|
||||||
'{request_id}' => $requestId,
|
'{request_id}' => $requestId,
|
||||||
@@ -45,4 +70,16 @@ final readonly class LineFormatter implements LogFormatter
|
|||||||
|
|
||||||
return strtr($this->format, $replacements);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
190
src/Framework/Logging/HandlerFactory.php
Normal file
190
src/Framework/Logging/HandlerFactory.php
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Logging;
|
||||||
|
|
||||||
|
use App\Framework\Config\EnvKey;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
use App\Framework\Config\TypedConfiguration;
|
||||||
|
use App\Framework\Console\CliSapi;
|
||||||
|
use App\Framework\Console\TerminalDetector;
|
||||||
|
use App\Framework\Core\PathProvider;
|
||||||
|
use App\Framework\Logging\Formatter\DevelopmentFormatter;
|
||||||
|
use App\Framework\Logging\Formatter\LineFormatter;
|
||||||
|
use App\Framework\Logging\Handlers\ConsoleHandler;
|
||||||
|
use App\Framework\Logging\Handlers\DockerJsonHandler;
|
||||||
|
use App\Framework\Logging\Handlers\FileHandler;
|
||||||
|
use App\Framework\Logging\Handlers\MultiFileHandler;
|
||||||
|
use App\Framework\Queue\Queue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory für Log-Handler-Erstellung basierend auf Umgebung und Konfiguration.
|
||||||
|
*/
|
||||||
|
final readonly class HandlerFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 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<LogHandler>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Logging\Handlers;
|
namespace App\Framework\Logging\Handlers;
|
||||||
|
|
||||||
use App\Framework\Logging\LogHandler;
|
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\Formatter\LogFormatter;
|
||||||
use App\Framework\Logging\LogLevel;
|
use App\Framework\Logging\LogLevel;
|
||||||
use App\Framework\Logging\LogRecord;
|
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.
|
* 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 CLI: Nutzt stderr für WARNING+ und stdout für niedrigere Levels.
|
||||||
* Bei Web-Requests: Alle Logs gehen auf stderr (POSIX-konform, Docker-kompatibel).
|
* Bei Web-Requests: Alle Logs gehen auf stderr (POSIX-konform, Docker-kompatibel).
|
||||||
*/
|
*/
|
||||||
@@ -28,26 +34,53 @@ class ConsoleHandler implements LogHandler
|
|||||||
private bool $debugOnly;
|
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
|
* 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 LogLevel|int $minLevel Minimales Level, ab dem dieser Handler aktiv wird
|
||||||
* @param bool $debugOnly Ob der Handler nur im Debug-Modus aktiv ist
|
* @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(
|
public function __construct(
|
||||||
LogFormatter $formatter,
|
LogFormatter|array|null $formatter = null,
|
||||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
LogLevel|int $minLevel = LogLevel::DEBUG,
|
||||||
bool $debugOnly = true,
|
bool $debugOnly = true,
|
||||||
private readonly LogLevel $stderrLevel = LogLevel::WARNING,
|
private readonly LogLevel $stderrLevel = LogLevel::WARNING,
|
||||||
) {
|
) {
|
||||||
$this->formatter = $formatter;
|
|
||||||
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
$this->minLevel = $minLevel instanceof LogLevel ? $minLevel : LogLevel::fromValue($minLevel);
|
||||||
$this->debugOnly = $debugOnly;
|
$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
|
public function handle(LogRecord $record): void
|
||||||
{
|
{
|
||||||
|
// Formatter auswählen (intelligent oder einfach)
|
||||||
|
$formatter = $this->selectFormatter($record);
|
||||||
|
|
||||||
// Formatter verwenden für Formatierung
|
// Formatter verwenden für Formatierung
|
||||||
$formatted = ($this->formatter)($record);
|
$formatted = $formatter($record);
|
||||||
|
|
||||||
// Formatter gibt immer string zurück für Console
|
// Formatter gibt immer string zurück für Console
|
||||||
$output = is_string($formatted)
|
$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;
|
return $this->formatter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Framework\Logging;
|
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
|
||||||
use App\Framework\DateTime\SystemClock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory für Logger-Instanzen.
|
|
||||||
*
|
|
||||||
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
|
|
||||||
* Diese Klasse wird nur noch für Legacy-Code verwendet und sollte nicht in neuem Code genutzt werden.
|
|
||||||
*/
|
|
||||||
final class LoggerFactory
|
|
||||||
{
|
|
||||||
private static ?DefaultLogger $defaultLogger = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erzeugt einen neuen Logger mit optionalen Einstellungen.
|
|
||||||
*
|
|
||||||
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
|
|
||||||
*/
|
|
||||||
public static function create(
|
|
||||||
?Clock $clock = null,
|
|
||||||
LogLevel|int $minLevel = LogLevel::DEBUG,
|
|
||||||
array $handlers = []
|
|
||||||
): DefaultLogger {
|
|
||||||
$clock ??= new SystemClock();
|
|
||||||
|
|
||||||
return new DefaultLogger($clock, $minLevel, $handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt den Standard-Logger zurück oder erstellt ihn, falls er noch nicht existiert.
|
|
||||||
*
|
|
||||||
* @deprecated Verwende stattdessen Dependency Injection. Der Logger wird automatisch über LoggerInitializer erstellt.
|
|
||||||
*/
|
|
||||||
public static function getDefaultLogger(): DefaultLogger
|
|
||||||
{
|
|
||||||
if (self::$defaultLogger === null) {
|
|
||||||
$debug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN);
|
|
||||||
$minLevel = $debug ? LogLevel::DEBUG : LogLevel::INFO;
|
|
||||||
|
|
||||||
self::$defaultLogger = self::create(null, $minLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$defaultLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setzt einen benutzerdefinierten Logger als Standard-Logger.
|
|
||||||
*
|
|
||||||
* @deprecated Verwende stattdessen Dependency Injection.
|
|
||||||
*/
|
|
||||||
public static function setDefaultLogger(DefaultLogger $logger): void
|
|
||||||
{
|
|
||||||
self::$defaultLogger = $logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,17 +11,7 @@ use App\Framework\Core\PathProvider;
|
|||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
use App\Framework\DI\Initializer;
|
use App\Framework\DI\Initializer;
|
||||||
use App\Framework\Logging\Formatter\DevelopmentFormatter;
|
|
||||||
use App\Framework\Logging\Formatter\LineFormatter;
|
|
||||||
use App\Framework\Logging\Handlers\ConsoleHandler;
|
|
||||||
use App\Framework\Logging\Handlers\DockerJsonHandler;
|
|
||||||
use App\Framework\Logging\Handlers\FileHandler;
|
|
||||||
use App\Framework\Logging\Handlers\MultiFileHandler;
|
|
||||||
use App\Framework\Logging\Handlers\NullHandler;
|
use App\Framework\Logging\Handlers\NullHandler;
|
||||||
use App\Framework\Queue\Queue;
|
|
||||||
use App\Framework\Queue\FileQueue;
|
|
||||||
use App\Framework\Redis\RedisConfig;
|
|
||||||
use App\Framework\Redis\RedisConnection;
|
|
||||||
|
|
||||||
final readonly class LoggerInitializer
|
final readonly class LoggerInitializer
|
||||||
{
|
{
|
||||||
@@ -44,8 +34,13 @@ final readonly class LoggerInitializer
|
|||||||
$processorManager = new ProcessorManager();
|
$processorManager = new ProcessorManager();
|
||||||
$minLevel = $this->determineMinLogLevel($config);
|
$minLevel = $this->determineMinLogLevel($config);
|
||||||
$logConfig = $this->initializeLogConfig($pathProvider);
|
$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);
|
$contextManager = $container->get(LogContextManager::class);
|
||||||
$clock = $container->get(Clock::class);
|
$clock = $container->get(Clock::class);
|
||||||
|
|
||||||
@@ -113,117 +108,4 @@ final readonly class LoggerInitializer
|
|||||||
return $logConfig;
|
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<LogHandler>
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/Framework/Logging/QueueFactory.php
Normal file
39
src/Framework/Logging/QueueFactory.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Logging;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
use App\Framework\Core\PathProvider;
|
||||||
|
use App\Framework\Queue\FileQueue;
|
||||||
|
use App\Framework\Queue\Queue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory für Queue-Instanzen für asynchrones Logging.
|
||||||
|
*
|
||||||
|
* Unterstützt aktuell FileQueue, kann in Zukunft um RedisQueue erweitert werden.
|
||||||
|
*/
|
||||||
|
final readonly class QueueFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Erstellt eine Queue-Instanz basierend auf der Konfiguration
|
||||||
|
*
|
||||||
|
* @param PathProvider $pathProvider Path provider für File-basierte Queues
|
||||||
|
* @param Environment|null $env Optional: Environment für zukünftige Redis-Integration
|
||||||
|
* @return Queue
|
||||||
|
*/
|
||||||
|
public function createQueue(PathProvider $pathProvider, ?Environment $env = null): Queue
|
||||||
|
{
|
||||||
|
// TODO: Redis-Queue Integration wenn benötigt
|
||||||
|
// if ($env && $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Logging\ValueObjects;
|
namespace App\Framework\Logging\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\Http\ServerRequest;
|
use App\Framework\Http\ServerRequest;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ namespace App\Framework\MachineLearning\ModelManagement;
|
|||||||
use App\Framework\Cache\Cache;
|
use App\Framework\Cache\Cache;
|
||||||
use App\Framework\Cache\CacheItem;
|
use App\Framework\Cache\CacheItem;
|
||||||
use App\Framework\Cache\CacheKey;
|
use App\Framework\Cache\CacheKey;
|
||||||
use App\Framework\Core\ValueObjects\Version;
|
|
||||||
use App\Framework\Core\ValueObjects\Duration;
|
use App\Framework\Core\ValueObjects\Duration;
|
||||||
use App\Framework\Core\ValueObjects\Timestamp;
|
use App\Framework\Core\ValueObjects\Timestamp;
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
use App\Framework\Core\ValueObjects\Version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache-based Performance Storage
|
* Cache-based Performance Storage
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ use App\Framework\Cache\Cache;
|
|||||||
use App\Framework\Cache\CacheKey;
|
use App\Framework\Cache\CacheKey;
|
||||||
use App\Framework\Core\ValueObjects\Duration;
|
use App\Framework\Core\ValueObjects\Duration;
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
use App\Framework\MagicLinks\MagicLinkData;
|
use App\Framework\MagicLinks\MagicLinkData;
|
||||||
use App\Framework\MagicLinks\MagicLinkToken;
|
use App\Framework\MagicLinks\MagicLinkToken;
|
||||||
use App\Framework\MagicLinks\TokenAction;
|
use App\Framework\MagicLinks\TokenAction;
|
||||||
use App\Framework\MagicLinks\TokenConfig;
|
use App\Framework\MagicLinks\TokenConfig;
|
||||||
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
|
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
|
||||||
final readonly class CacheMagicLinkService implements MagicLinkService
|
final readonly class CacheMagicLinkService implements MagicLinkService
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\MagicLinks\Services;
|
namespace App\Framework\MagicLinks\Services;
|
||||||
|
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
use App\Framework\MagicLinks\MagicLinkData;
|
use App\Framework\MagicLinks\MagicLinkData;
|
||||||
use App\Framework\MagicLinks\MagicLinkToken;
|
use App\Framework\MagicLinks\MagicLinkToken;
|
||||||
use App\Framework\MagicLinks\TokenAction;
|
use App\Framework\MagicLinks\TokenAction;
|
||||||
use App\Framework\MagicLinks\TokenConfig;
|
use App\Framework\MagicLinks\TokenConfig;
|
||||||
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
|
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
|
||||||
|
|
||||||
final class InMemoryMagicLinkService implements MagicLinkService
|
final class InMemoryMagicLinkService implements MagicLinkService
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ final class SmtpTransport implements TransportInterface
|
|||||||
|
|
||||||
private function buildMultipartAlternativeMessage(Message $message, array $lines): string
|
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();
|
$boundary = 'alt_' . $generator->generate();
|
||||||
|
|
||||||
$lines[] = 'MIME-Version: 1.0';
|
$lines[] = 'MIME-Version: 1.0';
|
||||||
@@ -292,7 +292,7 @@ final class SmtpTransport implements TransportInterface
|
|||||||
|
|
||||||
private function buildMultipartMixedMessage(Message $message, array $lines): string
|
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();
|
$boundary = 'mixed_' . $generator->generate();
|
||||||
|
|
||||||
$lines[] = 'MIME-Version: 1.0';
|
$lines[] = 'MIME-Version: 1.0';
|
||||||
@@ -377,7 +377,7 @@ final class SmtpTransport implements TransportInterface
|
|||||||
|
|
||||||
private function generateMessageId(): string
|
private function generateMessageId(): string
|
||||||
{
|
{
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
return $generator->generate() . '.' . time() . '@' . gethostname();
|
return $generator->generate() . '.' . time() . '@' . gethostname();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ final class SmtpTransport implements TransportInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to generated ID
|
// Fallback to generated ID
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
return $generator->generate() . '@' . gethostname();
|
return $generator->generate() . '@' . gethostname();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
$messageId = 'mock_' . $generator->generate();
|
||||||
$this->sentMessages[] = [
|
$this->sentMessages[] = [
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Notification\Templates;
|
namespace App\Framework\Notification\Templates;
|
||||||
|
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Template Identifier Value Object
|
* Template Identifier Value Object
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Notification\ValueObjects;
|
namespace App\Framework\Notification\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\DateTime\SystemClock;
|
use App\Framework\DateTime\SystemClock;
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
use App\Framework\Ulid\UlidValidator;
|
use App\Framework\Id\Ulid\UlidValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique identifier for notifications
|
* Unique identifier for notifications
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ namespace App\Framework\Queue\Entities;
|
|||||||
use App\Framework\Database\Attributes\Column;
|
use App\Framework\Database\Attributes\Column;
|
||||||
use App\Framework\Database\Attributes\Entity;
|
use App\Framework\Database\Attributes\Entity;
|
||||||
use App\Framework\Database\Attributes\Id;
|
use App\Framework\Database\Attributes\Id;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
|
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
|
||||||
use App\Framework\Queue\ValueObjects\FailureReason;
|
use App\Framework\Queue\ValueObjects\FailureReason;
|
||||||
use App\Framework\Queue\ValueObjects\JobPayload;
|
use App\Framework\Queue\ValueObjects\JobPayload;
|
||||||
use App\Framework\Queue\ValueObjects\QueueName;
|
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
|
* Entity representing a job that failed and was moved to the dead letter queue
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace App\Framework\Queue\Entities;
|
|||||||
use App\Framework\Database\Attributes\Column;
|
use App\Framework\Database\Attributes\Column;
|
||||||
use App\Framework\Database\Attributes\Entity;
|
use App\Framework\Database\Attributes\Entity;
|
||||||
use App\Framework\Database\Attributes\Id;
|
use App\Framework\Database\Attributes\Id;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
|
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
|
||||||
use App\Framework\Queue\ValueObjects\JobChain;
|
use App\Framework\Queue\ValueObjects\JobChain;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity representing a job chain entry in the database
|
* Entity representing a job chain entry in the database
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace App\Framework\Queue\Entities;
|
|||||||
use App\Framework\Database\Attributes\Column;
|
use App\Framework\Database\Attributes\Column;
|
||||||
use App\Framework\Database\Attributes\Entity;
|
use App\Framework\Database\Attributes\Entity;
|
||||||
use App\Framework\Database\Attributes\Id;
|
use App\Framework\Database\Attributes\Id;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Queue\ValueObjects\DependencyType;
|
use App\Framework\Queue\ValueObjects\DependencyType;
|
||||||
use App\Framework\Queue\ValueObjects\JobDependency;
|
use App\Framework\Queue\ValueObjects\JobDependency;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity representing a job dependency entry in the database
|
* Entity representing a job dependency entry in the database
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ namespace App\Framework\Queue\Entities;
|
|||||||
use App\Framework\Database\Attributes\Column;
|
use App\Framework\Database\Attributes\Column;
|
||||||
use App\Framework\Database\Attributes\Entity;
|
use App\Framework\Database\Attributes\Entity;
|
||||||
use App\Framework\Database\Attributes\Id;
|
use App\Framework\Database\Attributes\Id;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Queue\ValueObjects\JobMetrics;
|
use App\Framework\Queue\ValueObjects\JobMetrics;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
#[Entity(table: 'job_metrics')]
|
#[Entity(table: 'job_metrics')]
|
||||||
final readonly class JobMetricsEntry
|
final readonly class JobMetricsEntry
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use App\Framework\Core\ValueObjects\Percentage;
|
|||||||
use App\Framework\Database\Attributes\Column;
|
use App\Framework\Database\Attributes\Column;
|
||||||
use App\Framework\Database\Attributes\Entity;
|
use App\Framework\Database\Attributes\Entity;
|
||||||
use App\Framework\Database\Attributes\Id;
|
use App\Framework\Database\Attributes\Id;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Queue\ValueObjects\JobProgress;
|
use App\Framework\Queue\ValueObjects\JobProgress;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity representing a job progress tracking entry
|
* Entity representing a job progress tracking entry
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ final readonly class FileQueue implements Queue
|
|||||||
private function generatePriorityFilename(float $score): string
|
private function generatePriorityFilename(float $score): string
|
||||||
{
|
{
|
||||||
$scoreStr = str_pad((string) (int) ($score * 1000000), 15, '0', STR_PAD_LEFT);
|
$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';
|
return "job_{$scoreStr}_" . $generator->generate() . '.json';
|
||||||
}
|
}
|
||||||
@@ -349,7 +349,7 @@ final readonly class FileQueue implements Queue
|
|||||||
*/
|
*/
|
||||||
private function generateDelayedFilename(int $availableTime): string
|
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';
|
return "delayed_{$availableTime}_" . $generator->generate() . '.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ final readonly class QueueJobFeatureExtractor
|
|||||||
foreach ($metricsHistory as $metrics) {
|
foreach ($metricsHistory as $metrics) {
|
||||||
// Create minimal metadata from metrics
|
// Create minimal metadata from metrics
|
||||||
$metadata = new JobMetadata(
|
$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),
|
class: \App\Framework\Core\ValueObjects\ClassName::create($metrics->queueName),
|
||||||
type: 'job',
|
type: 'job',
|
||||||
queuedAt: \App\Framework\Core\ValueObjects\Timestamp::now(),
|
queuedAt: \App\Framework\Core\ValueObjects\Timestamp::now(),
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
|
|||||||
|
|
||||||
private function generateBatchId(): string
|
private function generateBatchId(): string
|
||||||
{
|
{
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
return 'batch_' . $generator->generate();
|
return 'batch_' . $generator->generate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Queue\ValueObjects;
|
namespace App\Framework\Queue\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value Object representing a unique Job identifier
|
* Value Object representing a unique Job identifier
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use App\Framework\Core\ValueObjects\ClassName;
|
|||||||
use App\Framework\Core\ValueObjects\Duration;
|
use App\Framework\Core\ValueObjects\Duration;
|
||||||
use App\Framework\Core\ValueObjects\Timestamp;
|
use App\Framework\Core\ValueObjects\Timestamp;
|
||||||
use App\Framework\DateTime\SystemClock;
|
use App\Framework\DateTime\SystemClock;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Job Metadata Value Object
|
* Job Metadata Value Object
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Queue\ValueObjects;
|
namespace App\Framework\Queue\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\Ulid\UlidGenerator;
|
use App\Framework\Id\Ulid\UlidGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value Object representing a unique Worker identifier
|
* Value Object representing a unique Worker identifier
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ final readonly class ConsoleTraceExporter implements TraceExporter
|
|||||||
$spanMap = [];
|
$spanMap = [];
|
||||||
$rootSpans = [];
|
$rootSpans = [];
|
||||||
|
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
// First, create a map of all spans
|
// First, create a map of all spans
|
||||||
foreach ($spans as $span) {
|
foreach ($spans as $span) {
|
||||||
$spanId = $span['spanId'] ?? $generator->generate();
|
$spanId = $span['spanId'] ?? $generator->generate();
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ final readonly class DatabaseTraceExporter implements TraceExporter
|
|||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||||
";
|
";
|
||||||
|
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
$traceId = $traceData['traceId'] ?? $generator->generate();
|
$traceId = $traceData['traceId'] ?? $generator->generate();
|
||||||
$startTime = $traceData['startTime'] ?? microtime(true);
|
$startTime = $traceData['startTime'] ?? microtime(true);
|
||||||
$endTime = $traceData['endTime'] ?? ($startTime + ($traceData['duration'] ?? 0));
|
$endTime = $traceData['endTime'] ?? ($startTime + ($traceData['duration'] ?? 0));
|
||||||
@@ -120,7 +120,7 @@ final readonly class DatabaseTraceExporter implements TraceExporter
|
|||||||
// Remove trailing comma
|
// Remove trailing comma
|
||||||
$sql = rtrim($sql, ',');
|
$sql = rtrim($sql, ',');
|
||||||
|
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
$values = [];
|
$values = [];
|
||||||
foreach ($spans as $span) {
|
foreach ($spans as $span) {
|
||||||
$spanStartTime = $span['startTime'] ?? microtime(true);
|
$spanStartTime = $span['startTime'] ?? microtime(true);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ final readonly class JaegerExporter implements TraceExporter
|
|||||||
|
|
||||||
private function convertToJaegerFormat(array $traceData): array
|
private function convertToJaegerFormat(array $traceData): array
|
||||||
{
|
{
|
||||||
$generator = new \App\Framework\Ulid\UlidGenerator();
|
$generator = new \App\Framework\Id\Ulid\UlidGenerator();
|
||||||
$traceId = $traceData['traceId'] ?? $generator->generate();
|
$traceId = $traceData['traceId'] ?? $generator->generate();
|
||||||
$spans = [];
|
$spans = [];
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\TypeCaster\Casters;
|
namespace App\Framework\TypeCaster\Casters;
|
||||||
|
|
||||||
use App\Framework\DateTime\SystemClock;
|
use App\Framework\DateTime\SystemClock;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\TypeCaster\TypeCasterInterface;
|
use App\Framework\TypeCaster\TypeCasterInterface;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
final readonly class UlidCaster implements TypeCasterInterface
|
final readonly class UlidCaster implements TypeCasterInterface
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Validation\Rules;
|
namespace App\Framework\Validation\Rules;
|
||||||
|
|
||||||
use App\Framework\Ulid\UlidValidator;
|
use App\Framework\Id\Ulid\UlidValidator;
|
||||||
use App\Framework\Validation\ValidationRule;
|
use App\Framework\Validation\ValidationRule;
|
||||||
use Attribute;
|
use Attribute;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use App\Framework\Database\ConnectionInterface;
|
|||||||
use App\Framework\Database\ValueObjects\SqlQuery;
|
use App\Framework\Database\ValueObjects\SqlQuery;
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\Http\IpAddress;
|
use App\Framework\Http\IpAddress;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\UserAgent\UserAgent;
|
use App\Framework\UserAgent\UserAgent;
|
||||||
use App\Framework\Vault\Exceptions\VaultException;
|
use App\Framework\Vault\Exceptions\VaultException;
|
||||||
use App\Framework\Vault\Exceptions\VaultKeyNotFoundException;
|
use App\Framework\Vault\Exceptions\VaultKeyNotFoundException;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use App\Framework\Http\Exception\NotFound;
|
|||||||
use App\Framework\Http\HttpRequest;
|
use App\Framework\Http\HttpRequest;
|
||||||
use App\Framework\Http\MimeType;
|
use App\Framework\Http\MimeType;
|
||||||
use App\Framework\Http\Responses\JsonResponse;
|
use App\Framework\Http\Responses\JsonResponse;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$this->imageRepository = Mockery::mock(ImageRepository::class);
|
$this->imageRepository = Mockery::mock(ImageRepository::class);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use App\Framework\Core\ValueObjects\Hash;
|
|||||||
use App\Framework\DateTime\SystemClock;
|
use App\Framework\DateTime\SystemClock;
|
||||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||||
use App\Framework\Http\MimeType;
|
use App\Framework\Http\MimeType;
|
||||||
use App\Framework\Ulid\Ulid;
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// Create test directory structure
|
// Create test directory structure
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ use App\Framework\Exception\FrameworkException;
|
|||||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||||
use App\Framework\Http\HttpRequest;
|
use App\Framework\Http\HttpRequest;
|
||||||
use App\Framework\Http\MimeType;
|
use App\Framework\Http\MimeType;
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Router\Result\FileResult;
|
use App\Framework\Router\Result\FileResult;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// Create test directory and file
|
// Create test directory and file
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Id\Ulid\Ulid;
|
||||||
use App\Framework\Queue\ValueObjects\JobId;
|
use App\Framework\Queue\ValueObjects\JobId;
|
||||||
use App\Framework\Ulid\Ulid;
|
|
||||||
|
|
||||||
describe('JobId Value Object', function () {
|
describe('JobId Value Object', function () {
|
||||||
|
|
||||||
|
|||||||
44
tests/Unit/Framework/Core/System/Ini/AccessTest.php
Normal file
44
tests/Unit/Framework/Core/System/Ini/AccessTest.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Core\System\Ini\Access;
|
||||||
|
|
||||||
|
describe('Access', function () {
|
||||||
|
it('converts INI_USER bitmask to USER enum', function () {
|
||||||
|
$access = Access::fromBitmask(INI_USER);
|
||||||
|
|
||||||
|
expect($access)->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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
95
tests/Unit/Framework/Core/System/Ini/IniDirectiveTest.php
Normal file
95
tests/Unit/Framework/Core/System/Ini/IniDirectiveTest.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Core\System\Ini\Access;
|
||||||
|
use App\Framework\Core\System\Ini\IniDirective;
|
||||||
|
|
||||||
|
describe('IniDirective', function () {
|
||||||
|
it('returns Access enum from getAccess()', function () {
|
||||||
|
$directive = new IniDirective(
|
||||||
|
name: 'test_directive',
|
||||||
|
value: 'test_value',
|
||||||
|
global: 'global_value',
|
||||||
|
accessMask: INI_USER
|
||||||
|
);
|
||||||
|
|
||||||
|
$access = $directive->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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
70
tests/Unit/Framework/Core/System/Ini/IniKeyTest.php
Normal file
70
tests/Unit/Framework/Core/System/Ini/IniKeyTest.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Core\System\Ini\IniKey;
|
||||||
|
|
||||||
|
describe('IniKey', function () {
|
||||||
|
it('has correct string value for MEMORY_LIMIT', function () {
|
||||||
|
expect(IniKey::MEMORY_LIMIT->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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
165
tests/Unit/Framework/Core/System/Ini/IniManagerTest.php
Normal file
165
tests/Unit/Framework/Core/System/Ini/IniManagerTest.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Core\System\Ini\Access;
|
||||||
|
use App\Framework\Core\System\Ini\IniDirective;
|
||||||
|
use App\Framework\Core\System\Ini\IniKey;
|
||||||
|
use App\Framework\Core\System\Ini\IniManager;
|
||||||
|
|
||||||
|
describe('IniManager', function () {
|
||||||
|
it('gets ini value by IniKey enum', function () {
|
||||||
|
$manager = new IniManager();
|
||||||
|
$value = $manager->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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
113
tests/Unit/Framework/Core/System/SystemConfigTest.php
Normal file
113
tests/Unit/Framework/Core/System/SystemConfigTest.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
use App\Framework\Core\System\Ini\IniKey;
|
||||||
|
use App\Framework\Core\System\Ini\IniManager;
|
||||||
|
use App\Framework\Core\System\SystemConfig;
|
||||||
|
|
||||||
|
describe('SystemConfig', function () {
|
||||||
|
it('provides access to ini manager via property', function () {
|
||||||
|
$iniManager = new IniManager();
|
||||||
|
$environment = new Environment();
|
||||||
|
$config = new SystemConfig($iniManager, $environment);
|
||||||
|
|
||||||
|
expect($config->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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user