refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives

This commit is contained in:
2025-11-04 13:44:27 +01:00
parent 980714f656
commit bfce93ce77
110 changed files with 2828 additions and 774 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

@@ -31,8 +31,8 @@ final class TreeHelper
private array $nodes = []; private array $nodes = [];
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);
} }
} }
} }

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 () {

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

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

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

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

View 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