Skip to content

Commit

Permalink
Merge pull request #456 from patchlevel/tranform-exception-in-array
Browse files Browse the repository at this point in the history
transfrom projector throwable into an array
  • Loading branch information
DavidBadura authored Jan 6, 2024
2 parents 81b322a + b7d3e1d commit b291567
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 115 deletions.
23 changes: 12 additions & 11 deletions baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
<code><![CDATA[ArrayIterator<int, Projection>]]></code>
</InvalidReturnType>
</file>
<file src="src/Projection/Projection/Store/DoctrineStore.php">
<MixedArgument>
<code>$context</code>
</MixedArgument>
<MixedAssignment>
<code>$context</code>
</MixedAssignment>
<PossiblyNullPropertyFetch>
<code><![CDATA[$projectionError->errorContext]]></code>
<code><![CDATA[$projectionError->errorContext]]></code>
</PossiblyNullPropertyFetch>
</file>
<file src="src/Projection/Projector/InMemoryProjectorRepository.php">
<InvalidOperand>
<code><![CDATA[$this->projectors]]></code>
Expand Down Expand Up @@ -135,12 +147,6 @@
<code>$name</code>
</PropertyNotSetInConstructor>
</file>
<file src="tests/Integration/BankAccountSplitStream/Projection/BankAccountProjection.php">
<MixedMethodCall>
<code>toString</code>
<code>toString</code>
</MixedMethodCall>
</file>
<file src="tests/Integration/BasicImplementation/Aggregate/Profile.php">
<PropertyNotSetInConstructor>
<code>$id</code>
Expand All @@ -153,11 +159,6 @@
<code>$name</code>
</PropertyNotSetInConstructor>
</file>
<file src="tests/Integration/Outbox/Projection/ProfileProjection.php">
<MixedMethodCall>
<code>toString</code>
</MixedMethodCall>
</file>
<file src="tests/Integration/Pipeline/Aggregate/Profile.php">
<PropertyNotSetInConstructor>
<code>$id</code>
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\\<int, array\\{message\\: string, code\\: int\\|string, file\\: string, line\\: int, trace\\: string\\}\\>\\|null, mixed given\\.$#"
count: 1
path: src/Projection/Projection/Store/DoctrineStore.php

-
message: "#^Method Patchlevel\\\\EventSourcing\\\\Projection\\\\Projector\\\\InMemoryProjectorRepository\\:\\:projectors\\(\\) should return array\\<int, object\\> but returns array\\<int\\|string, object\\>\\.$#"
count: 1
Expand Down
19 changes: 15 additions & 4 deletions src/Console/Command/ProjectionStatusCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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']);
}
}
7 changes: 5 additions & 2 deletions src/Projection/Projection/ProjectionError.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Context>|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));
}
}
23 changes: 15 additions & 8 deletions src/Projection/Projection/Store/DoctrineStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@
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,
* version: int,
* position: int,
* status: string,
* error_message: string|null,
* error_object: string|null,
* error_context: string|null,
* retry: int,
* }
*/
Expand Down Expand Up @@ -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'],
);
Expand All @@ -94,16 +101,16 @@ 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(
$this->projectionTable,
[
'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(),
],
[
Expand All @@ -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(),
],
);
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions src/Projection/Projection/Store/ErrorContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Projection\Projection\Store;

use Throwable;

/** @psalm-type Context = array{message: string, code: int|string, file: string, line: int, trace: string} */
final class ErrorContext
{
/** @return list<Context> */
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(),
];
}
}
41 changes: 0 additions & 41 deletions src/Projection/Projection/Store/ErrorSerializer.php

This file was deleted.

9 changes: 8 additions & 1 deletion tests/Unit/Projection/Projection/ProjectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions tests/Unit/Projection/Projection/Store/ErrorContextTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projection\Store;

use Patchlevel\EventSourcing\Projection\Projection\Store\ErrorContext;
use PHPUnit\Framework\TestCase;
use RuntimeException;

/** @covers \Patchlevel\EventSourcing\Projection\Projection\Store\ErrorContext */
final class ErrorContextTest extends TestCase
{
public function testWithoutPrevious(): void
{
$error = new RuntimeException('foo');

$result = ErrorContext::fromThrowable($error);

self::assertCount(1, $result);
self::assertSame('foo', $result[0]['message']);
self::assertSame(0, $result[0]['code']);
self::assertSame(__FILE__, $result[0]['file']);
}

public function testWithPrevious(): void
{
$error = new RuntimeException('foo', 0, new RuntimeException('bar'));

$result = ErrorContext::fromThrowable($error);

self::assertCount(2, $result);
self::assertSame('foo', $result[0]['message']);
self::assertSame(0, $result[0]['code']);
self::assertSame(__FILE__, $result[0]['file']);

self::assertSame('bar', $result[1]['message']);
self::assertSame(0, $result[1]['code']);
self::assertSame(__FILE__, $result[1]['file']);
}
}
46 changes: 0 additions & 46 deletions tests/Unit/Projection/Projection/Store/ErrorSerializerTest.php

This file was deleted.

Loading

0 comments on commit b291567

Please sign in to comment.