@@ -297,17 +297,21 @@ final class DoStuffSubscriber
297297 }
298298}
299299```
300- ### Setup and Teardown
300+ ##### Custom Resolvers
301301
302- Subscribers can have one ` setup ` and ` teardown ` method that is executed when the subscription is created or deleted.
303- For this there are the attributes ` Setup ` and ` Teardown ` . The method name itself doesn't matter.
302+ You can provide your own argument resolvers by implementing the ` ArgumentResolver ` interface.
303+ This can be useful for providing direct access to custom headers or other data.
304+
305+ ### Setup
306+
307+ Subscribers can have one ` setup ` method that is executed when the subscription is created.
308+ For this there is the attributes ` Setup ` . The method name itself doesn't matter.
304309This is especially helpful for projectors, as they can create the necessary structures for the projection here.
305310
306311``` php
307312use Doctrine\DBAL\Connection;
308313use Patchlevel\EventSourcing\Attribute\Projector;
309314use Patchlevel\EventSourcing\Attribute\Setup;
310- use Patchlevel\EventSourcing\Attribute\Teardown;
311315
312316#[Projector(self::TABLE)]
313317final class ProfileProjector
@@ -323,12 +327,6 @@ final class ProfileProjector
323327 sprintf('CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);', self::TABLE),
324328 );
325329 }
326-
327- #[Teardown]
328- public function drop(): void
329- {
330- $this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE));
331- }
332330}
333331```
334332
@@ -349,6 +347,91 @@ Most databases have a limit on the length of the table/collection name.
349347The limit is usually 64 characters.
350348:::
351349
350+ ### Teardown
351+
352+ Subscribers can have one ` teardown ` method that is executed when the subscription is removed.
353+ For this there is the attributes ` Teardown ` .
354+
355+ ``` php
356+ use Doctrine\DBAL\Connection;
357+ use Patchlevel\EventSourcing\Attribute\Projector;
358+ use Patchlevel\EventSourcing\Attribute\Teardown;
359+
360+ #[Projector(self::TABLE)]
361+ final class ProfileProjector
362+ {
363+ private const TABLE = 'profile_v1';
364+
365+ private Connection $connection;
366+
367+ #[Teardown]
368+ public function drop(): void
369+ {
370+ $this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s;', self::TABLE));
371+ }
372+ }
373+ ```
374+ !!! danger
375+
376+ MySQL and MariaDB don't support transactions for DDL statements.
377+ So you must use a different database connection in your projectors,
378+ otherwise you will get an error when the subscription tries to create the table.
379+
380+ !!! warning
381+
382+ A teardown can only be performed for a subscription if the code for the subscriber with that subscriber ID still exists.
383+ A another option is to use the `Cleanup` option.
384+
385+ !!! note
386+
387+ You can not mix the `cleanup` method with the `teardown` method.
388+
389+ ### Cleanup
390+
391+ Alternativ, you can use a ` cleanup ` method for cleanup tasks.
392+ Unlike Teardown, this method is called when the subscription is created.
393+ The tasks are then saved in the Subscription Store.
394+ When removing the subscription, the subscriber is not necessary anymore,
395+ as the cleanup can be performed using the tasks in the store and an associated external handler.
396+
397+ ``` php
398+ use Doctrine\DBAL\Connection;
399+ use Patchlevel\EventSourcing\Attribute\Cleanup;
400+ use Patchlevel\EventSourcing\Attribute\Projector;
401+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DropIndexTask;
402+
403+ #[Projector(self::TABLE)]
404+ final class ProfileProjector
405+ {
406+ private const TABLE = 'profile_v1';
407+
408+ private Connection $connection;
409+
410+ #[Cleanup]
411+ public function drop(): array
412+ {
413+ return [new DropIndexTask(self::TABLE)];
414+ }
415+ }
416+ ```
417+ !!! note
418+
419+ You can not mix the `cleanup` method with the `teardown` method.
420+
421+ #### Dbal Cleanup Tasks
422+
423+ Default, we provide the following cleanup tasks for ` doctrine/dbal ` :
424+
425+ | Task | Description |
426+ | -----------------| ------------------------------|
427+ | ` DropIndexTask ` | Drops an index from a table. |
428+ | ` DropTableTask ` | Drops a table. |
429+
430+ !!! tip
431+
432+ You can create your own cleanup tasks and handler.
433+ For more information, see [Cleanup Handler](#cleanup-handler).
434+
352435### On Failed
353436
354437The subscription engine has a [ retry strategy] ( #retry-strategy ) to retry subscriptions that have an error.
@@ -931,6 +1014,70 @@ This is what our default configuration looks like if you do not define the retry
9311014You can change the default retry strategy by define the name in the constructor as second parameter.
9321015:::
9331016
1017+ ### Cleanup Handler
1018+
1019+ You can also create your own cleanup tasks with associated handlers.
1020+ First, create a task class that has all necessary information for the task.
1021+ In our example, we create a task that deletes a collection from MongoDB.
1022+
1023+ ``` php
1024+ final class DropCollection
1025+ {
1026+ public function __construct(
1027+ public readonly string $collectionName,
1028+ ) {
1029+ }
1030+ }
1031+ ```
1032+ !!! warning
1033+
1034+ The task class must be serializable. It will be stored in the subscription store.
1035+
1036+ The next step is to create a handler for the task.
1037+ The handler must implement the ` CleanupHandler ` interface.
1038+
1039+ ``` php
1040+ use MongoDb\Database;
1041+ use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupTaskHandler;
1042+
1043+ final class MongodbCleanupTaskHandler implements CleanupTaskHandler
1044+ {
1045+ public function __construct(
1046+ private readonly Database $database,
1047+ ) {
1048+ }
1049+
1050+ public function __invoke(object $task): void
1051+ {
1052+ if (!($task instanceof DropCollection)) {
1053+ return;
1054+ }
1055+
1056+ $this->database->dropCollection($task->collectionName);
1057+ }
1058+
1059+ public function supports(object $task): bool
1060+ {
1061+ return $task instanceof DropCollection;
1062+ }
1063+ }
1064+ ```
1065+ Lastly, we have to add the new handler to ` DefaultCleaner ` ,
1066+ which is responsible for cleaning up subscriptions.
1067+
1068+ ``` php
1069+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupTaskHandler;
1070+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
1071+
1072+ $cleaner = new DefaultCleaner([
1073+ new DbalCleanupTaskHandler($projectionConnection),
1074+ new MongodbCleanupTaskHandler($mongodbDatabase),
1075+ ]);
1076+ ```
1077+ !!! warning
1078+
1079+ You need to pass the Cleaner to the Subscription Engine.
1080+
9341081### Subscriber Accessor
9351082
9361083The subscriber accessor repository is responsible for providing the subscribers to the subscription engine.
@@ -955,8 +1102,12 @@ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([
9551102Now we can create the subscription engine and plug together the necessary services.
9561103The message loader is needed to load the messages, the Subscription Store to store the subscription state
9571104and we need the subscriber accessor repository. Optionally, we can also pass a retry strategy.
1105+ Finally, if we want to use the cleanup feature, we need to pass the cleanup handlers.
9581106
9591107``` php
1108+ use Doctrine\DBAL\Connection;
1109+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupTaskHandler;
1110+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
9601111use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
9611112use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
9621113use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
@@ -968,12 +1119,16 @@ use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorR
9681119 * @var DoctrineSubscriptionStore $subscriptionStore
9691120 * @var MetadataSubscriberAccessorRepository $subscriberAccessorRepository
9701121 * @var RetryStrategyRepository $retryStrategyRepository
1122+ * @var LoggerInterface $logger
1123+ * @var Connection $projectionConnection
9711124 */
9721125$subscriptionEngine = new DefaultSubscriptionEngine(
9731126 $messageLoader,
9741127 $subscriptionStore,
9751128 $subscriberAccessorRepository,
9761129 $retryStrategyRepository, // optional, if not set the default retry strategy is used
1130+ $logger, // optional
1131+ new DefaultCleaner([new DbalCleanupTaskHandler($projectionConnection)]), // optional but required if you want to use the cleanup feature
9771132);
9781133```
9791134### Catch up Subscription Engine
0 commit comments