diff --git a/src/Entity/LogEntry.php b/src/Entity/LogEntry.php
index a766fcc..4601eae 100644
--- a/src/Entity/LogEntry.php
+++ b/src/Entity/LogEntry.php
@@ -112,6 +112,11 @@ class LogEntry
return $this->getExtra()['request_id'] ?? '-';
}
+ public function getErrorCode(): string
+ {
+ return $this->getExtra()['error_code'] ?? '-';
+ }
+
public function getUri(): string
{
return $this->getExtra()['uri'] ?? '';
diff --git a/src/Logger/ErrorCodeProcessor.php b/src/Logger/ErrorCodeProcessor.php
new file mode 100644
index 0000000..794ae13
--- /dev/null
+++ b/src/Logger/ErrorCodeProcessor.php
@@ -0,0 +1,43 @@
+level->value < Level::Error->value) {
+ return $record;
+ }
+
+ $errorCode = $this->errorCodeService->getErrorCode();
+ if (null === $errorCode) {
+ return $record;
+ }
+
+ $this->errorCodeService->markErrorOccurred();
+ $record->extra['error_code'] = $errorCode;
+
+ return $record;
+ }
+}
diff --git a/src/Service/ErrorCodeService.php b/src/Service/ErrorCodeService.php
new file mode 100644
index 0000000..203863a
--- /dev/null
+++ b/src/Service/ErrorCodeService.php
@@ -0,0 +1,99 @@
+errorCode) {
+ return $this->errorCode;
+ }
+
+ $request = $this->requestStack->getMainRequest();
+ if (null === $request) {
+ return null;
+ }
+
+ $requestId = $request->attributes->get('request_id');
+ if (null === $requestId) {
+ return null;
+ }
+
+ $this->errorCode = $this->generateCode((string) $requestId);
+
+ return $this->errorCode;
+ }
+
+ /**
+ * Marks that an error has occurred during this request.
+ *
+ * Called by the ErrorCodeProcessor when a log entry with ERROR level
+ * or higher is created.
+ */
+ public function markErrorOccurred(): void
+ {
+ $this->hasError = true;
+ }
+
+ /**
+ * Checks whether an error has been logged during this request.
+ *
+ * @return bool True if an error has been logged, false otherwise
+ */
+ public function hasError(): bool
+ {
+ return $this->hasError;
+ }
+
+ /**
+ * Generates an error code from a request ID.
+ *
+ * Strips sequence suffixes, hashes the normalized ID, and converts
+ * to a base36 representation prefixed with E-.
+ *
+ * @param string $requestId The request ID to generate a code from
+ *
+ * @return string The generated error code in format E-XXXXXXXX
+ */
+ private function generateCode(string $requestId): string
+ {
+ $normalizedId = preg_replace('/_\d+$/', '', $requestId);
+ $hash = md5((string) $normalizedId);
+ $hexPart = substr($hash, 0, 12);
+ $decimal = hexdec($hexPart);
+ $base36 = strtoupper(base_convert((string) $decimal, 10, 36));
+ $padded = str_pad($base36, 8, '0', STR_PAD_LEFT);
+
+ return 'E-'.substr($padded, 0, 8);
+ }
+}
diff --git a/src/Twig/ErrorCodeExtension.php b/src/Twig/ErrorCodeExtension.php
new file mode 100644
index 0000000..e321748
--- /dev/null
+++ b/src/Twig/ErrorCodeExtension.php
@@ -0,0 +1,35 @@
+errorCodeService->getErrorCode();
+ }
+
+ public function hasErrorCode(): bool
+ {
+ return $this->errorCodeService->hasError();
+ }
+}
diff --git a/templates/_partials/_alert.html.twig b/templates/_partials/_alert.html.twig
index 725dd31..428a910 100644
--- a/templates/_partials/_alert.html.twig
+++ b/templates/_partials/_alert.html.twig
@@ -35,6 +35,11 @@
{% endif %}
+ {% if level == 'error' and has_error_code() %}
+
+ Fehlercode (bitte angeben): {{ error_code() }}
+
+ {% endif %}
diff --git a/tests/Logger/ErrorCodeProcessorTest.php b/tests/Logger/ErrorCodeProcessorTest.php
new file mode 100644
index 0000000..0e32898
--- /dev/null
+++ b/tests/Logger/ErrorCodeProcessorTest.php
@@ -0,0 +1,184 @@
+errorCodeService = $this->createMock(ErrorCodeService::class);
+ $this->processor = new ErrorCodeProcessor($this->errorCodeService);
+ }
+
+ public function testIgnoresDebugLevel(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Debug,
+ message: 'Debug message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('getErrorCode');
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayNotHasKey('error_code', $result->extra);
+ }
+
+ public function testIgnoresInfoLevel(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Info,
+ message: 'Info message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('getErrorCode');
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayNotHasKey('error_code', $result->extra);
+ }
+
+ public function testIgnoresWarningLevel(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Warning,
+ message: 'Warning message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('getErrorCode');
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayNotHasKey('error_code', $result->extra);
+ }
+
+ public function testAddsErrorCodeForErrorLevel(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Error,
+ message: 'Error message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('getErrorCode')
+ ->willReturn('E-ABC12345');
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayHasKey('error_code', $result->extra);
+ $this->assertSame('E-ABC12345', $result->extra['error_code']);
+ }
+
+ public function testAddsErrorCodeForCriticalLevel(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Critical,
+ message: 'Critical message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('getErrorCode')
+ ->willReturn('E-XYZ78901');
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayHasKey('error_code', $result->extra);
+ $this->assertSame('E-XYZ78901', $result->extra['error_code']);
+ }
+
+ public function testAddsErrorCodeForEmergencyLevel(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Emergency,
+ message: 'Emergency message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('getErrorCode')
+ ->willReturn('E-EMR99999');
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayHasKey('error_code', $result->extra);
+ $this->assertSame('E-EMR99999', $result->extra['error_code']);
+ }
+
+ public function testDoesNotAddCodeWhenServiceReturnsNull(): void
+ {
+ $record = new LogRecord(
+ datetime: new \DateTimeImmutable(),
+ channel: 'test',
+ level: Level::Error,
+ message: 'Error message',
+ );
+
+ $this->errorCodeService
+ ->expects($this->once())
+ ->method('getErrorCode')
+ ->willReturn(null);
+
+ $this->errorCodeService
+ ->expects($this->never())
+ ->method('markErrorOccurred');
+
+ $result = ($this->processor)($record);
+
+ $this->assertArrayNotHasKey('error_code', $result->extra);
+ }
+}
diff --git a/tests/Service/ErrorCodeServiceTest.php b/tests/Service/ErrorCodeServiceTest.php
new file mode 100644
index 0000000..551bc72
--- /dev/null
+++ b/tests/Service/ErrorCodeServiceTest.php
@@ -0,0 +1,146 @@
+requestStack = new RequestStack();
+ $this->service = new ErrorCodeService($this->requestStack);
+ }
+
+ public function testReturnsNullWithoutRequest(): void
+ {
+ $result = $this->service->getErrorCode();
+
+ $this->assertNull($result);
+ }
+
+ public function testReturnsNullWithoutRequestId(): void
+ {
+ $request = new Request();
+ $this->requestStack->push($request);
+
+ $result = $this->service->getErrorCode();
+
+ $this->assertNull($result);
+ }
+
+ public function testGeneratesCodeWithRequestId(): void
+ {
+ $request = new Request();
+ $request->attributes->set('request_id', 'abc123');
+ $this->requestStack->push($request);
+
+ $result = $this->service->getErrorCode();
+
+ $this->assertNotNull($result);
+ $this->assertMatchesRegularExpression('/^E-[A-Z0-9]{8}$/', $result);
+ }
+
+ public function testGeneratesConsistentCodeForSameRequestId(): void
+ {
+ $request = new Request();
+ $request->attributes->set('request_id', 'abc123');
+ $this->requestStack->push($request);
+
+ $result1 = $this->service->getErrorCode();
+ $result2 = $this->service->getErrorCode();
+
+ $this->assertSame($result1, $result2);
+ }
+
+ public function testGeneratesDifferentCodesForDifferentRequestIds(): void
+ {
+ $request1 = new Request();
+ $request1->attributes->set('request_id', 'abc123');
+ $this->requestStack->push($request1);
+
+ $service1 = new ErrorCodeService($this->requestStack);
+ $result1 = $service1->getErrorCode();
+
+ $this->requestStack->pop();
+
+ $request2 = new Request();
+ $request2->attributes->set('request_id', 'xyz789');
+ $this->requestStack->push($request2);
+
+ $service2 = new ErrorCodeService($this->requestStack);
+ $result2 = $service2->getErrorCode();
+
+ $this->assertNotSame($result1, $result2);
+ }
+
+ public function testSequenceSuffixesProduceSameCode(): void
+ {
+ $request1 = new Request();
+ $request1->attributes->set('request_id', 'abc123_1');
+ $this->requestStack->push($request1);
+
+ $service1 = new ErrorCodeService($this->requestStack);
+ $result1 = $service1->getErrorCode();
+
+ $this->requestStack->pop();
+
+ $request2 = new Request();
+ $request2->attributes->set('request_id', 'abc123_2');
+ $this->requestStack->push($request2);
+
+ $service2 = new ErrorCodeService($this->requestStack);
+ $result2 = $service2->getErrorCode();
+
+ $this->assertSame($result1, $result2);
+ }
+
+ public function testSequenceSuffixMatchesBaseId(): void
+ {
+ $request1 = new Request();
+ $request1->attributes->set('request_id', 'abc123');
+ $this->requestStack->push($request1);
+
+ $service1 = new ErrorCodeService($this->requestStack);
+ $result1 = $service1->getErrorCode();
+
+ $this->requestStack->pop();
+
+ $request2 = new Request();
+ $request2->attributes->set('request_id', 'abc123_5');
+ $this->requestStack->push($request2);
+
+ $service2 = new ErrorCodeService($this->requestStack);
+ $result2 = $service2->getErrorCode();
+
+ $this->assertSame($result1, $result2);
+ }
+
+ public function testHasErrorReturnsFalseByDefault(): void
+ {
+ $this->assertFalse($this->service->hasError());
+ }
+
+ public function testMarkErrorOccurredSetsFlag(): void
+ {
+ $this->service->markErrorOccurred();
+
+ $this->assertTrue($this->service->hasError());
+ }
+
+ public function testHasErrorRemainsTrueAfterMultipleCalls(): void
+ {
+ $this->service->markErrorOccurred();
+ $this->service->markErrorOccurred();
+
+ $this->assertTrue($this->service->hasError());
+ }
+}