diff --git a/.coderabbit.yaml b/.coderabbit.yaml
index e6be6bb2..0bfc6829 100644
--- a/.coderabbit.yaml
+++ b/.coderabbit.yaml
@@ -9,32 +9,40 @@ reviews:
high_level_summary_in_walkthrough: false
poem: false
path_instructions:
- - path: "src/Domain/**"
- instructions: |
- You are reviewing PHP domain-layer code. Enforce strict domain purity:
- - ❌ Do not allow infrastructure persistence side effects here.
- - Flag ANY usage of Doctrine persistence APIs, especially:
- - $entityManager->flush(...), $this->entityManager->flush(...)
- - $em->persist(...), $em->remove(...)
- - direct transaction control ($em->beginTransaction(), commit(), rollback())
- - If found, request moving these calls to application-layer Command handlers or background Jobs.
- - Also flag repositories in Domain that invoke flush/transactional logic; Domain repositories should be abstractions without side effects.
- - Encourage domain events/outbox or return-values to signal write-intent, leaving orchestration to Commands/Jobs.
+ - path: "src/Domain/**"
+ instructions: |
+ You are reviewing PHP domain-layer code. Enforce strict domain purity:
+ - ❌ Do not allow persistence or transaction side effects here.
+ - Flag ANY usage of Doctrine persistence APIs, especially:
+ - `$entityManager->flush(...)`, `$this->entityManager->flush(...)`
+ - `$em->persist(...)`, `$em->remove(...)`
+ - `$em->beginTransaction()`, `$em->commit()`, `$em->rollback()`
+ - ✅ Accessing Doctrine *metadata*, *schema manager*, or *read-only schema info* is acceptable
+ as long as it does not modify state or perform writes.
+ - ⚠️ If code is invoking actual table-creation, DDL execution, or schema synchronization,
+ then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
+ - Also flag repositories in Domain that invoke flush/transactional logic;
+ Domain repositories should be abstractions without side effects.
+ - Encourage using domain events or return-values to signal intent to write,
+ leaving persistence orchestration to Commands/Jobs.
- - path: "src/**/Command/**"
- instructions: |
- Application layer (Commands/Handlers) is the right place to coordinate persistence.
- - ✅ It is acceptable to call $entityManager->flush() here.
- - Check that flush is used atomically (once per unit of work) after all domain operations.
- - Ensure no domain entity or domain service is calling flush; only the handler orchestrates it.
- - Prefer $em->transactional(...) or explicit try/catch with rollback on failure.
+ - path: "src/**/Command/**"
+ instructions: |
+ Application layer (Commands/Handlers) is the right place to coordinate persistence.
+ - ✅ It is acceptable to call $entityManager->flush() here.
+ - Check that flush is used atomically (once per unit of work) after all domain operations.
+ - Ensure no domain entity or domain service is calling flush; only the handler orchestrates it.
+ - Prefer $em->transactional(...) or explicit try/catch with rollback on failure.
- - path: "src/**/MessageHandler/**"
- instructions: |
- Background jobs/workers may perform persistence.
- - ✅ Allow $entityManager->flush() here when the job is the orchestration boundary.
- - Verify idempotency and that flush frequency is appropriate (batching where practical).
- - Ensure no domain-layer code invoked by the job performs flush/transaction control.
+ - path: "src/**/MessageHandler/**"
+ instructions: |
+ Background jobs/workers may perform persistence and schema management.
+ - ✅ Allow `$entityManager->flush()` when the job is the orchestration boundary.
+ - ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
+ as this is considered infrastructure-level orchestration.
+ - Verify idempotency for schema operations — e.g., check if a table exists before creating.
+ - Ensure domain-layer code invoked by the job remains free of persistence calls.
+ - Batch flush operations where practical.
auto_review:
enabled: true
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index aad3619f..0c6c6983 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,3 @@
-"@coderabbitai summary"
+@coderabbitai summary
Thanks for contributing to phpList!
diff --git a/composer.json b/composer.json
index 4ea09327..821b5c99 100644
--- a/composer.json
+++ b/composer.json
@@ -77,7 +77,8 @@
"symfony/lock": "^6.4",
"webklex/php-imap": "^6.2",
"ext-imap": "*",
- "tatevikgr/rss-feed": "dev-main"
+ "tatevikgr/rss-feed": "dev-main",
+ "ext-pdo": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
diff --git a/config/PHPMD/rules.xml b/config/PHPMD/rules.xml
index b8b0c3df..b3b8a8d4 100644
--- a/config/PHPMD/rules.xml
+++ b/config/PHPMD/rules.xml
@@ -8,22 +8,6 @@
-
-
-
-
-
-
diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml
index 38a75a1b..4193c501 100644
--- a/config/packages/messenger.yaml
+++ b/config/packages/messenger.yaml
@@ -30,4 +30,5 @@ framework:
'PhpList\Core\Domain\Messaging\Message\PasswordResetMessage': async_email
'PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage': async_email
'PhpList\Core\Domain\Messaging\Message\SyncCampaignProcessorMessage': sync
+ 'PhpList\Core\Domain\Subscription\Message\DynamicTableMessage': sync
diff --git a/config/parameters.yml.dist b/config/parameters.yml.dist
index 4e9e0cff..88f0b2b2 100644
--- a/config/parameters.yml.dist
+++ b/config/parameters.yml.dist
@@ -23,6 +23,8 @@ parameters:
env(PHPLIST_DATABASE_PASSWORD): 'phplist'
database_prefix: '%%env(DATABASE_PREFIX)%%'
env(DATABASE_PREFIX): 'phplist_'
+ list_table_prefix: '%%env(LIST_TABLE_PREFIX)%%'
+ env(LIST_TABLE_PREFIX): 'listattr_'
# Email configuration
app.mailer_from: '%%env(MAILER_FROM)%%'
diff --git a/config/services/managers.yml b/config/services/managers.yml
index 2f72bd7e..282f5e1d 100644
--- a/config/services/managers.yml
+++ b/config/services/managers.yml
@@ -48,6 +48,21 @@ services:
autowire: true
autoconfigure: true
+ PhpList\Core\Domain\Subscription\Service\Manager\DynamicListAttrManager:
+ autowire: true
+ autoconfigure: true
+
+ Doctrine\DBAL\Schema\AbstractSchemaManager:
+ factory: ['@doctrine.dbal.default_connection', 'createSchemaManager']
+
+ PhpList\Core\Domain\Subscription\Service\Manager\DynamicListAttrTablesManager:
+ autowire: true
+ autoconfigure: true
+ public: true
+ arguments:
+ $dbPrefix: '%database_prefix%'
+ $dynamicListTablePrefix: '%list_table_prefix%'
+
PhpList\Core\Domain\Subscription\Service\Manager\SubscriberHistoryManager:
autowire: true
autoconfigure: true
diff --git a/config/services/mappers.yml b/config/services/mappers.yml
index f84bd717..b0df7b58 100644
--- a/config/services/mappers.yml
+++ b/config/services/mappers.yml
@@ -4,10 +4,10 @@ services:
autoconfigure: true
public: false
- PhpList\Core\Domain\Subscription\Service\CsvRowToDtoMapper:
+ PhpList\Core\Domain\Subscription\Service\ArrayToDtoMapper:
autowire: true
autoconfigure: true
- PhpList\Core\Domain\Subscription\Service\CsvImporter:
+ PhpList\Core\Domain\Subscription\Service\CsvToDtoImporter:
autowire: true
autoconfigure: true
diff --git a/config/services/messenger.yml b/config/services/messenger.yml
index 214a4c86..79076663 100644
--- a/config/services/messenger.yml
+++ b/config/services/messenger.yml
@@ -29,3 +29,7 @@ services:
autoconfigure: true
tags: [ 'messenger.message_handler' ]
+ PhpList\Core\Domain\Subscription\MessageHandler\DynamicTableMessageHandler:
+ autowire: true
+ autoconfigure: true
+ tags: [ 'messenger.message_handler' ]
diff --git a/config/services/repositories.yml b/config/services/repositories.yml
index e9d4d8c6..bab5c2d4 100644
--- a/config/services/repositories.yml
+++ b/config/services/repositories.yml
@@ -70,7 +70,8 @@ services:
PhpList\Core\Domain\Subscription\Repository\DynamicListAttrRepository:
autowire: true
arguments:
- $prefix: '%database_prefix%'
+ $dbPrefix: '%database_prefix%'
+ $dynamicListTablePrefix: '%list_table_prefix%'
PhpList\Core\Domain\Subscription\Repository\SubscriberHistoryRepository:
parent: PhpList\Core\Domain\Common\Repository\AbstractRepository
arguments:
diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf
index efeb8ad2..02ca7140 100644
--- a/resources/translations/messages.en.xlf
+++ b/resources/translations/messages.en.xlf
@@ -342,10 +342,6 @@
Subscription not found for this subscriber and list.Subscription not found for this subscriber and list.
-
- Attribute definition already exists
- Attribute definition already exists
- Another attribute with this name already exists.Another attribute with this name already exists.
@@ -734,6 +730,14 @@ Thank you.
Conflict: email and foreign key refer to different subscribers.__Conflict: email and foreign key refer to different subscribers.
+
+ Invalid email: %email%
+ __Invalid email: %email%
+
+
+ Value must be an AttributeTypeEnum or string.
+ __Value must be an AttributeTypeEnum or string.
+