Skip to content

Commit 841b1f3

Browse files
committed
Merge branch '3.17.x' into 4.0.x
2 parents f9137a9 + 7a3e1f5 commit 841b1f3

35 files changed

Lines changed: 1670 additions & 274 deletions

.github/workflows/benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
services:
1515
postgres:
1616
# Docker Hub image
17-
image: "postgres:18.1"
17+
image: "postgres:18.2"
1818
# Provide the password for postgres
1919
env:
2020
POSTGRES_PASSWORD: postgres

composer.lock

Lines changed: 221 additions & 197 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/subscription.md

Lines changed: 165 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
304309
This is especially helpful for projectors, as they can create the necessary structures for the projection here.
305310

306311
```php
307312
use Doctrine\DBAL\Connection;
308313
use Patchlevel\EventSourcing\Attribute\Projector;
309314
use Patchlevel\EventSourcing\Attribute\Setup;
310-
use Patchlevel\EventSourcing\Attribute\Teardown;
311315

312316
#[Projector(self::TABLE)]
313317
final 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.
349347
The 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

354437
The 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
9311014
You 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

9361083
The subscriber accessor repository is responsible for providing the subscribers to the subscription engine.
@@ -955,8 +1102,12 @@ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([
9551102
Now we can create the subscription engine and plug together the necessary services.
9561103
The message loader is needed to load the messages, the Subscription Store to store the subscription state
9571104
and 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;
9601111
use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
9611112
use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
9621113
use 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

phpstan-baseline.neon

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
parameters:
22
ignoreErrors:
33
-
4-
message: '#^Cannot unset offset ''url'' on array\{application_name\?\: string, charset\?\: string, dbname\?\: string, defaultTableOptions\?\: array\<string, mixed\>, driver\?\: ''ibm_db2''\|''mysqli''\|''oci8''\|''pdo_mysql''\|''pdo_oci''\|''pdo_pgsql''\|''pdo_sqlite''\|''pdo_sqlsrv''\|''pgsql''\|''sqlite3''\|''sqlsrv'', driverClass\?\: class\-string\<Doctrine\\DBAL\\Driver\>, driverOptions\?\: array\<mixed\>, host\?\: string, \.\.\.\}\.$#'
4+
message: '#^Cannot unset offset ''url'' on array\{application_name\?\: string, charset\?\: string, defaultTableOptions\?\: array\<string, mixed\>, driver\?\: ''ibm_db2''\|''mysqli''\|''oci8''\|''pdo_mysql''\|''pdo_oci''\|''pdo_pgsql''\|''pdo_sqlite''\|''pdo_sqlsrv''\|''pgsql''\|''sqlite3''\|''sqlsrv'', driverClass\?\: class\-string\<Doctrine\\DBAL\\Driver\>, driverOptions\?\: array\<mixed\>, host\?\: string, keepReplica\?\: bool, \.\.\.\}\.$#'
55
identifier: unset.offset
66
count: 1
77
path: src/Console/DoctrineHelper.php
@@ -156,6 +156,12 @@ parameters:
156156
count: 1
157157
path: src/Subscription/Store/DoctrineSubscriptionStore.php
158158

159+
-
160+
message: '#^Parameter \#9 \$cleanupTasks of class Patchlevel\\EventSourcing\\Subscription\\Subscription constructor expects list\<object\>\|null, mixed given\.$#'
161+
identifier: argument.type
162+
count: 1
163+
path: src/Subscription/Store/DoctrineSubscriptionStore.php
164+
159165
-
160166
message: '#^Cannot cast mixed to string\.$#'
161167
identifier: cast.string
@@ -174,12 +180,6 @@ parameters:
174180
count: 1
175181
path: tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php
176182

177-
-
178-
message: '#^Parameter \#1 \$messageLoader of class Patchlevel\\EventSourcing\\Subscription\\Engine\\DefaultSubscriptionEngine constructor expects Patchlevel\\EventSourcing\\Subscription\\Engine\\MessageLoader, Patchlevel\\EventSourcing\\Store\\StreamDoctrineDbalStore given\.$#'
179-
identifier: argument.type
180-
count: 1
181-
path: tests/Benchmark/CommandToQueryBench.php
182-
183183
-
184184
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Patchlevel\\\\EventSourcing\\\\Tests\\\\Integration\\\\BankAccountSplitStream\\\\BankAccount'' and Patchlevel\\EventSourcing\\Tests\\Integration\\BankAccountSplitStream\\BankAccount will always evaluate to true\.$#'
185185
identifier: staticMethod.alreadyNarrowedType
@@ -228,30 +228,6 @@ parameters:
228228
count: 1
229229
path: tests/Integration/BasicImplementation/BasicIntegrationTest.php
230230

231-
-
232-
message: '#^Instantiated class Patchlevel\\EventSourcing\\Tests\\Integration\\BasicImplementation\\DoctrineDbalStore not found\.$#'
233-
identifier: class.notFound
234-
count: 1
235-
path: tests/Integration/BasicImplementation/BasicIntegrationTest.php
236-
237-
-
238-
message: '#^Parameter \#1 \$messageLoader of class Patchlevel\\EventSourcing\\Subscription\\Engine\\DefaultSubscriptionEngine constructor expects Patchlevel\\EventSourcing\\Subscription\\Engine\\MessageLoader, Patchlevel\\EventSourcing\\Tests\\Integration\\BasicImplementation\\DoctrineDbalStore given\.$#'
239-
identifier: argument.type
240-
count: 1
241-
path: tests/Integration/BasicImplementation/BasicIntegrationTest.php
242-
243-
-
244-
message: '#^Parameter \#2 \$schemaConfigurator of class Patchlevel\\EventSourcing\\Schema\\DoctrineSchemaDirector constructor expects Patchlevel\\EventSourcing\\Schema\\DoctrineSchemaConfigurator, Patchlevel\\EventSourcing\\Tests\\Integration\\BasicImplementation\\DoctrineDbalStore given\.$#'
245-
identifier: argument.type
246-
count: 1
247-
path: tests/Integration/BasicImplementation/BasicIntegrationTest.php
248-
249-
-
250-
message: '#^Parameter \#2 \$store of class Patchlevel\\EventSourcing\\Repository\\DefaultRepositoryManager constructor expects Patchlevel\\EventSourcing\\Store\\Store, Patchlevel\\EventSourcing\\Tests\\Integration\\BasicImplementation\\DoctrineDbalStore given\.$#'
251-
identifier: argument.type
252-
count: 1
253-
path: tests/Integration/BasicImplementation/BasicIntegrationTest.php
254-
255231
-
256232
message: '#^Cannot access offset ''name'' on array\<string, mixed\>\|false\.$#'
257233
identifier: offsetAccess.nonOffsetAccessible
@@ -318,6 +294,12 @@ parameters:
318294
count: 1
319295
path: tests/Integration/Subscription/SubscriptionTest.php
320296

297+
-
298+
message: '#^Instantiated class Patchlevel\\EventSourcing\\Tests\\Integration\\Subscription\\MigrateAggregateToStreamStoreSubscriber not found\.$#'
299+
identifier: class.notFound
300+
count: 1
301+
path: tests/Integration/Subscription/SubscriptionTest.php
302+
321303
-
322304
message: '#^Parameter \#4 \$maxAttempts of class Patchlevel\\EventSourcing\\Subscription\\RetryStrategy\\ClockBasedRetryStrategy constructor expects int\<1, max\>, 0 given\.$#'
323305
identifier: argument.type
@@ -451,7 +433,7 @@ parameters:
451433
path: tests/Unit/Message/Translator/ReplaceEventTranslatorTest.php
452434

453435
-
454-
message: '#^Method class@anonymous/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest\.php\:211\:\:profileVisited\(\) has parameter \$message with no type specified\.$#'
436+
message: '#^Method class@anonymous/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest\.php\:236\:\:profileVisited\(\) has parameter \$message with no type specified\.$#'
455437
identifier: missingType.parameter
456438
count: 1
457439
path: tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php

src/Attribute/Cleanup.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Attribute;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_METHOD)]
10+
final class Cleanup
11+
{
12+
}

src/Metadata/Subscriber/ArgumentMetadata.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final class ArgumentMetadata
99
public function __construct(
1010
public readonly string $name,
1111
public readonly string $type,
12+
public readonly bool $allowsNull = false,
1213
) {
1314
}
1415
}

0 commit comments

Comments
 (0)