diff --git a/Makefile b/Makefile
index 07f36723..1fc5334d 100644
--- a/Makefile
+++ b/Makefile
@@ -30,8 +30,15 @@ psalm-baseline: vendor
vendor/bin/psalm --update-baseline --set-baseline=baseline.xml
.PHONY: phpunit
-phpunit: vendor ## run phpunit tests
- XDEBUG_MODE=coverage vendor/bin/phpunit
+phpunit: vendor phpunit-unit phpunit-integration ## run phpunit tests
+
+.PHONY: phpunit-integration
+phpunit-integration: vendor ## run phpunit integration tests
+ vendor/bin/phpunit --testsuite=integration
+
+.PHONY: phpunit-unit
+phpunit-unit: vendor ## run phpunit unit tests
+ XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit
.PHONY: infection
infection: vendor ## run infection
diff --git a/baseline.xml b/baseline.xml
index 2a1ccf71..e107ec3e 100644
--- a/baseline.xml
+++ b/baseline.xml
@@ -56,12 +56,16 @@
errorContext]]>
-
+
+
+ projector->$method(...)]]>
+
+
diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md
index ca32cfb0..9b8c53ea 100644
--- a/docs/pages/getting_started.md
+++ b/docs/pages/getting_started.md
@@ -281,6 +281,7 @@ use Doctrine\DBAL\DriverManager;
use Patchlevel\EventSourcing\EventBus\DefaultEventBus;
use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore;
use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
use Patchlevel\EventSourcing\Store\DoctrineDbalStore;
@@ -306,7 +307,7 @@ $eventStore = new DoctrineDbalStore(
$hotelProjector = new HotelProjector($projectionConnection);
-$projectorRepository = new ProjectorRepository([
+$projectorRepository = new MetadataProjectorAccessorRepository([
$hotelProjector,
]);
diff --git a/docs/pages/projection.md b/docs/pages/projection.md
index ecfe7916..a57b3246 100644
--- a/docs/pages/projection.md
+++ b/docs/pages/projection.md
@@ -469,6 +469,17 @@ $retryStrategy = new ClockBasedRetryStrategy(
You can reactivate the projection manually or remove it and rebuild it from scratch.
+### Projector Accessor
+
+The projector accessor is responsible for providing the projectors to the projectionist.
+We provide a metadata projector accessor repository by default.
+
+```php
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
+
+$projectorAccessorRepository = new MetadataProjectorAccessorRepository([$projector1, $projector2, $projector3]);
+```
+
### Projectionist
Now we can create the projectionist and plug together the necessary services.
@@ -481,7 +492,7 @@ use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
$projectionist = new DefaultProjectionist(
$eventStore,
$projectionStore,
- [$projector1, $projector2, $projector3],
+ $projectorAccessorRepository,
$retryStrategy,
);
```
diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php
index 6e566fda..0f3e905c 100644
--- a/src/Projection/Projectionist/DefaultProjectionist.php
+++ b/src/Projection/Projectionist/DefaultProjectionist.php
@@ -5,17 +5,15 @@
namespace Patchlevel\EventSourcing\Projection\Projectionist;
use Closure;
-use Patchlevel\EventSourcing\Attribute\Subscribe;
use Patchlevel\EventSourcing\EventBus\Message;
-use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory;
-use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadata;
-use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadataFactory;
use Patchlevel\EventSourcing\Projection\Projection\Projection;
use Patchlevel\EventSourcing\Projection\Projection\ProjectionCriteria;
use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus;
use Patchlevel\EventSourcing\Projection\Projection\RunMode;
use Patchlevel\EventSourcing\Projection\Projection\Store\LockableProjectionStore;
use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore;
+use Patchlevel\EventSourcing\Projection\Projector\ProjectorAccessor;
+use Patchlevel\EventSourcing\Projection\Projector\ProjectorAccessorRepository;
use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy;
use Patchlevel\EventSourcing\Projection\RetryStrategy\RetryStrategy;
use Patchlevel\EventSourcing\Store\Criteria;
@@ -23,24 +21,17 @@
use Psr\Log\LoggerInterface;
use Throwable;
-use function array_map;
-use function array_merge;
use function count;
use function in_array;
use function sprintf;
final class DefaultProjectionist implements Projectionist
{
- /** @var array|null */
- private array|null $projectorIndex = null;
-
- /** @param iterable $projectors */
public function __construct(
private readonly Store $messageStore,
private readonly ProjectionStore $projectionStore,
- private readonly iterable $projectors,
+ private readonly ProjectorAccessorRepository $projectorRepository,
private readonly RetryStrategy $retryStrategy = new ClockBasedRetryStrategy(),
- private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(),
private readonly LoggerInterface|null $logger = null,
) {
}
@@ -323,7 +314,7 @@ function (array $projections): void {
continue;
}
- $teardownMethod = $this->resolveTeardownMethod($projector);
+ $teardownMethod = $projector->teardownMethod();
if (!$teardownMethod) {
$this->projectionStore->remove($projection);
@@ -402,7 +393,7 @@ function (array $projections): void {
continue;
}
- $teardownMethod = $this->resolveTeardownMethod($projector);
+ $teardownMethod = $projector->teardownMethod();
if (!$teardownMethod) {
$this->projectionStore->remove($projection);
@@ -561,7 +552,7 @@ private function handleMessage(int $index, Message $message, Projection $project
throw ProjectorNotFound::forProjectionId($projection->id());
}
- $subscribeMethods = $this->resolveSubscribeMethods($projector, $message);
+ $subscribeMethods = $projector->subscribeMethods($message->event()::class);
if ($subscribeMethods === []) {
$projection->changePosition($index);
@@ -611,19 +602,9 @@ private function handleMessage(int $index, Message $message, Projection $project
);
}
- private function projector(string $projectionId): object|null
+ private function projector(string $projectionId): ProjectorAccessor|null
{
- if ($this->projectorIndex === null) {
- $this->projectorIndex = [];
-
- foreach ($this->projectors as $projector) {
- $projectorId = $this->projectorMetadata($projector)->id;
-
- $this->projectorIndex[$projectorId] = $projector;
- }
- }
-
- return $this->projectorIndex[$projectionId] ?? null;
+ return $this->projectorRepository->get($projectionId);
}
private function handleOutdatedProjections(ProjectionistCriteria $criteria): void
@@ -764,7 +745,7 @@ function (array $projections): void {
throw ProjectorNotFound::forProjectionId($projection->id());
}
- $setupMethod = $this->resolveSetupMethod($projector);
+ $setupMethod = $projector->setupMethod();
if (!$setupMethod) {
$projection->booting();
@@ -810,25 +791,25 @@ private function discoverNewProjections(): void
$this->findForUpdate(
new ProjectionCriteria(),
function (array $projections): void {
- foreach ($this->projectors as $projector) {
- $metadata = $this->projectorMetadata($projector);
-
+ foreach ($this->projectorRepository->all() as $projector) {
foreach ($projections as $projection) {
- if ($projection->id() === $metadata->id) {
+ if ($projection->id() === $projector->id()) {
continue 2;
}
}
- $this->projectionStore->add(new Projection(
- $metadata->id,
- $metadata->group,
- $metadata->runMode,
- ));
+ $this->projectionStore->add(
+ new Projection(
+ $projector->id(),
+ $projector->group(),
+ $projector->runMode(),
+ ),
+ );
$this->logger?->info(
sprintf(
'Projectionist: New Projector "%s" was found and added to the projection store.',
- $metadata->id,
+ $projector->id(),
),
);
}
@@ -836,52 +817,6 @@ function (array $projections): void {
);
}
- private function resolveSetupMethod(object $projector): Closure|null
- {
- $metadata = $this->metadataFactory->metadata($projector::class);
- $method = $metadata->setupMethod;
-
- if ($method === null) {
- return null;
- }
-
- return $projector->$method(...);
- }
-
- private function resolveTeardownMethod(object $projector): Closure|null
- {
- $metadata = $this->metadataFactory->metadata($projector::class);
- $method = $metadata->teardownMethod;
-
- if ($method === null) {
- return null;
- }
-
- return $projector->$method(...);
- }
-
- /** @return iterable */
- private function resolveSubscribeMethods(object $projector, Message $message): iterable
- {
- $event = $message->event();
- $metadata = $this->metadataFactory->metadata($projector::class);
-
- $methods = array_merge(
- $metadata->subscribeMethods[$event::class] ?? [],
- $metadata->subscribeMethods[Subscribe::ALL] ?? [],
- );
-
- return array_map(
- static fn (string $method) => $projector->$method(...),
- $methods,
- );
- }
-
- private function projectorMetadata(object $projector): ProjectorMetadata
- {
- return $this->metadataFactory->metadata($projector::class);
- }
-
private function latestIndex(): int
{
$stream = $this->messageStore->load(null, 1, null, true);
diff --git a/src/Projection/Projector/MetadataProjectorAccessor.php b/src/Projection/Projector/MetadataProjectorAccessor.php
new file mode 100644
index 00000000..543c406f
--- /dev/null
+++ b/src/Projection/Projector/MetadataProjectorAccessor.php
@@ -0,0 +1,89 @@
+> */
+ private array $subscribeCache = [];
+
+ public function __construct(
+ private readonly object $projector,
+ private readonly ProjectorMetadata $metadata,
+ ) {
+ }
+
+ public function id(): string
+ {
+ return $this->metadata->id;
+ }
+
+ public function group(): string
+ {
+ return $this->metadata->group;
+ }
+
+ public function runMode(): RunMode
+ {
+ return $this->metadata->runMode;
+ }
+
+ public function setupMethod(): Closure|null
+ {
+ $method = $this->metadata->setupMethod;
+
+ if ($method === null) {
+ return null;
+ }
+
+ return $this->projector->$method(...);
+ }
+
+ public function teardownMethod(): Closure|null
+ {
+ $method = $this->metadata->teardownMethod;
+
+ if ($method === null) {
+ return null;
+ }
+
+ return $this->projector->$method(...);
+ }
+
+ /**
+ * @param class-string $eventClass
+ *
+ * @return list
+ */
+ public function subscribeMethods(string $eventClass): array
+ {
+ if (array_key_exists($eventClass, $this->subscribeCache)) {
+ return $this->subscribeCache[$eventClass];
+ }
+
+ $methods = array_merge(
+ $this->metadata->subscribeMethods[$eventClass] ?? [],
+ $this->metadata->subscribeMethods[Subscribe::ALL] ?? [],
+ );
+
+ $this->subscribeCache[$eventClass] = array_map(
+ /** @return Closure(Message):void */
+ fn (string $method) => $this->projector->$method(...),
+ $methods,
+ );
+
+ return $this->subscribeCache[$eventClass];
+ }
+}
diff --git a/src/Projection/Projector/MetadataProjectorAccessorRepository.php b/src/Projection/Projector/MetadataProjectorAccessorRepository.php
new file mode 100644
index 00000000..81c4714a
--- /dev/null
+++ b/src/Projection/Projector/MetadataProjectorAccessorRepository.php
@@ -0,0 +1,51 @@
+ */
+ private array $projectorsMap = [];
+
+ /** @param iterable $projectors */
+ public function __construct(
+ private readonly iterable $projectors,
+ private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(),
+ ) {
+ }
+
+ /** @return iterable */
+ public function all(): iterable
+ {
+ return array_values($this->projectorAccessorMap());
+ }
+
+ public function get(string $id): ProjectorAccessor|null
+ {
+ $map = $this->projectorAccessorMap();
+
+ return $map[$id] ?? null;
+ }
+
+ /** @return array */
+ private function projectorAccessorMap(): array
+ {
+ if ($this->projectorsMap !== []) {
+ return $this->projectorsMap;
+ }
+
+ foreach ($this->projectors as $projector) {
+ $metadata = $this->metadataFactory->metadata($projector::class);
+ $this->projectorsMap[$metadata->id] = new MetadataProjectorAccessor($projector, $metadata);
+ }
+
+ return $this->projectorsMap;
+ }
+}
diff --git a/src/Projection/Projector/ProjectorAccessor.php b/src/Projection/Projector/ProjectorAccessor.php
new file mode 100644
index 00000000..00e378da
--- /dev/null
+++ b/src/Projection/Projector/ProjectorAccessor.php
@@ -0,0 +1,29 @@
+
+ */
+ public function subscribeMethods(string $eventClass): array;
+}
diff --git a/src/Projection/Projector/ProjectorAccessorRepository.php b/src/Projection/Projector/ProjectorAccessorRepository.php
new file mode 100644
index 00000000..267a97e5
--- /dev/null
+++ b/src/Projection/Projector/ProjectorAccessorRepository.php
@@ -0,0 +1,13 @@
+ */
+ public function all(): iterable;
+
+ public function get(string $id): ProjectorAccessor|null;
+}
diff --git a/src/Projection/Projector/TraceableProjectorAccessor.php b/src/Projection/Projector/TraceableProjectorAccessor.php
new file mode 100644
index 00000000..1ba70b22
--- /dev/null
+++ b/src/Projection/Projector/TraceableProjectorAccessor.php
@@ -0,0 +1,79 @@
+parent->id();
+ }
+
+ public function group(): string
+ {
+ return $this->parent->group();
+ }
+
+ public function runMode(): RunMode
+ {
+ return $this->parent->runMode();
+ }
+
+ public function setupMethod(): Closure|null
+ {
+ return $this->parent->setupMethod();
+ }
+
+ public function teardownMethod(): Closure|null
+ {
+ return $this->parent->teardownMethod();
+ }
+
+ /**
+ * @param class-string $eventClass
+ *
+ * @return list
+ */
+ public function subscribeMethods(string $eventClass): array
+ {
+ return array_map(
+ /**
+ * @param Closure(Message):void $closure
+ *
+ * @return Closure(Message):void
+ */
+ fn (Closure $closure) => function (Message $message) use ($closure): void {
+ $trace = new Trace(
+ $this->id(),
+ 'event_sourcing/projector/' . $this->group(),
+ );
+
+ $this->traceStack->add($trace);
+
+ try {
+ $closure($message);
+ } finally {
+ $this->traceStack->remove($trace);
+ }
+ },
+ $this->parent->subscribeMethods($eventClass),
+ );
+ }
+}
diff --git a/src/Projection/Projector/TraceableProjectorAccessorRepository.php b/src/Projection/Projector/TraceableProjectorAccessorRepository.php
new file mode 100644
index 00000000..cb5beb78
--- /dev/null
+++ b/src/Projection/Projector/TraceableProjectorAccessorRepository.php
@@ -0,0 +1,52 @@
+ */
+ private array $projectorsMap = [];
+
+ public function __construct(
+ private readonly ProjectorAccessorRepository $parent,
+ private readonly TraceStack $traceStack,
+ ) {
+ }
+
+ /** @return iterable */
+ public function all(): iterable
+ {
+ return array_values($this->projectorAccessorMap());
+ }
+
+ public function get(string $id): TraceableProjectorAccessor|null
+ {
+ $map = $this->projectorAccessorMap();
+
+ return $map[$id] ?? null;
+ }
+
+ /** @return array */
+ private function projectorAccessorMap(): array
+ {
+ if ($this->projectorsMap !== []) {
+ return $this->projectorsMap;
+ }
+
+ foreach ($this->parent->all() as $projectorAccessor) {
+ $this->projectorsMap[$projectorAccessor->id()] = new TraceableProjectorAccessor(
+ $projectorAccessor,
+ $this->traceStack,
+ );
+ }
+
+ return $this->projectorsMap;
+ }
+}
diff --git a/src/Repository/MessageDecorator/Trace.php b/src/Repository/MessageDecorator/Trace.php
new file mode 100644
index 00000000..bcc2357b
--- /dev/null
+++ b/src/Repository/MessageDecorator/Trace.php
@@ -0,0 +1,15 @@
+traceStack->get();
+
+ if ($traces === []) {
+ return $message;
+ }
+
+ return $message->withHeader(new TraceHeader(
+ array_map(
+ static fn (Trace $trace) => [
+ 'name' => $trace->name,
+ 'category' => $trace->category,
+ ],
+ $traces,
+ ),
+ ));
+ }
+}
diff --git a/src/Repository/MessageDecorator/TraceHeader.php b/src/Repository/MessageDecorator/TraceHeader.php
new file mode 100644
index 00000000..c7c10d23
--- /dev/null
+++ b/src/Repository/MessageDecorator/TraceHeader.php
@@ -0,0 +1,18 @@
+ $traces */
+ public function __construct(
+ public readonly array $traces,
+ ) {
+ }
+}
diff --git a/src/Repository/MessageDecorator/TraceStack.php b/src/Repository/MessageDecorator/TraceStack.php
new file mode 100644
index 00000000..24b71b9e
--- /dev/null
+++ b/src/Repository/MessageDecorator/TraceStack.php
@@ -0,0 +1,35 @@
+ */
+ private array $traces = [];
+
+ public function add(Trace $trace): void
+ {
+ $this->traces[self::key($trace)] = $trace;
+ }
+
+ /** @return list */
+ public function get(): array
+ {
+ return array_values($this->traces);
+ }
+
+ public function remove(Trace $trace): void
+ {
+ unset($this->traces[self::key($trace)]);
+ }
+
+ private static function key(Trace $trace): string
+ {
+ return $trace->category . '#' . $trace->name;
+ }
+}
diff --git a/tests/Benchmark/ProjectionistBench.php b/tests/Benchmark/ProjectionistBench.php
index fd5050e4..fa8ff604 100644
--- a/tests/Benchmark/ProjectionistBench.php
+++ b/tests/Benchmark/ProjectionistBench.php
@@ -11,6 +11,7 @@
use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore;
use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
use Patchlevel\EventSourcing\Projection\Projectionist\Projectionist;
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
use Patchlevel\EventSourcing\Repository\DefaultRepository;
use Patchlevel\EventSourcing\Repository\Repository;
use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator;
@@ -83,10 +84,12 @@ public function setUp(): void
$this->projectionist = new DefaultProjectionist(
$this->store,
$projectionStore,
- [
- new ProfileProjector($connection),
- new SendEmailProcessor(),
- ],
+ new MetadataProjectorAccessorRepository(
+ [
+ new ProfileProjector($connection),
+ new SendEmailProcessor(),
+ ],
+ ),
);
}
diff --git a/tests/Integration/BankAccountSplitStream/IntegrationTest.php b/tests/Integration/BankAccountSplitStream/IntegrationTest.php
index 7fff903b..9df66124 100644
--- a/tests/Integration/BankAccountSplitStream/IntegrationTest.php
+++ b/tests/Integration/BankAccountSplitStream/IntegrationTest.php
@@ -12,6 +12,7 @@
use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore;
use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria;
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
use Patchlevel\EventSourcing\Repository\MessageDecorator\ChainMessageDecorator;
use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator;
@@ -60,7 +61,7 @@ public function testSuccessful(): void
$projectionist = new DefaultProjectionist(
$store,
new InMemoryStore(),
- [$bankAccountProjector],
+ new MetadataProjectorAccessorRepository([$bankAccountProjector]),
);
$eventBus = DefaultEventBus::create([$bankAccountProjector]);
diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php
index 0b095150..f8f3fe47 100644
--- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php
+++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php
@@ -11,6 +11,7 @@
use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore;
use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria;
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector;
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
@@ -57,7 +58,7 @@ public function testSuccessful(): void
$projectionist = new DefaultProjectionist(
$store,
new InMemoryStore(),
- [$profileProjector],
+ new MetadataProjectorAccessorRepository([$profileProjector]),
);
$eventBus = DefaultEventBus::create([
@@ -125,7 +126,7 @@ public function testSnapshot(): void
$projectionist = new DefaultProjectionist(
$store,
new InMemoryStore(),
- [$profileProjection],
+ new MetadataProjectorAccessorRepository([$profileProjection]),
);
$eventBus = DefaultEventBus::create([
diff --git a/tests/Integration/Projectionist/Aggregate/Profile.php b/tests/Integration/Projectionist/Aggregate/Profile.php
index 0d827bad..64a4378a 100644
--- a/tests/Integration/Projectionist/Aggregate/Profile.php
+++ b/tests/Integration/Projectionist/Aggregate/Profile.php
@@ -9,6 +9,7 @@
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
+use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\NameChanged;
use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated;
use Patchlevel\EventSourcing\Tests\Integration\Projectionist\ProfileId;
@@ -28,13 +29,24 @@ public static function create(ProfileId $id, string $name): self
return $self;
}
- #[Apply(ProfileCreated::class)]
+ public function changeName(string $name): void
+ {
+ $this->recordThat(new NameChanged($name));
+ }
+
+ #[Apply]
protected function applyProfileCreated(ProfileCreated $event): void
{
$this->id = $event->profileId;
$this->name = $event->name;
}
+ #[Apply]
+ protected function applyNameChanged(NameChanged $event): void
+ {
+ $this->name = $event->name;
+ }
+
public function name(): string
{
return $this->name;
diff --git a/tests/Integration/Projectionist/Events/NameChanged.php b/tests/Integration/Projectionist/Events/NameChanged.php
new file mode 100644
index 00000000..3ffd6e5d
--- /dev/null
+++ b/tests/Integration/Projectionist/Events/NameChanged.php
@@ -0,0 +1,16 @@
+event();
+
+ assert($profileCreated instanceof ProfileCreated);
+
+ $repository = $this->repositoryManager->get(Profile::class);
+
+ $profile = $repository->load($profileCreated->profileId);
+
+ $profile->changeName('new name');
+
+ $repository->save($profile);
+ }
+}
diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php
index a80ac1e1..0b5ca0c5 100644
--- a/tests/Integration/Projectionist/ProjectionistTest.php
+++ b/tests/Integration/Projectionist/ProjectionistTest.php
@@ -8,6 +8,7 @@
use Doctrine\DBAL\Connection;
use Patchlevel\EventSourcing\Clock\FrozenClock;
use Patchlevel\EventSourcing\EventBus\DefaultEventBus;
+use Patchlevel\EventSourcing\EventBus\Message;
use Patchlevel\EventSourcing\EventBus\Serializer\DefaultHeadersSerializer;
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Projection\Projection\Projection;
@@ -16,8 +17,13 @@
use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore;
use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria;
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
+use Patchlevel\EventSourcing\Projection\Projector\TraceableProjectorAccessorRepository;
use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy;
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
+use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceDecorator;
+use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceHeader;
+use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceStack;
use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator;
use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector;
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
@@ -25,9 +31,12 @@
use Patchlevel\EventSourcing\Tests\DbalManager;
use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Aggregate\Profile;
use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ErrorProducerProjector;
+use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProcessor;
use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProjector;
use PHPUnit\Framework\TestCase;
+use function iterator_to_array;
+
/** @coversNothing */
final class ProjectionistTest extends TestCase
{
@@ -86,7 +95,7 @@ public function testHappyPath(): void
$projectionist = new DefaultProjectionist(
$store,
$projectionStore,
- [new ProfileProjector($this->projectionConnection)],
+ new MetadataProjectorAccessorRepository([new ProfileProjector($this->projectionConnection)]),
);
self::assertEquals(
@@ -128,7 +137,10 @@ public function testHappyPath(): void
$projectionist->projections(),
);
- $result = $this->projectionConnection->fetchAssociative('SELECT * FROM projection_profile_1 WHERE id = ?', ['1']);
+ $result = $this->projectionConnection->fetchAssociative(
+ 'SELECT * FROM projection_profile_1 WHERE id = ?',
+ ['1'],
+ );
self::assertIsArray($result);
self::assertArrayHasKey('id', $result);
@@ -195,7 +207,7 @@ public function testErrorHandling(): void
$projectionist = new DefaultProjectionist(
$store,
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
new ClockBasedRetryStrategy(
$clock,
ClockBasedRetryStrategy::DEFAULT_BASE_DELAY,
@@ -289,6 +301,111 @@ public function testErrorHandling(): void
self::assertEquals(0, $projection->retryAttempt());
}
+ public function testProcessor(): void
+ {
+ $store = new DoctrineDbalStore(
+ $this->connection,
+ DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']),
+ DefaultHeadersSerializer::createFromPaths([
+ __DIR__ . '/../../../src',
+ __DIR__,
+ ]),
+ 'eventstore',
+ );
+
+ $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00'));
+
+ $projectionStore = new DoctrineStore(
+ $this->connection,
+ $clock,
+ );
+
+ $traceStack = new TraceStack();
+
+ $manager = new DefaultRepositoryManager(
+ new AggregateRootRegistry(['profile' => Profile::class]),
+ $store,
+ DefaultEventBus::create(),
+ null,
+ new TraceDecorator($traceStack),
+ );
+
+ $projectorAccessorRepository = new TraceableProjectorAccessorRepository(
+ new MetadataProjectorAccessorRepository([new ProfileProcessor($manager)]),
+ $traceStack,
+ );
+
+ $repository = $manager->get(Profile::class);
+
+ $schemaDirector = new DoctrineSchemaDirector(
+ $this->connection,
+ new ChainDoctrineSchemaConfigurator([
+ $store,
+ $projectionStore,
+ ]),
+ );
+
+ $schemaDirector->create();
+
+ $projectionist = new DefaultProjectionist(
+ $store,
+ $projectionStore,
+ $projectorAccessorRepository,
+ );
+
+ self::assertEquals(
+ [new Projection('profile', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))],
+ $projectionist->projections(),
+ );
+
+ $projectionist->boot();
+
+ self::assertEquals(
+ [
+ new Projection(
+ 'profile',
+ Projection::DEFAULT_GROUP,
+ RunMode::FromBeginning,
+ ProjectionStatus::Active,
+ lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'),
+ ),
+ ],
+ $projectionist->projections(),
+ );
+
+ $profile = Profile::create(ProfileId::fromString('1'), 'John');
+ $repository->save($profile);
+
+ $projectionist->run();
+
+ $projections = $projectionist->projections();
+
+ self::assertCount(1, $projections);
+ self::assertArrayHasKey(0, $projections);
+
+ $projection = $projections[0];
+
+ self::assertEquals('profile', $projection->id());
+
+ self::assertEquals(ProjectionStatus::Active, $projection->status());
+
+ /** @var list $messages */
+ $messages = iterator_to_array($store->load());
+
+ self::assertCount(2, $messages);
+ self::assertArrayHasKey(1, $messages);
+
+ self::assertEquals(
+ new TraceHeader([
+ [
+ 'name' => 'profile',
+ 'category' => 'event_sourcing/projector/default',
+ ],
+ ]),
+ $messages[1]->header(TraceHeader::class),
+ );
+ }
+
/** @param list $projections */
private static function findProjection(array $projections, string $id): Projection
{
diff --git a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php
index 33506502..b87f83e0 100644
--- a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php
+++ b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php
@@ -21,6 +21,7 @@
use Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer;
use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist;
use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria;
+use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository;
use Patchlevel\EventSourcing\Projection\RetryStrategy\RetryStrategy;
use Patchlevel\EventSourcing\Store\ArrayStream;
use Patchlevel\EventSourcing\Store\Criteria;
@@ -49,7 +50,7 @@ public function testNothingToBoot(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$store,
- [],
+ new MetadataProjectorAccessorRepository([]),
);
$projectionist->boot();
@@ -73,7 +74,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -122,7 +123,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -182,7 +183,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -254,7 +255,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot(new ProjectionistCriteria(), 1);
@@ -338,7 +339,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector1, $projector2],
+ new MetadataProjectorAccessorRepository([$projector1, $projector2]),
);
$projectionist->boot();
@@ -405,7 +406,7 @@ public function create(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -462,7 +463,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -518,7 +519,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -567,7 +568,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -616,7 +617,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->boot();
@@ -654,7 +655,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->run();
@@ -700,7 +701,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->run();
@@ -753,7 +754,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->run(new ProjectionistCriteria(), 1);
@@ -821,7 +822,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector1, $projector2],
+ new MetadataProjectorAccessorRepository([$projector1, $projector2]),
);
$projectionist->run();
@@ -881,7 +882,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->run();
@@ -924,7 +925,7 @@ public function testRunningMarkOutdated(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [],
+ new MetadataProjectorAccessorRepository([]),
);
$projectionist->run();
@@ -959,7 +960,7 @@ public function testRunningWithoutActiveProjectors(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [],
+ new MetadataProjectorAccessorRepository([]),
);
$projectionist->run();
@@ -1000,7 +1001,7 @@ public function handle(Message $message): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->run();
@@ -1031,7 +1032,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->teardown();
@@ -1067,7 +1068,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->teardown();
@@ -1105,7 +1106,7 @@ public function drop(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->teardown();
@@ -1144,7 +1145,7 @@ public function drop(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->teardown();
@@ -1171,7 +1172,7 @@ public function testTeardownWithoutProjector(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [],
+ new MetadataProjectorAccessorRepository([]),
);
$projectionist->teardown();
@@ -1193,7 +1194,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->remove();
@@ -1235,7 +1236,7 @@ public function drop(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->remove();
@@ -1265,7 +1266,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->remove();
@@ -1301,7 +1302,7 @@ public function drop(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->remove();
@@ -1327,7 +1328,7 @@ public function testRemoveWithoutProjector(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [],
+ new MetadataProjectorAccessorRepository([]),
);
$projectionist->remove();
@@ -1349,7 +1350,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->reactivate();
@@ -1387,7 +1388,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->reactivate();
@@ -1424,7 +1425,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->reactivate();
@@ -1460,7 +1461,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->reactivate();
@@ -1496,7 +1497,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->reactivate();
@@ -1524,7 +1525,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->pause();
@@ -1560,7 +1561,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->pause();
@@ -1596,7 +1597,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->pause();
@@ -1634,7 +1635,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->pause();
@@ -1664,7 +1665,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projections = $projectionist->projections();
@@ -1722,7 +1723,7 @@ public function subscribe(): void
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
$retryStrategy->reveal(),
);
@@ -1774,7 +1775,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore,
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
$retryStrategy->reveal(),
);
@@ -1809,7 +1810,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore->reveal(),
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionistCriteria = new ProjectionistCriteria(
@@ -1849,7 +1850,7 @@ class {
$projectionist = new DefaultProjectionist(
$streamableStore->reveal(),
$projectionStore->reveal(),
- [$projector],
+ new MetadataProjectorAccessorRepository([$projector]),
);
$projectionist->{$method}();
diff --git a/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php b/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php
new file mode 100644
index 00000000..48615474
--- /dev/null
+++ b/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php
@@ -0,0 +1,45 @@
+all());
+ self::assertNull($repository->get('foo'));
+ }
+
+ public function testWithProjector(): void
+ {
+ $projector = new #[Projector('foo')]
+ class {
+ };
+ $metadataFactory = new AttributeProjectorMetadataFactory();
+
+ $repository = new MetadataProjectorAccessorRepository(
+ [$projector],
+ $metadataFactory,
+ );
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ $metadataFactory->metadata($projector::class),
+ );
+
+ self::assertEquals([$accessor], $repository->all());
+ self::assertEquals($accessor, $repository->get('foo'));
+ }
+}
diff --git a/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php b/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php
new file mode 100644
index 00000000..9b5d801e
--- /dev/null
+++ b/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php
@@ -0,0 +1,206 @@
+metadata($projector::class),
+ );
+
+ self::assertEquals('profile', $accessor->id());
+ }
+
+ public function testGroup(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ self::assertEquals('default', $accessor->group());
+ }
+
+ public function testRunMode(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ self::assertEquals(RunMode::FromBeginning, $accessor->runMode());
+ }
+
+ public function testSubscribeMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ #[Subscribe(ProfileCreated::class)]
+ public function onProfileCreated(Message $message): void
+ {
+ }
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->subscribeMethods(ProfileCreated::class);
+
+ self::assertEquals([
+ $projector->onProfileCreated(...),
+ ], $result);
+ }
+
+ public function testMultipleSubscribeMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ #[Subscribe(ProfileCreated::class)]
+ public function onProfileCreated(Message $message): void
+ {
+ }
+
+ #[Subscribe(ProfileCreated::class)]
+ public function onFoo(Message $message): void
+ {
+ }
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->subscribeMethods(ProfileCreated::class);
+
+ self::assertEquals([
+ $projector->onProfileCreated(...),
+ $projector->onFoo(...),
+ ], $result);
+ }
+
+ public function testSubscribeAllMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ #[Subscribe('*')]
+ public function onProfileCreated(Message $message): void
+ {
+ }
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->subscribeMethods(ProfileCreated::class);
+
+ self::assertEquals([
+ $projector->onProfileCreated(...),
+ ], $result);
+ }
+
+ public function testSetupMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ #[Setup]
+ public function method(): void
+ {
+ }
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->setupMethod();
+
+ self::assertEquals($projector->method(...), $result);
+ }
+
+ public function testNotSetupMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->setupMethod();
+
+ self::assertNull($result);
+ }
+
+ public function testTeardownMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ #[Teardown]
+ public function method(): void
+ {
+ }
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->teardownMethod();
+
+ self::assertEquals($projector->method(...), $result);
+ }
+
+ public function testNotTeardownMethod(): void
+ {
+ $projector = new #[Projector('profile')]
+ class {
+ };
+
+ $accessor = new MetadataProjectorAccessor(
+ $projector,
+ (new AttributeProjectorMetadataFactory())->metadata($projector::class),
+ );
+
+ $result = $accessor->teardownMethod();
+
+ self::assertNull($result);
+ }
+}
diff --git a/tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php b/tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php
new file mode 100644
index 00000000..4434d586
--- /dev/null
+++ b/tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php
@@ -0,0 +1,59 @@
+expectException(HeaderNotFound::class);
+
+ $stack = new TraceStack();
+ $decorator = new TraceDecorator($stack);
+
+ $message = new Message(new stdClass());
+
+ $decoratedMessage = $decorator($message);
+
+ self::assertEquals($message, $decoratedMessage);
+
+ $decoratedMessage->header(TraceHeader::class);
+ }
+
+ public function testWithTrace(): void
+ {
+ $stack = new TraceStack();
+ $stack->add(new Trace('name', 'category'));
+
+ $decorator = new TraceDecorator($stack);
+
+ $message = new Message(new stdClass());
+
+ $decoratedMessage = $decorator($message);
+
+ self::assertEquals(
+ new TraceHeader([
+ [
+ 'name' => 'name',
+ 'category' => 'category',
+ ],
+ ]),
+ $decoratedMessage->header(TraceHeader::class),
+ );
+ }
+}
diff --git a/tests/Unit/Repository/MessageDecorator/TraceStackTest.php b/tests/Unit/Repository/MessageDecorator/TraceStackTest.php
new file mode 100644
index 00000000..f348e353
--- /dev/null
+++ b/tests/Unit/Repository/MessageDecorator/TraceStackTest.php
@@ -0,0 +1,33 @@
+get());
+
+ $trace = new Trace('name', 'category');
+
+ $stack->add($trace);
+
+ self::assertEquals([$trace], $stack->get());
+
+ $stack->remove($trace);
+
+ self::assertEquals([], $stack->get());
+ }
+}