Skip to content

Commit 8258f60

Browse files
committed
update subscriber cleanup docs & add some tests
1 parent 80cd972 commit 8258f60

7 files changed

Lines changed: 332 additions & 20 deletions

File tree

docs/pages/subscription.md

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ final class DoStuffSubscriber
299299
You can provide your own argument resolvers by implementing the `ArgumentResolver` interface.
300300
This can be useful for providing direct access to custom headers or other data.
301301

302-
### Setup and Teardown
302+
### Setup
303303

304304
Subscribers can have one `setup` method that is executed when the subscription is created.
305305
For this there is the attributes `Setup`. The method name itself doesn't matter.
@@ -377,16 +377,20 @@ final class ProfileProjector
377377

378378
!!! warning
379379

380-
A teardown can only be performed for a subscription if the subscriber with that subscriber ID still exists.
380+
A teardown can only be performed for a subscription if the code for the subscriber with that subscriber ID still exists.
381+
A another option is to use the `Cleanup` option.
381382

382383
!!! note
383384

384385
You can not mix the `cleanup` method with the `teardown` method.
385386

386387
### Cleanup
387388

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,
389+
Alternativ, you can use a `cleanup` method for cleanup tasks.
390+
Unlike Teardown, this method is called when the subscription is created.
391+
The tasks are then saved in the Subscription Store.
392+
When removing the subscription, the subscriber is not necessary anymore,
393+
as the cleanup can be performed using the tasks in the store and an associated external handler.
390394

391395
```php
392396
use Doctrine\DBAL\Connection;
@@ -416,9 +420,18 @@ final class ProfileProjector
416420

417421
You can not mix the `cleanup` method with the `teardown` method.
418422

423+
#### Dbal Cleanup Tasks
424+
425+
Default, we provide the following cleanup tasks for `doctrine/dbal`:
426+
427+
| Task | Description |
428+
|-----------------|------------------------------|
429+
| `DropIndexTask` | Drops an index from a table. |
430+
| `DropTableTask` | Drops a table. |
431+
419432
!!! tip
420433

421-
You can create your own cleanup tasks and add them to the array.
434+
You can create your own cleanup tasks and handler.
422435
For more information, see [Cleanup Handler](#cleanup-handler).
423436

424437
### On Failed
@@ -998,11 +1011,9 @@ $retryStrategyRepository = new RetryStrategyRepository([
9981011

9991012
### Cleanup Handler
10001013

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.
1014+
You can also create your own cleanup tasks with associated handlers.
1015+
First, create a task class that has all necessary information for the task.
1016+
In our example, we create a task that deletes a collection from MongoDB.
10061017

10071018
```php
10081019
final class DropCollection {
@@ -1016,7 +1027,8 @@ final class DropCollection {
10161027

10171028
The task class must be serializable. It will be stored in the subscription store.
10181029

1019-
Then create a handler that supports this task.
1030+
The next step is to create a handler for the task.
1031+
The handler must implement the `CleanupHandler` interface.
10201032

10211033
```php
10221034
use MongoDb\Database;
@@ -1041,7 +1053,8 @@ final class MongodbCleanupHandler implements CleanupHandler {
10411053
}
10421054
```
10431055

1044-
Last step, you need to add the handler to the `DefaultCleaner`, that is used by the subscription engine.
1056+
Lastly, we have to add the new handler to `DefaultCleaner`,
1057+
which is responsible for cleaning up subscriptions.
10451058

10461059
```php
10471060
use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
@@ -1052,6 +1065,10 @@ $cleaner = new DefaultCleaner([
10521065
]);
10531066
```
10541067

1068+
!!! warning
1069+
1070+
You need to pass the Cleaner to the Subscription Engine.
1071+
10551072
### Subscriber Accessor
10561073

10571074
The subscriber accessor repository is responsible for providing the subscribers to the subscription engine.
@@ -1105,7 +1122,7 @@ $subscriptionEngine = new DefaultSubscriptionEngine(
11051122
$logger, // optional
11061123
new DefaultCleaner([
11071124
new DbalCleanupHandler($projectionConnection)
1108-
]), // required, if you want to use the cleanup feature
1125+
]), // optional but required if you want to use the cleanup feature
11091126
);
11101127
```
11111128
### Catch up Subscription Engine

src/Subscription/Cleanup/DefaultCleaner.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ final class DefaultCleaner implements Cleaner
1111
{
1212
/** @param iterable<CleanupHandler> $handlers */
1313
public function __construct(
14-
private readonly iterable $handlers,
14+
private readonly iterable $handlers = [],
1515
) {
1616
}
1717

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Subscription\Engine;
6+
7+
use RuntimeException;
8+
9+
final class CleanerNotConfigured extends RuntimeException
10+
{
11+
public function __construct()
12+
{
13+
parent::__construct('Cleaner not configured.');
14+
}
15+
}

src/Subscription/Engine/DefaultSubscriptionEngine.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Psr\Log\LoggerInterface;
2626
use Throwable;
2727

28+
use function array_values;
2829
use function count;
2930
use function sprintf;
3031

@@ -1245,7 +1246,7 @@ private function retryStrategy(Subscription $subscription): RetryStrategy
12451246
private function cleanup(Subscription $subscription): Error|null
12461247
{
12471248
if (!$this->cleaner) {
1248-
throw new UnexpectedError('Cleaner is not configured.');
1249+
throw new CleanerNotConfigured();
12491250
}
12501251

12511252
try {
@@ -1295,6 +1296,10 @@ private function cleanupTasks(SubscriberAccessor $subscriber): array|null
12951296
return null;
12961297
}
12971298

1299+
if (!$this->cleaner) {
1300+
throw new CleanerNotConfigured();
1301+
}
1302+
12981303
return array_values([...$method()]);
12991304
}
13001305
}

tests/Integration/Subscription/SubscriptionTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,12 @@ public function testCleanup(): void
11851185
{
11861186
// Test Setup
11871187

1188+
$cleaner = new DefaultCleaner([
1189+
new DbalCleanupHandler(
1190+
$this->projectionConnection,
1191+
),
1192+
]);
1193+
11881194
$store = new DoctrineDbalStore(
11891195
$this->connection,
11901196
DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']),
@@ -1218,6 +1224,7 @@ public function testCleanup(): void
12181224
$store,
12191225
$subscriptionStore,
12201226
new MetadataSubscriberAccessorRepository([new ProfileProjectionWithCleanup($this->projectionConnection)]),
1227+
cleaner: $cleaner,
12211228
);
12221229

12231230
// Deploy first version
@@ -1267,11 +1274,7 @@ public function testCleanup(): void
12671274
$store,
12681275
$subscriptionStore,
12691276
new MetadataSubscriberAccessorRepository([new ProfileNewProjection($this->projectionConnection)]),
1270-
cleaner: new DefaultCleaner([
1271-
new DbalCleanupHandler(
1272-
$this->projectionConnection,
1273-
),
1274-
]),
1277+
cleaner: $cleaner,
12751278
);
12761279

12771280
$secondEngine->setup();
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Unit\Subscription\Cleanup;
6+
7+
use Patchlevel\EventSourcing\Subscription\Cleanup\CleanupHandler;
8+
use Patchlevel\EventSourcing\Subscription\Cleanup\Dbal\DropTableTask;
9+
use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
10+
use Patchlevel\EventSourcing\Subscription\Cleanup\NoHandlerForCleanupTask;
11+
use Patchlevel\EventSourcing\Subscription\Subscription;
12+
use PHPUnit\Framework\Attributes\CoversClass;
13+
use PHPUnit\Framework\TestCase;
14+
use RuntimeException;
15+
16+
#[CoversClass(DefaultCleaner::class)]
17+
final class DefaultCleanerTest extends TestCase
18+
{
19+
public function testClean(): void
20+
{
21+
$handler = new class implements CleanupHandler {
22+
public bool $called = false;
23+
24+
public function __invoke(object $task): void
25+
{
26+
$this->called = true;
27+
}
28+
29+
public function supports(object $task): bool
30+
{
31+
return true;
32+
}
33+
};
34+
35+
$cleaner = new DefaultCleaner([$handler]);
36+
$cleaner->cleanup(
37+
new Subscription('test', cleanupTasks: [new DropTableTask('test')]),
38+
);
39+
40+
self::assertTrue($handler->called);
41+
}
42+
43+
public function testCleanupFailed(): void
44+
{
45+
$handler = new class implements CleanupHandler {
46+
public function __invoke(object $task): void
47+
{
48+
throw new RuntimeException('Failed to cleanup');
49+
}
50+
51+
public function supports(object $task): bool
52+
{
53+
return true;
54+
}
55+
};
56+
57+
$cleaner = new DefaultCleaner([$handler]);
58+
$this->expectException(RuntimeException::class);
59+
60+
$cleaner->cleanup(
61+
new Subscription('test', cleanupTasks: [new DropTableTask('test')]),
62+
);
63+
}
64+
65+
public function testNoTasks(): void
66+
{
67+
$handler = new class implements CleanupHandler {
68+
public bool $called = false;
69+
70+
public function __invoke(object $task): void
71+
{
72+
$this->called = true;
73+
}
74+
75+
public function supports(object $task): bool
76+
{
77+
return true;
78+
}
79+
};
80+
81+
$cleaner = new DefaultCleaner([$handler]);
82+
$cleaner->cleanup(new Subscription('test'));
83+
84+
self::assertTrue($handler->called === false);
85+
}
86+
87+
public function testNoHandler(): void
88+
{
89+
$this->expectException(NoHandlerForCleanupTask::class);
90+
91+
$cleaner = new DefaultCleaner();
92+
$cleaner->cleanup(
93+
new Subscription('test', cleanupTasks: [new DropTableTask('test')]),
94+
);
95+
}
96+
}

0 commit comments

Comments
 (0)