@@ -293,10 +293,11 @@ final class DoStuffSubscriber
293293 }
294294}
295295```
296+
296297### Setup and Teardown
297298
298- Subscribers can have one ` setup ` and ` teardown ` method that is executed when the subscription is created or deleted .
299- For this there are the attributes ` Setup ` and ` Teardown ` . The method name itself doesn't matter.
299+ Subscribers can have one ` setup ` method that is executed when the subscription is created.
300+ For this there is the attributes ` Setup ` . The method name itself doesn't matter.
300301This is especially helpful for projectors, as they can create the necessary structures for the projection here.
301302
302303``` php
@@ -319,12 +320,6 @@ final class ProfileProjector
319320 sprintf('CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);', self::TABLE),
320321 );
321322 }
322-
323- #[Teardown]
324- public function drop(): void
325- {
326- $this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE));
327- }
328323}
329324```
330325!!! danger
@@ -344,6 +339,83 @@ final class ProfileProjector
344339 Most databases have a limit on the length of the table/collection name.
345340 The limit is usually 64 characters.
346341
342+ ### Teardown
343+
344+ Subscribers can have one ` teardown ` method that is executed when the subscription is removed.
345+ For this there is the attributes ` Teardown ` .
346+
347+ ``` php
348+ use Doctrine\DBAL\Connection;
349+ use Patchlevel\EventSourcing\Attribute\Projector;
350+ use Patchlevel\EventSourcing\Attribute\Setup;
351+ use Patchlevel\EventSourcing\Attribute\Teardown;
352+
353+ #[Projector(self::TABLE)]
354+ final class ProfileProjector
355+ {
356+ private const TABLE = 'profile_v1';
357+
358+ private Connection $connection;
359+
360+ #[Teardown]
361+ public function drop(): void
362+ {
363+ $this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE));
364+ }
365+ }
366+ ```
367+ !!! danger
368+
369+ MySQL and MariaDB don't support transactions for DDL statements.
370+ So you must use a different database connection in your projectors,
371+ otherwise you will get an error when the subscription tries to create the table.
372+
373+ !!! warning
374+
375+ A teardown can only be performed for a subscription if the subscriber with that subscriber ID still exists.
376+
377+ !!! note
378+
379+ You can not mix the `cleanup` method with the `teardown` method.
380+
381+ ### Cleanup
382+
383+ The cleanup option allows you to delete the subscription from the database after the subscription has finished.
384+ This is especially useful for projectors,
385+
386+ ``` php
387+ use Doctrine\DBAL\Connection;
388+ use Patchlevel\EventSourcing\Attribute\Cleanup;
389+ use Patchlevel\EventSourcing\Attribute\Projector;
390+ use Patchlevel\EventSourcing\Attribute\Setup;
391+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DropIndexTask;
392+
393+ #[Projector(self::TABLE)]
394+ final class ProfileProjector
395+ {
396+ private const TABLE = 'profile_v1';
397+
398+ private Connection $connection;
399+
400+ #[Cleanup]
401+ public function drop(): array
402+ {
403+ return [
404+ new DropIndexTask(self::TABLE)
405+ ];
406+ }
407+ }
408+ ```
409+
410+ !!! note
411+
412+ You can not mix the `cleanup` method with the `teardown` method.
413+
414+ !!! tip
415+
416+ You can create your own cleanup tasks and add them to the array.
417+ For more information, see [Cleanup Handler](#cleanup-handler).
418+
347419### On Failed
348420
349421The subscription engine has a [ retry strategy] ( #retry-strategy ) to retry subscriptions that have an error.
@@ -918,7 +990,63 @@ $retryStrategyRepository = new RetryStrategyRepository([
918990!!! tip
919991
920992 You can change the default retry strategy by define the name in the constructor as second parameter.
993+
994+ ### Cleanup Handler
995+
996+ You can also create your own cleanup tasks.
997+ The subscription engine will call the method ` __invoke ` on the task object.
998+ For example, you can create a task that drops a collection from a MongoDB database.
999+
1000+ First, create a task class, that holds the collection name.
1001+
1002+ ``` php
1003+ final class DropCollection {
1004+ public function __construct(
1005+ public readonly string $collectionName
1006+ ) {}
1007+ }
1008+ ```
1009+
1010+ !!! warning
1011+
1012+ The task class must be serializable. It will be stored in the subscription store.
1013+
1014+ Then create a handler that supports this task.
1015+
1016+ ``` php
1017+ use MongoDb\Database;
1018+ use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupHandler;
1019+
1020+ final class MongodbCleanupHandler implements CleanupHandler {
1021+ public function __construct(
1022+ private readonly Database $database
1023+ ) {}
1024+
1025+ public function __invoke(object $task): void
1026+ {
1027+ if ($task instanceof DropCollection) {
1028+ $this->database->dropCollection($task->collectionName);
1029+ }
1030+ }
9211031
1032+ public function supports(object $task): bool
1033+ {
1034+ return $task instanceof DropCollection;
1035+ }
1036+ }
1037+ ```
1038+
1039+ Last step, you need to add the handler to the ` DefaultCleaner ` , that is used by the subscription engine.
1040+
1041+ ``` php
1042+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
1043+
1044+ $cleaner = new DefaultCleaner([
1045+ new DbalCleanupHandler($projectionConnection),
1046+ new MongodbCleanupHandler($mongodbDatabase),
1047+ ]);
1048+ ```
1049+
9221050### Subscriber Accessor
9231051
9241052The subscriber accessor repository is responsible for providing the subscribers to the subscription engine.
@@ -938,30 +1066,41 @@ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([
9381066 $subscriber3,
9391067]);
9401068```
1069+
9411070### Subscription Engine
9421071
9431072Now we can create the subscription engine and plug together the necessary services.
9441073The message loader is needed to load the messages, the Subscription Store to store the subscription state
9451074and we need the subscriber accessor repository. Optionally, we can also pass a retry strategy.
1075+ Finally, if we want to use the cleanup feature, we need to pass the cleanup handlers.
9461076
9471077``` php
1078+ use Doctrine\DBAL\Connection;
9481079use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
9491080use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
9501081use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
9511082use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore;
9521083use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository;
1084+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupHandler;
1085+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
9531086
9541087/**
9551088 * @var MessageLoader $messageLoader
9561089 * @var DoctrineSubscriptionStore $subscriptionStore
9571090 * @var MetadataSubscriberAccessorRepository $subscriberAccessorRepository
9581091 * @var RetryStrategyRepository $retryStrategyRepository
1092+ * @var LoggerInterface $logger
1093+ * @var Connection $projectionConnection
9591094 */
9601095$subscriptionEngine = new DefaultSubscriptionEngine(
9611096 $messageLoader,
9621097 $subscriptionStore,
9631098 $subscriberAccessorRepository,
9641099 $retryStrategyRepository, // optional, if not set the default retry strategy is used
1100+ $logger, // optional
1101+ new DefaultCleaner([
1102+ new DbalCleanupHandler($projectionConnection)
1103+ ]), // required, if you want to use the cleanup feature
9651104);
9661105```
9671106### Catch up Subscription Engine
0 commit comments