@@ -293,23 +293,21 @@ final class DoStuffSubscriber
293293 }
294294}
295295```
296-
297296##### Custom Resolvers
298297
299298You can provide your own argument resolvers by implementing the ` ArgumentResolver ` interface.
300299This can be useful for providing direct access to custom headers or other data.
301300
302- ### Setup and Teardown
301+ ### Setup
303302
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.
303+ Subscribers can have one ` setup ` method that is executed when the subscription is created.
304+ For this there is the attributes ` Setup ` . The method name itself doesn't matter.
306305This is especially helpful for projectors, as they can create the necessary structures for the projection here.
307306
308307``` php
309308use Doctrine\DBAL\Connection;
310309use Patchlevel\EventSourcing\Attribute\Projector;
311310use Patchlevel\EventSourcing\Attribute\Setup;
312- use Patchlevel\EventSourcing\Attribute\Teardown;
313311
314312#[Projector(self::TABLE)]
315313final class ProfileProjector
@@ -325,6 +323,41 @@ final class ProfileProjector
325323 sprintf('CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);', self::TABLE),
326324 );
327325 }
326+ }
327+ ```
328+ !!! danger
329+
330+ MySQL and MariaDB don't support transactions for DDL statements.
331+ So you must use a different database connection in your projectors,
332+ otherwise you will get an error when the subscription tries to create the table.
333+
334+ !!! warning
335+
336+ If you change the subscriber id, you must also change the table/collection name.
337+ The subscription engine will create a new subscription with the new subscriber id.
338+ That means the setup method will be called again and the table/collection will conflict with the old existing projection.
339+
340+ !!! note
341+
342+ Most databases have a limit on the length of the table/collection name.
343+ The limit is usually 64 characters.
344+
345+ ### Teardown
346+
347+ Subscribers can have one ` teardown ` method that is executed when the subscription is removed.
348+ For this there is the attributes ` Teardown ` .
349+
350+ ``` php
351+ use Doctrine\DBAL\Connection;
352+ use Patchlevel\EventSourcing\Attribute\Projector;
353+ use Patchlevel\EventSourcing\Attribute\Teardown;
354+
355+ #[Projector(self::TABLE)]
356+ final class ProfileProjector
357+ {
358+ private const TABLE = 'profile_v1';
359+
360+ private Connection $connection;
328361
329362 #[Teardown]
330363 public function drop(): void
@@ -341,14 +374,58 @@ final class ProfileProjector
341374
342375!!! warning
343376
344- If you change the subscriber id, you must also change the table/collection name.
345- The subscription engine will create a new subscription with the new subscriber id.
346- That means the setup method will be called again and the table/collection will conflict with the old existing projection.
377+ A teardown can only be performed for a subscription if the code for the subscriber with that subscriber ID still exists.
378+ A another option is to use the `Cleanup` option.
347379
348380!!! note
349381
350- Most databases have a limit on the length of the table/collection name.
351- The limit is usually 64 characters.
382+ You can not mix the `cleanup` method with the `teardown` method.
383+
384+ ### Cleanup
385+
386+ Alternativ, you can use a ` cleanup ` method for cleanup tasks.
387+ Unlike Teardown, this method is called when the subscription is created.
388+ The tasks are then saved in the Subscription Store.
389+ When removing the subscription, the subscriber is not necessary anymore,
390+ as the cleanup can be performed using the tasks in the store and an associated external handler.
391+
392+ ``` php
393+ use Doctrine\DBAL\Connection;
394+ use Patchlevel\EventSourcing\Attribute\Cleanup;
395+ use Patchlevel\EventSourcing\Attribute\Projector;
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 [new DropIndexTask(self::TABLE)];
409+ }
410+ }
411+ ```
412+ !!! note
413+
414+ You can not mix the `cleanup` method with the `teardown` method.
415+
416+ #### Dbal Cleanup Tasks
417+
418+ Default, we provide the following cleanup tasks for ` doctrine/dbal ` :
419+
420+ | Task | Description |
421+ | -----------------| ------------------------------|
422+ | ` DropIndexTask ` | Drops an index from a table. |
423+ | ` DropTableTask ` | Drops a table. |
424+
425+ !!! tip
426+
427+ You can create your own cleanup tasks and handler.
428+ For more information, see [Cleanup Handler](#cleanup-handler).
352429
353430### On Failed
354431
@@ -925,6 +1002,70 @@ $retryStrategyRepository = new RetryStrategyRepository([
9251002
9261003 You can change the default retry strategy by define the name in the constructor as second parameter.
9271004
1005+ ### Cleanup Handler
1006+
1007+ You can also create your own cleanup tasks with associated handlers.
1008+ First, create a task class that has all necessary information for the task.
1009+ In our example, we create a task that deletes a collection from MongoDB.
1010+
1011+ ``` php
1012+ final class DropCollection
1013+ {
1014+ public function __construct(
1015+ public readonly string $collectionName,
1016+ ) {
1017+ }
1018+ }
1019+ ```
1020+ !!! warning
1021+
1022+ The task class must be serializable. It will be stored in the subscription store.
1023+
1024+ The next step is to create a handler for the task.
1025+ The handler must implement the ` CleanupHandler ` interface.
1026+
1027+ ``` php
1028+ use MongoDb\Database;
1029+ use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupTaskHandler;
1030+
1031+ final class MongodbCleanupTaskHandler implements CleanupTaskHandler
1032+ {
1033+ public function __construct(
1034+ private readonly Database $database,
1035+ ) {
1036+ }
1037+
1038+ public function __invoke(object $task): void
1039+ {
1040+ if (!($task instanceof DropCollection)) {
1041+ return;
1042+ }
1043+
1044+ $this->database->dropCollection($task->collectionName);
1045+ }
1046+
1047+ public function supports(object $task): bool
1048+ {
1049+ return $task instanceof DropCollection;
1050+ }
1051+ }
1052+ ```
1053+ Lastly, we have to add the new handler to ` DefaultCleaner ` ,
1054+ which is responsible for cleaning up subscriptions.
1055+
1056+ ``` php
1057+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupTaskHandler;
1058+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
1059+
1060+ $cleaner = new DefaultCleaner([
1061+ new DbalCleanupTaskHandler($projectionConnection),
1062+ new MongodbCleanupTaskHandler($mongodbDatabase),
1063+ ]);
1064+ ```
1065+ !!! warning
1066+
1067+ You need to pass the Cleaner to the Subscription Engine.
1068+
9281069### Subscriber Accessor
9291070
9301071The subscriber accessor repository is responsible for providing the subscribers to the subscription engine.
@@ -949,8 +1090,12 @@ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([
9491090Now we can create the subscription engine and plug together the necessary services.
9501091The message loader is needed to load the messages, the Subscription Store to store the subscription state
9511092and we need the subscriber accessor repository. Optionally, we can also pass a retry strategy.
1093+ Finally, if we want to use the cleanup feature, we need to pass the cleanup handlers.
9521094
9531095``` php
1096+ use Doctrine\DBAL\Connection;
1097+ use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DbalCleanupTaskHandler;
1098+ use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
9541099use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
9551100use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
9561101use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
@@ -962,12 +1107,16 @@ use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorR
9621107 * @var DoctrineSubscriptionStore $subscriptionStore
9631108 * @var MetadataSubscriberAccessorRepository $subscriberAccessorRepository
9641109 * @var RetryStrategyRepository $retryStrategyRepository
1110+ * @var LoggerInterface $logger
1111+ * @var Connection $projectionConnection
9651112 */
9661113$subscriptionEngine = new DefaultSubscriptionEngine(
9671114 $messageLoader,
9681115 $subscriptionStore,
9691116 $subscriberAccessorRepository,
9701117 $retryStrategyRepository, // optional, if not set the default retry strategy is used
1118+ $logger, // optional
1119+ new DefaultCleaner([new DbalCleanupTaskHandler($projectionConnection)]), // optional but required if you want to use the cleanup feature
9711120);
9721121```
9731122### Catch up Subscription Engine
0 commit comments