@@ -301,8 +301,8 @@ This can be useful for providing direct access to custom headers or other data.
301301
302302### Setup and Teardown
303303
304- Subscribers can have one ` setup ` and ` teardown ` method that is executed when the subscription is created or deleted .
305- For this there are the attributes ` Setup ` and ` Teardown ` . The method name itself doesn't matter.
304+ Subscribers can have one ` setup ` method that is executed when the subscription is created.
305+ For this there is the attributes ` Setup ` . The method name itself doesn't matter.
306306This is especially helpful for projectors, as they can create the necessary structures for the projection here.
307307
308308``` php
@@ -325,12 +325,6 @@ final class ProfileProjector
325325 sprintf('CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);', self::TABLE),
326326 );
327327 }
328-
329- #[Teardown]
330- public function drop(): void
331- {
332- $this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE));
333- }
334328}
335329```
336330!!! danger
@@ -350,6 +344,83 @@ final class ProfileProjector
350344 Most databases have a limit on the length of the table/collection name.
351345 The limit is usually 64 characters.
352346
347+ ### Teardown
348+
349+ Subscribers can have one ` teardown ` method that is executed when the subscription is removed.
350+ For this there is the attributes ` Teardown ` .
351+
352+ ``` php
353+ use Doctrine\DBAL\Connection;
354+ use Patchlevel\EventSourcing\Attribute\Projector;
355+ use Patchlevel\EventSourcing\Attribute\Setup;
356+ use Patchlevel\EventSourcing\Attribute\Teardown;
357+
358+ #[Projector(self::TABLE)]
359+ final class ProfileProjector
360+ {
361+ private const TABLE = 'profile_v1';
362+
363+ private Connection $connection;
364+
365+ #[Teardown]
366+ public function drop(): void
367+ {
368+ $this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE));
369+ }
370+ }
371+ ```
372+ !!! danger
373+
374+ MySQL and MariaDB don't support transactions for DDL statements.
375+ So you must use a different database connection in your projectors,
376+ otherwise you will get an error when the subscription tries to create the table.
377+
378+ !!! warning
379+
380+ A teardown can only be performed for a subscription if the subscriber with that subscriber ID still exists.
381+
382+ !!! note
383+
384+ You can not mix the `cleanup` method with the `teardown` method.
385+
386+ ### Cleanup
387+
388+ The cleanup option allows you to delete the subscription from the database after the subscription has finished.
389+ This is especially useful for projectors,
390+
391+ ``` php
392+ use Doctrine\DBAL\Connection;
393+ use Patchlevel\EventSourcing\Attribute\Cleanup;
394+ use Patchlevel\EventSourcing\Attribute\Projector;
395+ use Patchlevel\EventSourcing\Attribute\Setup;
396+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DropIndexTask;
397+
398+ #[Projector(self::TABLE)]
399+ final class ProfileProjector
400+ {
401+ private const TABLE = 'profile_v1';
402+
403+ private Connection $connection;
404+
405+ #[Cleanup]
406+ public function drop(): array
407+ {
408+ return [
409+ new DropIndexTask(self::TABLE)
410+ ];
411+ }
412+ }
413+ ```
414+
415+ !!! note
416+
417+ You can not mix the `cleanup` method with the `teardown` method.
418+
419+ !!! tip
420+
421+ You can create your own cleanup tasks and add them to the array.
422+ For more information, see [Cleanup Handler](#cleanup-handler).
423+
353424### On Failed
354425
355426The subscription engine has a [ retry strategy] ( #retry-strategy ) to retry subscriptions that have an error.
@@ -924,7 +995,63 @@ $retryStrategyRepository = new RetryStrategyRepository([
924995!!! tip
925996
926997 You can change the default retry strategy by define the name in the constructor as second parameter.
998+
999+ ### Cleanup Handler
1000+
1001+ You can also create your own cleanup tasks.
1002+ The subscription engine will call the method ` __invoke ` on the task object.
1003+ For example, you can create a task that drops a collection from a MongoDB database.
1004+
1005+ First, create a task class, that holds the collection name.
1006+
1007+ ``` php
1008+ final class DropCollection {
1009+ public function __construct(
1010+ public readonly string $collectionName
1011+ ) {}
1012+ }
1013+ ```
1014+
1015+ !!! warning
1016+
1017+ The task class must be serializable. It will be stored in the subscription store.
1018+
1019+ Then create a handler that supports this task.
1020+
1021+ ``` php
1022+ use MongoDb\Database;
1023+ use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupHandler;
1024+
1025+ final class MongodbCleanupHandler implements CleanupHandler {
1026+ public function __construct(
1027+ private readonly Database $database
1028+ ) {}
1029+
1030+ public function __invoke(object $task): void
1031+ {
1032+ if ($task instanceof DropCollection) {
1033+ $this->database->dropCollection($task->collectionName);
1034+ }
1035+ }
9271036
1037+ public function supports(object $task): bool
1038+ {
1039+ return $task instanceof DropCollection;
1040+ }
1041+ }
1042+ ```
1043+
1044+ Last step, you need to add the handler to the ` DefaultCleaner ` , that is used by the subscription engine.
1045+
1046+ ``` php
1047+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
1048+
1049+ $cleaner = new DefaultCleaner([
1050+ new DbalCleanupHandler($projectionConnection),
1051+ new MongodbCleanupHandler($mongodbDatabase),
1052+ ]);
1053+ ```
1054+
9281055### Subscriber Accessor
9291056
9301057The subscriber accessor repository is responsible for providing the subscribers to the subscription engine.
@@ -944,30 +1071,41 @@ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([
9441071 $subscriber3,
9451072]);
9461073```
1074+
9471075### Subscription Engine
9481076
9491077Now we can create the subscription engine and plug together the necessary services.
9501078The message loader is needed to load the messages, the Subscription Store to store the subscription state
9511079and we need the subscriber accessor repository. Optionally, we can also pass a retry strategy.
1080+ Finally, if we want to use the cleanup feature, we need to pass the cleanup handlers.
9521081
9531082``` php
1083+ use Doctrine\DBAL\Connection;
9541084use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
9551085use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
9561086use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
9571087use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore;
9581088use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository;
1089+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupHandler;
1090+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
9591091
9601092/**
9611093 * @var MessageLoader $messageLoader
9621094 * @var DoctrineSubscriptionStore $subscriptionStore
9631095 * @var MetadataSubscriberAccessorRepository $subscriberAccessorRepository
9641096 * @var RetryStrategyRepository $retryStrategyRepository
1097+ * @var LoggerInterface $logger
1098+ * @var Connection $projectionConnection
9651099 */
9661100$subscriptionEngine = new DefaultSubscriptionEngine(
9671101 $messageLoader,
9681102 $subscriptionStore,
9691103 $subscriberAccessorRepository,
9701104 $retryStrategyRepository, // optional, if not set the default retry strategy is used
1105+ $logger, // optional
1106+ new DefaultCleaner([
1107+ new DbalCleanupHandler($projectionConnection)
1108+ ]), // required, if you want to use the cleanup feature
9711109);
9721110```
9731111### Catch up Subscription Engine
0 commit comments