diff --git a/baseline.xml b/baseline.xml
index 552bc8ad4..0dd1d9878 100644
--- a/baseline.xml
+++ b/baseline.xml
@@ -50,6 +50,18 @@
]]>
+
+
+ $context
+
+
+ $context
+
+
+ errorContext]]>
+ errorContext]]>
+
+
projectors]]>
@@ -135,12 +147,6 @@
$name
-
-
- toString
- toString
-
-
$id
@@ -153,11 +159,6 @@
$name
-
-
- toString
-
-
$id
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index b54320150..6262131e1 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -5,6 +5,11 @@ parameters:
count: 1
path: src/EventBus/Message.php
+ -
+ message: "#^Parameter \\#2 \\$errorContext of class Patchlevel\\\\EventSourcing\\\\Projection\\\\Projection\\\\ProjectionError constructor expects array\\\\|null, mixed given\\.$#"
+ count: 1
+ path: src/Projection/Projection/Store/DoctrineStore.php
+
-
message: "#^Method Patchlevel\\\\EventSourcing\\\\Projection\\\\Projector\\\\InMemoryProjectorRepository\\:\\:projectors\\(\\) should return array\\ but returns array\\\\.$#"
count: 1
diff --git a/src/Console/Command/ProjectionStatusCommand.php b/src/Console/Command/ProjectionStatusCommand.php
index e07d7c581..4f2796e1b 100644
--- a/src/Console/Command/ProjectionStatusCommand.php
+++ b/src/Console/Command/ProjectionStatusCommand.php
@@ -8,14 +8,16 @@
use Patchlevel\EventSourcing\Console\OutputStyle;
use Patchlevel\EventSourcing\Projection\Projection\Projection;
use Patchlevel\EventSourcing\Projection\Projection\ProjectionId;
+use Patchlevel\EventSourcing\Projection\Projection\Store\ErrorContext;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-use Throwable;
use function array_map;
+use function is_array;
+/** @psalm-import-type Context from ErrorContext */
#[AsCommand(
'event-sourcing:projection:status',
'View the current status of the projections',
@@ -89,12 +91,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
],
);
- $errorObject = $projection->projectionError()?->errorObject;
+ $contexts = $projection->projectionError()?->errorContext;
- if ($errorObject instanceof Throwable) {
- $io->throwable($errorObject);
+ if (is_array($contexts)) {
+ foreach ($contexts as $context) {
+ $this->displayError($io, $context);
+ }
}
return 0;
}
+
+ /** @param Context $context */
+ private function displayError(OutputStyle $io, array $context): void
+ {
+ $io->error($context['message']);
+ $io->block($context['trace']);
+ }
}
diff --git a/src/Projection/Projection/ProjectionError.php b/src/Projection/Projection/ProjectionError.php
index beba99847..1500b768d 100644
--- a/src/Projection/Projection/ProjectionError.php
+++ b/src/Projection/Projection/ProjectionError.php
@@ -4,18 +4,21 @@
namespace Patchlevel\EventSourcing\Projection\Projection;
+use Patchlevel\EventSourcing\Projection\Projection\Store\ErrorContext;
use Throwable;
+/** @psalm-import-type Context from ErrorContext */
final class ProjectionError
{
+ /** @param list|null $errorContext */
public function __construct(
public readonly string $errorMessage,
- public readonly Throwable|null $errorObject = null,
+ public readonly array|null $errorContext = null,
) {
}
public static function fromThrowable(Throwable $error): self
{
- return new self($error->getMessage(), $error);
+ return new self($error->getMessage(), ErrorContext::fromThrowable($error));
}
}
diff --git a/src/Projection/Projection/Store/DoctrineStore.php b/src/Projection/Projection/Store/DoctrineStore.php
index ff8ebb619..fd2e6c00b 100644
--- a/src/Projection/Projection/Store/DoctrineStore.php
+++ b/src/Projection/Projection/Store/DoctrineStore.php
@@ -16,6 +16,10 @@
use Patchlevel\EventSourcing\Schema\SchemaConfigurator;
use function array_map;
+use function json_decode;
+use function json_encode;
+
+use const JSON_THROW_ON_ERROR;
/** @psalm-type Data = array{
* name: string,
@@ -23,7 +27,7 @@
* position: int,
* status: string,
* error_message: string|null,
- * error_object: string|null,
+ * error_context: string|null,
* retry: int,
* }
*/
@@ -77,13 +81,16 @@ public function all(): ProjectionCollection
/** @param Data $row */
private function createProjection(array $row): Projection
{
+ $context = $row['error_context'] ?
+ json_decode($row['error_context'], true, 512, JSON_THROW_ON_ERROR) : null;
+
return new Projection(
new ProjectionId($row['name'], $row['version']),
ProjectionStatus::from($row['status']),
$row['position'],
$row['error_message'] ? new ProjectionError(
$row['error_message'],
- ErrorSerializer::unserialize($row['error_object']),
+ $context,
) : null,
$row['retry'],
);
@@ -94,7 +101,7 @@ public function save(Projection ...$projections): void
$this->connection->transactional(
function (Connection $connection) use ($projections): void {
foreach ($projections as $projection) {
- $errorObject = ErrorSerializer::serialize($projection->projectionError()?->errorObject);
+ $projectionError = $projection->projectionError();
try {
$effectedRows = (int)$connection->update(
@@ -102,8 +109,8 @@ function (Connection $connection) use ($projections): void {
[
'position' => $projection->position(),
'status' => $projection->status()->value,
- 'error_message' => $projection->projectionError()?->errorMessage,
- 'error_object' => $errorObject,
+ 'error_message' => $projectionError?->errorMessage,
+ 'error_context' => $projectionError?->errorContext ? json_encode($projectionError->errorContext, JSON_THROW_ON_ERROR) : null,
'retry' => $projection->retry(),
],
[
@@ -123,8 +130,8 @@ function (Connection $connection) use ($projections): void {
'version' => $projection->id()->version(),
'position' => $projection->position(),
'status' => $projection->status()->value,
- 'error_message' => $projection->projectionError()?->errorMessage,
- 'error_object' => $errorObject,
+ 'error_message' => $projectionError?->errorMessage,
+ 'error_context' => $projectionError?->errorContext ? json_encode($projectionError->errorContext, JSON_THROW_ON_ERROR) : null,
'retry' => $projection->retry(),
],
);
@@ -162,7 +169,7 @@ public function configureSchema(Schema $schema, Connection $connection): void
->setNotnull(true);
$table->addColumn('error_message', Types::STRING)
->setNotnull(false);
- $table->addColumn('error_object', Types::BLOB)
+ $table->addColumn('error_context', Types::JSON)
->setNotnull(false);
$table->addColumn('retry', Types::INTEGER)
->setNotnull(true)
diff --git a/src/Projection/Projection/Store/ErrorContext.php b/src/Projection/Projection/Store/ErrorContext.php
new file mode 100644
index 000000000..af18b3655
--- /dev/null
+++ b/src/Projection/Projection/Store/ErrorContext.php
@@ -0,0 +1,36 @@
+ */
+ public static function fromThrowable(Throwable $error): array
+ {
+ $errors = [];
+
+ do {
+ $errors[] = self::transform($error);
+ $error = $error->getPrevious();
+ } while ($error);
+
+ return $errors;
+ }
+
+ /** @return Context */
+ private static function transform(Throwable $error): array
+ {
+ return [
+ 'message' => $error->getMessage(),
+ 'code' => $error->getCode(),
+ 'file' => $error->getFile(),
+ 'line' => $error->getLine(),
+ 'trace' => $error->getTraceAsString(),
+ ];
+ }
+}
diff --git a/src/Projection/Projection/Store/ErrorSerializer.php b/src/Projection/Projection/Store/ErrorSerializer.php
deleted file mode 100644
index 523af671b..000000000
--- a/src/Projection/Projection/Store/ErrorSerializer.php
+++ /dev/null
@@ -1,41 +0,0 @@
- true]);
-
- if (!$result instanceof Throwable) {
- return null;
- }
-
- return $result;
- }
-}
diff --git a/tests/Unit/Projection/Projection/ProjectionTest.php b/tests/Unit/Projection/Projection/ProjectionTest.php
index 44468a60b..620dd2379 100644
--- a/tests/Unit/Projection/Projection/ProjectionTest.php
+++ b/tests/Unit/Projection/Projection/ProjectionTest.php
@@ -8,6 +8,7 @@
use Patchlevel\EventSourcing\Projection\Projection\ProjectionError;
use Patchlevel\EventSourcing\Projection\Projection\ProjectionId;
use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus;
+use Patchlevel\EventSourcing\Projection\Projection\Store\ErrorContext;
use PHPUnit\Framework\TestCase;
use RuntimeException;
@@ -77,7 +78,13 @@ public function testError(): void
self::assertFalse($projection->isActive());
self::assertTrue($projection->isError());
self::assertFalse($projection->isOutdated());
- self::assertEquals(new ProjectionError('test', $exception), $projection->projectionError());
+ self::assertEquals(
+ new ProjectionError(
+ 'test',
+ ErrorContext::fromThrowable($exception),
+ ),
+ $projection->projectionError(),
+ );
}
public function testOutdated(): void
diff --git a/tests/Unit/Projection/Projection/Store/ErrorContextTest.php b/tests/Unit/Projection/Projection/Store/ErrorContextTest.php
new file mode 100644
index 000000000..2006e8962
--- /dev/null
+++ b/tests/Unit/Projection/Projection/Store/ErrorContextTest.php
@@ -0,0 +1,41 @@
+exception),
+ new ProjectionError('ERROR', ErrorContext::fromThrowable($projector->exception)),
-1,
),
],
@@ -463,7 +464,7 @@ public function handle(Message $message): void
$projectionId,
ProjectionStatus::Error,
0,
- new ProjectionError('ERROR', $projector->exception),
+ new ProjectionError('ERROR', ErrorContext::fromThrowable($projector->exception)),
1,
),
],