Skip to content

Conversation

@TatevikGr
Copy link
Contributor

@TatevikGr TatevikGr commented Sep 23, 2025

Summary

Unit test

Are your changes covered with unit tests, and do they not break anything?

You can run the existing unit tests using this command:

vendor/bin/phpunit tests/

Code style

Have you checked that you code is well-documented and follows the PSR-2 coding
style?

You can check for this using this command:

vendor/bin/phpcs --standard=PSR2 src/ tests/

Other Information

If there's anything else that's important and relevant to your pull
request, mention that information here. This could include benchmarks,
or other information.

If you are updating any of the CHANGELOG files or are asked to update the
CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file.

Thanks for contributing to phpList!

Summary by CodeRabbit

  • New Features

    • Bounce regex management API (list, get, create/update, delete)
    • Subscriber blacklist API (check, add, remove, info)
    • Subscribe pages management API (CRUD plus page data endpoints)
  • Documentation

    • Added OpenAPI schemas for BounceRegex and SubscribePage
  • Tests

    • New integration and unit tests covering bounce regex, blacklist, and subscribe page flows

TatevikGr and others added 11 commits August 8, 2025 22:30
* blacklist controller

* blacklist request

* UserBlacklistNormalizer

* add in development flags

* phpcs fix

* use blacklist branch

* fix swagger

* use core dev branch

---------

Co-authored-by: Tatevik <[email protected]>
* SubscribePageController

* SubscribePageNormalizer

* SubscribePageCreateRequest

* Get/set page data, update/delete page, add tests

---------

Co-authored-by: Tatevik <[email protected]>
* Add BounceRegexController

* Use code dev branch

---------

Co-authored-by: Tatevik <[email protected]>
TatevikGr and others added 2 commits October 22, 2025 13:54
* Fix composer json

* Add flush after delete

* Fix: test

* SubscriberBlacklistManager

* listMessageManager

* AdminController flush

* TemplateController flush

* SubscribePageController flush

* save

* fix test

---------

Co-authored-by: Tatevik <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Nov 6, 2025

📝 Walkthrough

Walkthrough

This PR adds bounce-regex, blacklist, and subscribe-page REST endpoints, new request DTOs and normalizers, and many integration/unit tests and fixtures. It reorganizes several messaging services under a Manager namespace, swaps direct campaign processing for Symfony Messenger dispatch, removes SubscriberService in favor of manager usage, and injects Doctrine EntityManager with explicit flush() calls across numerous controllers. Composer gains a VCS repository and tatevikgr/rss-feed dependency. Several validator attributes were adjusted to trim inputs.

Sequence Diagram

sequenceDiagram
    autonumber
    actor Client
    participant API as REST Controller
    participant Manager
    participant DB as EntityManager
    participant Bus as MessageBus
    participant Handler

    rect rgba(120,180,120,0.08)
    Note over Client,API: Typical synchronous CRUD (new controllers)
    Client->>API: HTTP create/update/delete
    API->>Manager: call manager (business logic)
    Manager->>DB: persist/remove
    DB-->>API: ack (after flush)
    API-->>Client: HTTP response (200/201/204)
    end

    rect rgba(120,160,220,0.06)
    Note over Client,Bus: Campaign send flow moved to Messenger
    Client->>API: sendMessage()
    API->>Bus: dispatch(SyncCampaignProcessorMessage)
    Bus->>Handler: handle()
    Handler->>Manager: process()
    Manager->>DB: persist
    DB-->>Handler: ack
    Handler-->>API: result (sync)
    API-->>Client: HTTP response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • CampaignController: verify semantics after replacing direct processor calls with MessageBus dispatch and ensure response shape/error handling unchanged.
  • SubscriberService removal & SubscriberController refactor: confirm behavior preserved and tests updated accordingly.
  • Widespread EntityManager injection and added flush() calls across many controllers/services: check for transactional correctness and duplicate/missing flushes.
  • New controllers: BounceRegexController, BlacklistController, SubscribePageController — verify authentication, validation, OpenAPI annotations, and normalizer usage.
  • Namespace moves to Service\Manager: ensure all import paths updated and no stale references remain.
  • Tests and fixtures changes (new fixtures, altered constructors): validate fixture load order and constructor signature compatibility.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.16% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Dev' is vague and non-descriptive, using a generic term that doesn't convey the actual changes being merged. Replace 'Dev' with a descriptive title that summarizes the main changes, such as 'Add bounce regex, blacklist, and subscribe page management features' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/Subscription/Controller/SubscriberAttributeValueController.php (2)

344-361: Critical bug: GET endpoint deletes data instead of reading it.

This method has multiple critical issues:

  1. Data corruption: Lines 354-355 delete the attribute and flush the deletion, but this is a GET endpoint that should only read data. Every call to this endpoint will permanently delete the attribute being fetched.

  2. Type mismatch: Line 346 incorrectly types $subscriber as SubscriberAttributeDefinition when it should be Subscriber.

Apply this diff to fix both issues:

 public function getAttributeDefinition(
     Request $request,
-    #[MapEntity(mapping: ['subscriberId' => 'id'])] ?SubscriberAttributeDefinition $subscriber,
+    #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber,
     #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $definition,
 ): JsonResponse {
     $this->requireAuthentication($request);
     if (!$definition || !$subscriber) {
         throw $this->createNotFoundException('Subscriber attribute not found.');
     }
     $attribute = $this->attributeManager->getSubscriberAttribute($subscriber->getId(), $definition->getId());
-    $this->attributeManager->delete($attribute);
-    $this->entityManager->flush();
+    if ($attribute === null) {
+        throw $this->createNotFoundException('Subscriber attribute not found.');
+    }

     return $this->json(
         $this->normalizer->normalize($attribute),
         Response::HTTP_OK
     );
 }

122-129: Add flush call after createOrUpdate to persist changes.

The createOrUpdate method on line 122–126 is missing $this->entityManager->flush() required to persist the attribute. This is inconsistent with the delete operation (line 198) and with the identical createOrUpdate call in AdminAttributeValueController (line 129), which includes the flush. Per the PR objective to move flush calls to controllers, add the flush after line 126.

$attributeDefinition = $this->attributeManager->createOrUpdate(
    subscriber:$subscriber,
    definition: $definition,
    value: $request->toArray()['value'] ?? null
);
$this->entityManager->flush();
$json = $this->normalizer->normalize($attributeDefinition, 'json');
src/Messaging/Controller/ListMessageController.php (2)

326-343: Missing EntityManager flush after disassociation.

The disassociateMessageFromList method removes an association but doesn't call $this->entityManager->flush() to persist the change, unlike associateMessageWithList at line 267. This inconsistency could result in data loss if the application terminates before an implicit flush occurs.

Apply this diff to ensure persistence:

         $this->listMessageManager->removeAssociation($message, $subscriberList);
+        $this->entityManager->flush();
 
         return $this->json(null, Response::HTTP_NO_CONTENT);

390-402: Missing EntityManager flush after removing all associations.

The removeAllListAssociationsForMessage method removes all list associations but doesn't call $this->entityManager->flush() to persist the changes, unlike associateMessageWithList at line 267. This inconsistency could result in data loss.

Apply this diff to ensure persistence:

         $this->listMessageManager->removeAllListAssociationsForMessage($message);
+        $this->entityManager->flush();
 
         return $this->json(null, Response::HTTP_NO_CONTENT);
src/Identity/Controller/AdminAttributeValueController.php (2)

345-365: Critical: GET endpoint deletes data.

The getAttributeDefinition method is mapped to a GET route but calls delete() on line 358. GET requests must be idempotent and should never mutate state. This will cause data loss when clients retrieve an attribute.

Remove the delete call:

     $attribute = $this->attributeManager->getAdminAttribute(
         adminId: $admin->getId(),
         attributeDefinitionId: $definition->getId()
     );
-    $this->attributeManager->delete($attribute);
-    $this->entityManager->flush();

     return $this->json(
         $this->normalizer->normalize($attribute),
         Response::HTTP_OK
     );

347-347: Critical: Wrong type hint for $admin parameter.

Line 347 maps the adminId route parameter to AdminAttributeDefinition, but it should map to Administrator. This will cause entity resolution to fail.

Fix the type hint:

-    #[MapEntity(mapping: ['adminId' => 'id'])] ?AdminAttributeDefinition $admin,
+    #[MapEntity(mapping: ['adminId' => 'id'])] ?Administrator $admin,
🧹 Nitpick comments (7)
src/Subscription/Controller/SubscriberAttributeDefinitionController.php (1)

84-96: Consider explicit transaction management for better atomicity.

Currently, flush occurs before normalization completes. If normalization or response generation fails, the database changes are already committed. For stronger guarantees, consider wrapping each operation in an explicit transaction:

$this->entityManager->beginTransaction();
try {
    $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto());
    $this->entityManager->flush();
    $json = $this->normalizer->normalize($attributeDefinition, 'json');
    $this->entityManager->commit();
    return $this->json($json, Response::HTTP_CREATED);
} catch (\Throwable $e) {
    $this->entityManager->rollback();
    throw $e;
}

Alternatively, consider using a Doctrine event listener or Symfony middleware to flush only after successful request completion.

Also applies to: 144-164, 206-219

tests/Integration/Subscription/Controller/SubscriberControllerTest.php (1)

75-76: Remove unnecessary blank lines.

The extra blank line doesn't add value and should be removed to maintain consistent spacing between test methods.

src/Identity/Controller/AdminAttributeDefinitionController.php (1)

93-95: Remove extra blank line.

The blank line at line 95 is unnecessary and should be removed for consistency.

src/Subscription/Request/SubscribePageRequest.php (1)

12-14: Consider renaming title to clarify its email constraint.

The field name title combined with #[Assert\Email] validation is semantically confusing. Consider renaming to email or another name that clearly indicates the field expects an email address.

Apply this diff to improve clarity:

-    #[Assert\NotBlank]
-    #[Assert\Email]
-    public string $title;
+    #[Assert\NotBlank]
+    #[Assert\Email]
+    public string $email;
src/Subscription/Serializer/SubscribePageNormalizer.php (1)

23-25: Consider consistency with other normalizers.

This normalizer returns an empty array for non-SubscribePage objects, whereas AdministratorNormalizer throws an InvalidArgumentException. While both approaches work, consider aligning error handling for consistency unless there's a specific reason for the difference.

tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1)

35-58: Prefer conventional while loop.

The do-while(true) with conditional break is less idiomatic than a standard while loop. Consider refactoring for clarity.

Apply this diff:

-        do {
-            $data = fgetcsv($handle);
-            if ($data === false) {
-                break;
-            }
+        while (($data = fgetcsv($handle)) !== false) {
             $row = array_combine($headers, $data);
             // ... rest of loop body
-        } while (true);
+        }
src/Subscription/Serializer/UserBlacklistNormalizer.php (1)

30-30: Use DateTimeInterface::ATOM constant for clarity.

The date format string 'Y-m-d\TH:i:sP' is equivalent to DateTimeInterface::ATOM. Using the constant improves readability and aligns with the pattern in AdministratorNormalizer.

Apply this diff:

-            'added' => $object->getAdded()?->format('Y-m-d\TH:i:sP'),
+            'added' => $object->getAdded()?->format(DateTimeInterface::ATOM),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca5387 and 58ff110.

⛔ Files ignored due to path filters (2)
  • tests/Integration/Messaging/Fixtures/Message.csv is excluded by !**/*.csv
  • tests/Integration/Subscription/Fixtures/SubscribePage.csv is excluded by !**/*.csv
📒 Files selected for processing (48)
  • composer.json (3 hunks)
  • config/services/managers.yml (1 hunks)
  • config/services/messenger_handlers.yml (1 hunks)
  • config/services/normalizers.yml (1 hunks)
  • src/Identity/Controller/AdminAttributeDefinitionController.php (5 hunks)
  • src/Identity/Controller/AdminAttributeValueController.php (5 hunks)
  • src/Identity/Controller/AdministratorController.php (5 hunks)
  • src/Identity/Controller/PasswordResetController.php (5 hunks)
  • src/Identity/Controller/SessionController.php (4 hunks)
  • src/Messaging/Controller/BounceRegexController.php (1 hunks)
  • src/Messaging/Controller/CampaignController.php (7 hunks)
  • src/Messaging/Controller/ListMessageController.php (3 hunks)
  • src/Messaging/Controller/TemplateController.php (4 hunks)
  • src/Messaging/OpenApi/SwaggerSchemasResponse.php (1 hunks)
  • src/Messaging/Request/CreateBounceRegexRequest.php (1 hunks)
  • src/Messaging/Request/Message/MessageMetadataRequest.php (1 hunks)
  • src/Messaging/Serializer/BounceRegexNormalizer.php (1 hunks)
  • src/Messaging/Serializer/MessageNormalizer.php (1 hunks)
  • src/Messaging/Service/CampaignService.php (3 hunks)
  • src/Subscription/Controller/BlacklistController.php (1 hunks)
  • src/Subscription/Controller/SubscribePageController.php (1 hunks)
  • src/Subscription/Controller/SubscriberAttributeDefinitionController.php (5 hunks)
  • src/Subscription/Controller/SubscriberAttributeValueController.php (4 hunks)
  • src/Subscription/Controller/SubscriberController.php (8 hunks)
  • src/Subscription/Controller/SubscriberImportController.php (2 hunks)
  • src/Subscription/Controller/SubscriberListController.php (4 hunks)
  • src/Subscription/Controller/SubscriptionController.php (4 hunks)
  • src/Subscription/OpenApi/SwaggerSchemasResponse.php (1 hunks)
  • src/Subscription/Request/AddToBlacklistRequest.php (1 hunks)
  • src/Subscription/Request/SubscribePageDataRequest.php (1 hunks)
  • src/Subscription/Request/SubscribePageRequest.php (1 hunks)
  • src/Subscription/Serializer/SubscribePageNormalizer.php (1 hunks)
  • src/Subscription/Serializer/UserBlacklistNormalizer.php (1 hunks)
  • src/Subscription/Service/SubscriberService.php (0 hunks)
  • tests/Integration/Messaging/Controller/BounceRegexControllerTest.php (1 hunks)
  • tests/Integration/Messaging/Controller/CampaignControllerTest.php (1 hunks)
  • tests/Integration/Messaging/Fixtures/MessageFixture.php (1 hunks)
  • tests/Integration/Subscription/Controller/BlacklistControllerTest.php (1 hunks)
  • tests/Integration/Subscription/Controller/SubscribePageControllerTest.php (1 hunks)
  • tests/Integration/Subscription/Controller/SubscriberControllerTest.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1 hunks)
  • tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (1 hunks)
  • tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php (1 hunks)
  • tests/Unit/Messaging/Serializer/MessageNormalizerTest.php (2 hunks)
  • tests/Unit/Messaging/Service/CampaignServiceTest.php (2 hunks)
  • tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php (1 hunks)
  • tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php (1 hunks)
  • tests/Unit/Subscription/Service/SubscriberServiceTest.php (0 hunks)
💤 Files with no reviewable changes (2)
  • src/Subscription/Service/SubscriberService.php
  • tests/Unit/Subscription/Service/SubscriberServiceTest.php
🧰 Additional context used
🧬 Code graph analysis (22)
src/Subscription/Request/SubscribePageDataRequest.php (1)
src/Subscription/Request/SubscribePageRequest.php (1)
  • getDto (19-22)
tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php (1)
src/Messaging/Serializer/BounceRegexNormalizer.php (3)
  • BounceRegexNormalizer (10-41)
  • supportsNormalization (37-40)
  • normalize (15-32)
src/Subscription/Request/SubscribePageRequest.php (1)
src/Subscription/Request/SubscribePageDataRequest.php (1)
  • getDto (17-20)
src/Subscription/Serializer/UserBlacklistNormalizer.php (1)
src/Subscription/Controller/BlacklistController.php (1)
  • __construct (30-41)
src/Identity/Controller/AdminAttributeDefinitionController.php (1)
src/Common/Service/Provider/PaginatedDataProvider.php (1)
  • PaginatedDataProvider (17-56)
src/Subscription/Controller/SubscriberAttributeDefinitionController.php (1)
src/Common/Service/Provider/PaginatedDataProvider.php (1)
  • PaginatedDataProvider (17-56)
src/Subscription/Controller/SubscribePageController.php (5)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-156)
src/Common/Controller/BaseController.php (2)
  • BaseController (15-41)
  • requireAuthentication (28-40)
src/Subscription/Request/SubscribePageDataRequest.php (1)
  • SubscribePageDataRequest (10-21)
src/Subscription/Request/SubscribePageRequest.php (1)
  • SubscribePageRequest (10-23)
src/Subscription/Serializer/SubscribePageNormalizer.php (3)
  • SubscribePageNormalizer (11-42)
  • __construct (13-16)
  • normalize (21-33)
src/Subscription/Controller/BlacklistController.php (3)
src/Common/Controller/BaseController.php (2)
  • BaseController (15-41)
  • requireAuthentication (28-40)
src/Subscription/Request/AddToBlacklistRequest.php (1)
  • AddToBlacklistRequest (10-23)
src/Subscription/Serializer/UserBlacklistNormalizer.php (3)
  • UserBlacklistNormalizer (11-42)
  • __construct (13-15)
  • normalize (20-33)
tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php (1)
src/Subscription/Serializer/UserBlacklistNormalizer.php (3)
  • UserBlacklistNormalizer (11-42)
  • supportsNormalization (38-41)
  • normalize (20-33)
src/Subscription/Controller/SubscriberAttributeValueController.php (1)
src/Common/Service/Provider/PaginatedDataProvider.php (1)
  • PaginatedDataProvider (17-56)
tests/Integration/Messaging/Controller/BounceRegexControllerTest.php (1)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • AdministratorFixture (14-59)
src/Identity/Controller/AdministratorController.php (1)
src/Common/Service/Provider/PaginatedDataProvider.php (1)
  • PaginatedDataProvider (17-56)
src/Subscription/Controller/SubscriberListController.php (3)
src/Subscription/Controller/SubscriberController.php (1)
  • __construct (35-45)
src/Subscription/Controller/SubscriptionController.php (1)
  • __construct (34-45)
src/Common/Service/Provider/PaginatedDataProvider.php (1)
  • PaginatedDataProvider (17-56)
src/Subscription/Controller/SubscriptionController.php (3)
src/Identity/Controller/AdminAttributeValueController.php (1)
  • __construct (33-46)
src/Subscription/Controller/SubscriberController.php (1)
  • __construct (35-45)
src/Subscription/Controller/SubscriberListController.php (1)
  • __construct (38-51)
src/Messaging/Controller/BounceRegexController.php (4)
src/Messaging/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-140)
src/Common/Controller/BaseController.php (2)
  • BaseController (15-41)
  • requireAuthentication (28-40)
src/Messaging/Request/CreateBounceRegexRequest.php (1)
  • CreateBounceRegexRequest (10-42)
src/Messaging/Serializer/BounceRegexNormalizer.php (2)
  • BounceRegexNormalizer (10-41)
  • normalize (15-32)
src/Messaging/Controller/CampaignController.php (1)
src/Messaging/Service/CampaignService.php (4)
  • __construct (23-29)
  • CampaignService (21-93)
  • createMessage (47-56)
  • updateMessage (58-78)
tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php (2)
src/Identity/Serializer/AdministratorNormalizer.php (1)
  • AdministratorNormalizer (12-41)
src/Subscription/Serializer/SubscribePageNormalizer.php (3)
  • SubscribePageNormalizer (11-42)
  • supportsNormalization (38-41)
  • normalize (21-33)
tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (1)
src/Messaging/Request/CreateBounceRegexRequest.php (2)
  • CreateBounceRegexRequest (10-42)
  • getDto (31-41)
src/Subscription/Serializer/SubscribePageNormalizer.php (2)
src/Identity/Serializer/AdministratorNormalizer.php (1)
  • AdministratorNormalizer (12-41)
src/Subscription/Controller/SubscribePageController.php (1)
  • __construct (27-35)
tests/Integration/Subscription/Controller/SubscribePageControllerTest.php (2)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • AdministratorFixture (14-59)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1)
  • SubscribePageFixture (14-62)
src/Identity/Controller/AdminAttributeValueController.php (4)
src/Identity/Controller/AdminAttributeDefinitionController.php (1)
  • __construct (31-43)
src/Subscription/Controller/SubscriberAttributeValueController.php (1)
  • __construct (32-44)
src/Subscription/Controller/SubscriberListController.php (1)
  • __construct (38-51)
src/Common/Service/Provider/PaginatedDataProvider.php (1)
  • PaginatedDataProvider (17-56)
src/Subscription/Controller/SubscriberController.php (3)
src/Common/Controller/BaseController.php (2)
  • BaseController (15-41)
  • requireAuthentication (28-40)
src/Subscription/Service/SubscriberHistoryService.php (2)
  • SubscriberHistoryService (18-53)
  • getSubscriberHistory (26-52)
src/Subscription/Controller/SubscriberImportController.php (2)
  • Route (22-158)
  • Route (36-157)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
🔇 Additional comments (45)
src/Subscription/Controller/SubscriberImportController.php (1)

10-10: LGTM!

The import statement is correctly added to support the new exception handling below.

tests/Integration/Messaging/Fixtures/MessageFixture.php (2)

61-99: Excellent refactoring to named parameters.

The migration from positional to named constructor arguments significantly improves readability and maintainability across all domain model instantiations (MessageSchedule, MessageMetadata, MessageContent, MessageOptions, and Message).


69-69: Verify MessageStatus enum cases are accessible and contain the CSV value.

The fixture code loads a MessageStatus enum value from CSV using MessageStatus::from($row['status']) at line 69. The CSV file contains a single status value: 0. While I can confirm MessageStatus is a backed enum from the external phplist/core package, I cannot access the enum definition in the sandbox environment to verify that 0 maps to a valid enum case.

Please manually confirm:

  1. The MessageStatus enum in phplist/core contains a case with backing value 0
  2. Or update the CSV to use a valid status value if 0 is not supported

If the value is invalid, MessageStatus::from() will throw a ValueError at runtime.

src/Subscription/Controller/SubscriberAttributeDefinitionController.php (5)

7-7: LGTM!

The import is correctly added to support the EntityManager dependency.


31-43: LGTM!

The EntityManager dependency is correctly injected using constructor property promotion with readonly for immutability.


144-164: LGTM!

The explicit flush after the update operation correctly persists changes before normalization and response generation, consistent with the PR's architectural change.


206-219: LGTM!

The explicit flush after the delete operation correctly persists the removal before the response is sent.


84-96: Manual verification required: AttributeDefinitionManager class not found in codebase.

The AttributeDefinitionManager class referenced in the controller cannot be located via code search. While the controller's dependency injection clearly expects this class, its definition is not accessible. This may indicate the class is new in this PR, generated, or located outside the searchable codebase.

Without access to the create() method implementation, I cannot verify whether it internally calls flush(). Please confirm:

  • The manager class exists and its create() method does not independently call $this->entityManager->flush()
src/Subscription/Controller/SubscriberAttributeValueController.php (2)

32-44: LGTM! EntityManager dependency properly injected.

The addition of EntityManagerInterface as a constructor dependency follows best practices and supports the explicit flush pattern being introduced throughout controllers.


197-198: LGTM! Explicit flush ensures deletion is persisted.

The flush call after delete ensures the deletion is immediately persisted to the database, which is appropriate for this operation.

src/Identity/Controller/AdministratorController.php (1)

7-7: LGTM! Consistent persistence pattern.

The EntityManagerInterface injection and flush calls after create, update, and delete operations ensure immediate persistence and align with the broader pattern applied across controllers in this PR.

Also applies to: 39-40, 154-154, 261-261, 310-310

src/Identity/Controller/SessionController.php (1)

7-7: LGTM! Consistent persistence for session operations.

The EntityManagerInterface injection and flush calls after session creation and deletion ensure changes are immediately persisted, consistent with the broader pattern in this PR.

Also applies to: 38-38, 101-101, 169-169

src/Subscription/Request/SubscribePageDataRequest.php (1)

1-21: LGTM! Clean DTO implementation.

The request class follows the established pattern with appropriate validation constraints and typed properties.

src/Identity/Controller/PasswordResetController.php (2)

7-7: Consistent persistence pattern for password reset operations.

The EntityManagerInterface injection and flush calls after token generation and password updates ensure changes are immediately persisted, consistent with the broader pattern in this PR.

Also applies to: 33-33, 79-79, 176-176


117-126: Verify whether validation modifies state in the external phplist/core dependency.

The validatePasswordResetToken method is from the external phplist/core package and its implementation cannot be inspected in this repository. Without confirming whether this method modifies entity state (e.g., marking tokens as validated, updating timestamps), the flush at line 123 cannot be verified as necessary. Check the phplist/core source or documentation to confirm whether this validation method has side effects requiring persistence.

src/Identity/Controller/AdminAttributeDefinitionController.php (1)

7-7: LGTM! Consistent persistence pattern.

The EntityManagerInterface injection and flush calls after create, update, and delete operations ensure immediate persistence and align with the broader pattern applied across controllers in this PR.

Also applies to: 36-37, 94-94, 163-163, 219-219

src/Identity/Controller/AdminAttributeValueController.php (3)

33-46: LGTM: EntityManager injection follows project-wide pattern.

The EntityManager injection and initialization are consistent with the broader refactoring across controllers in this PR.


124-133: LGTM: Explicit flush after create/update.

The flush call correctly persists the attribute value changes immediately after the createOrUpdate operation.


196-204: LGTM: Explicit flush after delete.

The flush call correctly persists the deletion immediately.

src/Subscription/Controller/SubscriberListController.php (3)

38-51: LGTM: EntityManager injection follows project pattern.

The EntityManager dependency injection is consistent with the broader refactoring across controllers in this PR.


229-233: LGTM: Explicit flush after delete.

The flush call ensures the list deletion is immediately persisted.


280-284: LGTM: Explicit flush after create.

The flush call ensures the newly created list is immediately persisted.

src/Messaging/Request/Message/MessageMetadataRequest.php (1)

16-24: LGTM: Status string converted to enum.

The change correctly converts the string status to a MessageStatus enum using from(). The @SuppressWarnings annotation appropriately handles the PHPMD static access warning for enum conversion.

src/Messaging/Serializer/MessageNormalizer.php (1)

42-42: LGTM: Enum serialized as scalar value.

The change correctly serializes the MessageStatus enum as its scalar value rather than the enum object, which is appropriate for JSON output.

tests/Unit/Messaging/Serializer/MessageNormalizerTest.php (2)

49-49: LGTM: Test updated for enum-based status.

The test correctly constructs MessageMetadata with the MessageStatus::Draft enum, aligning with the enum-based status handling in the production code.


83-83: LGTM: Assertion validates enum value serialization.

The assertion correctly validates that the status is serialized as the enum's scalar value.

tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (1)

1-46: LGTM: Comprehensive unit tests for CreateBounceRegexRequest.

The tests appropriately validate both explicit field population and default values in the DTO extraction.

src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)

145-153: LGTM: SubscribePage schema added.

The OpenAPI schema definition for SubscribePage is well-structured and includes appropriate property types and references.

tests/Integration/Messaging/Controller/CampaignControllerTest.php (1)

97-106: Verify that Message ID 2 is in a sendable state.

The MessageFixture loads from Message.csv, which contains Message ID 2 with status submitted. However, the MessageStatus enum definition could not be located in the repository to confirm whether submitted is a valid sendable status. Review the MessageStatus enum (likely in the core package) and ensure that submitted is an acceptable status for the send operation, or update the fixture if a different status is required.

tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php (1)

1-60: LGTM!

The unit tests are well-structured and provide comprehensive coverage of the BounceRegexNormalizer behavior, including positive cases, type checking, and error handling.

tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php (1)

1-71: LGTM!

The test properly validates the normalizer's behavior, including correct delegation to AdministratorNormalizer for nested owner data.

src/Subscription/Request/AddToBlacklistRequest.php (1)

1-23: LGTM!

The DTO is well-defined with appropriate validation constraints for adding entries to the blacklist.

src/Messaging/OpenApi/SwaggerSchemasResponse.php (1)

123-137: LGTM!

The OpenAPI schema definition for BounceRegex is complete and correctly mirrors the normalizer output structure.

config/services/normalizers.yml (1)

97-107: LGTM!

The new normalizer services are correctly registered following the established pattern used by existing normalizers.

src/Subscription/Controller/SubscriptionController.php (2)

7-7: LGTM!

EntityManager injection follows proper dependency injection patterns and aligns with the broader refactoring across controllers in this PR.

Also applies to: 32-32, 39-39, 44-44


134-134: LGTM!

The explicit flush calls after mutations ensure proper persistence and align with the systematic approach across this PR to manage transaction boundaries in controllers.

Also applies to: 201-201

config/services/messenger_handlers.yml (1)

1-5: LGTM!

The message handler service registration follows Symfony best practices and correctly configures the handler for the messaging workflow.

src/Subscription/Request/SubscribePageRequest.php (1)

16-17: LGTM!

The active field is properly defined with appropriate type validation.

src/Messaging/Serializer/BounceRegexNormalizer.php (1)

10-41: LGTM!

The normalizer follows the established pattern consistently, with proper type checking and comprehensive field mapping.

src/Messaging/Controller/TemplateController.php (2)

265-266: LGTM!

Good refactor to store the created template before flushing. This ensures the normalized response reflects the actual persisted entity.


325-325: LGTM!

The EntityManager flush call after deletion is consistent with the PR's objective to move persistence control to controllers.

tests/Unit/Messaging/Service/CampaignServiceTest.php (1)

40-45: LGTM!

The test setup correctly reflects the updated CampaignService constructor. Using named arguments improves readability and makes the parameter mapping explicit.

tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php (1)

16-56: LGTM!

Comprehensive test coverage validating normalization behavior, type support checks, and edge cases. The tests properly mock dependencies and verify interactions.

src/Subscription/Serializer/UserBlacklistNormalizer.php (1)

26-26: No issues found. The manager call is necessary.

The verification confirms that UserBlacklist only exposes getEmail() and getAdded() methods (visible in the test mocks). The getBlacklistReason() call to the manager is not redundant—it's the intended design pattern where the manager provides business logic for fetching the reason, while the object provides only its direct attributes.

src/Subscription/Serializer/SubscribePageNormalizer.php (1)

31-31: Verify whether SubscribePage.getOwner() can return null in the phplist/core domain model.

The concern is valid: AdministratorNormalizer::normalize() throws InvalidArgumentException for non-Administrator objects (including null). However, the review comment cannot be definitively resolved without confirming the nullability of the getOwner() return type in the external phplist/core package.

Evidence supporting the concern:

  • No null safety check at line 31 before passing result to adminNormalizer->normalize()
  • Test suite lacks a null-owner test case
  • Similar pattern in AdminAttributeValueNormalizer (line 28) also lacks null handling
  • Codebase shows awareness of nullable fields elsewhere (e.g., getValue() ?? ...)

If getOwner() can be null, add a null check or use a ternary:

'owner' => $object->getOwner() ? $this->adminNormalizer->normalize($object->getOwner()) : null,

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58ff110 and 6d0fbeb.

📒 Files selected for processing (4)
  • tests/Integration/Identity/Controller/SessionControllerTest.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorFixture.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
🔇 Additional comments (4)
tests/Integration/Identity/Controller/SessionControllerTest.php (1)

103-103: Confirm missing test coverage for valid user with invalid password scenario.

The change shifted this test from "valid user with wrong password" to "non-existent user with wrong password." Based on the test file, the "valid username + invalid password" scenario is not tested anywhere else—all other uses of 'john.doe' pair it with the valid password 'Bazinga!'.

This removes coverage for an important authentication edge case. Verify if this was intentional, or add a dedicated test for the "valid user + incorrect password" scenario to maintain security test coverage.

tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)

32-33: LGTM! Good idempotency pattern for fixtures.

The fixture now checks for existing Administrator entities before creating new ones, making it safe to run multiple times. This is a solid approach for test fixtures.

Also applies to: 41-45

tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (2)

45-46: LGTM! Good defensive practice.

The deterministic login name pattern prevents fixture conflicts and makes the fixture more robust for repeated loads or when combined with other fixtures.


51-51: Constructor injection successfully implemented across the codebase.

Verification confirms that all instantiations of AdministratorToken have been updated to use the new constructor signature. The only direct instantiation found (line 51 in the fixture) correctly passes the $admin parameter, and no legacy setAdministrator() calls remain in the codebase.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
tests/Integration/Subscription/Controller/SubscribePageControllerTest.php (1)

82-89: Load auth fixtures before using authenticatedJsonRequest.

This test uses an authenticated request but doesn’t load AdministratorTokenFixture, causing intermittent 403s when state is clean. Please load the token fixture.

-        $this->loadFixtures([AdministratorFixture::class, SubscribePageFixture::class]);
+        $this->loadFixtures([
+            AdministratorFixture::class,
+            AdministratorTokenFixture::class,
+            SubscribePageFixture::class,
+        ]);
🧹 Nitpick comments (10)
src/Messaging/Request/CreateBounceRegexRequest.php (4)

19-21: Ensure listOrder is non-negative.

Guard against negative ordering with a dedicated constraint. Also, nice job resolving the earlier nullable/default mismatch.

     #[Assert\Type('integer')]
+    #[Assert\PositiveOrZero]
     public int $listOrder = 0;

22-23: Constrain admin identifier to a positive integer.

If this maps to an existing admin id, restrict to > 0 while still allowing null.

     #[Assert\Type('integer')]
+    #[Assert\Positive]
     public ?int $admin = null;

31-41: Consider omitting nulls from the DTO payload.

If consumers interpret “missing” vs “explicit null” differently, filter out nulls to reduce ambiguity.

-        return [
+        $data = [
             'regex' => $this->regex,
             'action' => $this->action,
             'listOrder' => $this->listOrder,
             'admin' => $this->admin,
             'comment' => $this->comment,
             'status' => $this->status,
-        ];
+        ];
+        return array_filter($data, static fn($v) => $v !== null);

If explicit nulls have meaning for your create flow, keep current behavior.


12-29: Minor: Type constraints may be redundant with typed props.

Given strict_types and typed properties, some Assert\Type checks are defensive duplication. Keep them if you rely on serializer casting/validation for incoming scalars; otherwise, consider dropping to reduce noise.

src/Subscription/Controller/SubscribePageController.php (4)

98-106: OpenAPI: add explicit type: 'object' to request bodies.

Swagger-php infers objects from properties, but being explicit improves tooling.

-        requestBody: new OA\RequestBody(
+        requestBody: new OA\RequestBody(
             required: true,
-            content: new OA\JsonContent(
+            content: new OA\JsonContent(
+                type: 'object',
                 properties: [
                     new OA\Property(property: 'title', type: 'string'),
                     new OA\Property(property: 'active', type: 'boolean', nullable: true),
                 ]
             )
         ),
@@
-            content: new OA\JsonContent(
+            content: new OA\JsonContent(
+                type: 'object',
                 properties: [
-                    new OA\Property(property: 'title', type: 'string', nullable: true),
+                    new OA\Property(property: 'title', type: 'string', nullable: true),
                     new OA\Property(property: 'active', type: 'boolean', nullable: true),
                 ]
             )
         ),

Also applies to: 158-164


390-400: OpenAPI: include type: 'object' for the 200 response schema.

Minor clarity improvement for generators.

-            new OA\Response(
+            new OA\Response(
                 response: 200,
                 description: 'Success',
-                content: new OA\JsonContent(
+                content: new OA\JsonContent(
+                    type: 'object',
                     properties: [
                         new OA\Property(property: 'id', type: 'integer'),
                         new OA\Property(property: 'name', type: 'string'),
                         new OA\Property(property: 'data', type: 'string', nullable: true),
                     ],
-                    type: 'object'
                 )
             ),

279-279: 204 should not include a body.

$this->json(null, 204) can emit a null body. Prefer an empty response for 204.

-        return $this->json(null, Response::HTTP_NO_CONTENT);
+        return new Response('', Response::HTTP_NO_CONTENT);

151-164: Update DTO vs. docs: are fields optional on update?

updatePage() uses the same DTO as create; OpenAPI marks title/active nullable (optional), but the DTO likely requires title. Either split DTOs (Create vs Update with optional fields) or mark title non-nullable in the update docs.

Option A (recommended): introduce Update DTO with optional fields and use it in updatePage():

-use PhpList\RestBundle\Subscription\Request\SubscribePageRequest;
+use PhpList\RestBundle\Subscription\Request\SubscribePageRequest;
+use PhpList\RestBundle\Subscription\Request\SubscribePageUpdateRequest;
@@
-        /** @var SubscribePageRequest $updateRequest */
-        $updateRequest = $this->validator->validate($request, SubscribePageRequest::class);
+        /** @var SubscribePageUpdateRequest $updateRequest */
+        $updateRequest = $this->validator->validate($request, SubscribePageUpdateRequest::class);

New DTO (src/Subscription/Request/SubscribePageUpdateRequest.php):

<?php
declare(strict_types=1);

namespace PhpList\RestBundle\Subscription\Request;

use Symfony\Component\Validator\Constraints as Assert;

final class SubscribePageUpdateRequest implements RequestInterface
{
    #[Assert\Type('string')]
    #[Assert\Length(min: 1, max: 255)]
    public ?string $title = null;

    #[Assert\Type('bool')]
    public ?bool $active = null;

    public function getDto(): self
    {
        return $this;
    }
}

Also applies to: 200-225

tests/Integration/Subscription/Controller/SubscribePageControllerTest.php (2)

83-86: Align test payloads with intended title semantics.

If you switch title to free text (recommended), update payloads to human-friendly titles.

-        $payload = json_encode([
-            'title' => '[email protected]',
-            'active' => true,
-        ], JSON_THROW_ON_ERROR);
+        $payload = json_encode([
+            'title' => 'Welcome Page',
+            'active' => true,
+        ], JSON_THROW_ON_ERROR);
@@
-        $payload = json_encode([
-            'title' => '[email protected]',
-            'active' => false,
-        ], JSON_THROW_ON_ERROR);
+        $payload = json_encode([
+            'title' => 'Updated Welcome Page',
+            'active' => false,
+        ], JSON_THROW_ON_ERROR);

Also applies to: 125-127


240-253: Consider adding a create/update validation test for missing/invalid title.

You already cover set-data 422. Add analogous tests for create/update to lock the contract. I can draft them if helpful.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0fbeb and c8621bf.

📒 Files selected for processing (14)
  • .coderabbit.yaml (1 hunks)
  • src/Messaging/Request/CreateBounceRegexRequest.php (1 hunks)
  • src/Subscription/Controller/SubscribePageController.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorFixture.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1 hunks)
  • tests/Integration/Messaging/Fixtures/MessageFixture.php (2 hunks)
  • tests/Integration/Messaging/Fixtures/TemplateFixture.php (1 hunks)
  • tests/Integration/Subscription/Controller/SubscribePageControllerTest.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberFixture.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (3 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriptionFixture.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Integration/Subscription/Fixtures/SubscribePageFixture.php
🧰 Additional context used
🧬 Code graph analysis (3)
tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (2)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • AdministratorFixture (14-70)
tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php (1)
  • getDependencies (68-74)
src/Subscription/Controller/SubscribePageController.php (5)
src/Subscription/OpenApi/SwaggerSchemasResponse.php (1)
  • OA (9-156)
src/Common/Controller/BaseController.php (2)
  • BaseController (15-41)
  • requireAuthentication (28-40)
src/Subscription/Request/SubscribePageDataRequest.php (1)
  • SubscribePageDataRequest (10-21)
src/Subscription/Request/SubscribePageRequest.php (1)
  • SubscribePageRequest (10-23)
src/Subscription/Serializer/SubscribePageNormalizer.php (3)
  • SubscribePageNormalizer (11-42)
  • __construct (13-16)
  • normalize (21-33)
tests/Integration/Subscription/Controller/SubscribePageControllerTest.php (3)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • AdministratorFixture (14-70)
tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1)
  • AdministratorTokenFixture (15-65)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1)
  • SubscribePageFixture (14-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
🔇 Additional comments (17)
.coderabbit.yaml (1)

1-16: Configuration looks solid—schema-compliant YAML for the CodeRabbit reviewer.

All properties conform to the CodeRabbit configuration schema. The chill review profile with high-level summaries and collapsed walkthroughs makes sense for the PR guidance to keep things relaxed and focused.

One tiny note: Line 3 sets tone_instructions: "chill", which is valid but pretty minimal. Since you've already specified reviews.profile: "chill" (line 5), the tone_instructions could either be removed or expanded into a more descriptive guideline if you want to override or augment the predefined profile behavior. The schema allows up to 250 characters, so you could add something like "Be relaxed and supportive in review tone, focusing on high-impact items." if that aligns with your intent.

Also, line 15 base_branches: [".*"] will enable auto-review on all branches—confirm that's intentional.

tests/Integration/Messaging/Fixtures/MessageFixture.php (2)

48-50: Nice defensive check for CSV integrity.

Catching the array_combine failure here prevents cryptic errors downstream if the CSV data is malformed. The error message is clear and helpful.


65-103: Solid refactor to named arguments.

Switching from positional to named arguments across all the Message-related constructors makes this code much easier to read and maintain. The explicit MessageStatus::from() conversion is also a nice touch. No concerns here.

tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (3)

40-42: Nice defensive check!

Catching the array_combine failure upfront with a clear error message will save debugging time if the CSV ever gets malformed.


48-49: Good call on the deterministic naming.

The admin_{id} pattern makes fixture data predictable and the comment clearly explains why. Works well for avoiding collisions.


54-54: Constructor injection FTW.

Passing $admin directly to the constructor enforces the relationship at creation time—cleaner than relying on a separate setter call. Assuming other usages across the PR got the same treatment.

tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php (1)

43-45: Good defensive validation.

The header/data length mismatch check prevents downstream errors when CSV rows are malformed. This pattern is consistently applied across all fixtures in the PR.

tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)

32-32: Excellent addition of idempotency.

Fetching the repository and checking for existing administrators prevents duplicate creation when fixtures are re-run. Combined with the CSV validation, this makes the fixture more robust.

Also applies to: 40-48

tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (2)

9-9: Past concern resolved with fixture dependencies.

Implementing DependentFixtureInterface and declaring AdministratorFixture as a dependency ensures administrators exist with correct IDs before this fixture runs. This addresses the ID conflation concern from the previous review.

Also applies to: 14-14, 17-17, 80-85


36-36: Nice optimization with admin caching.

The adminsById cache reduces repeated repository lookups for the same owner. The deterministic owner_X login name pattern and CSV validation both improve robustness.

Also applies to: 44-59

tests/Integration/Messaging/Fixtures/TemplateFixture.php (1)

38-40: Consistent validation pattern.

The CSV validation aligns with the pattern applied across all fixtures in this PR, preventing malformed data from causing issues downstream.

tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php (1)

38-40: Solid validation check.

This validation ensures CSV integrity and matches the approach used throughout the fixture improvements in this PR.

tests/Integration/Subscription/Fixtures/SubscriptionFixture.php (1)

44-46: Robust CSV validation.

The header/data mismatch check is a good defensive measure, consistent with the improvements across all fixtures in this PR.

tests/Integration/Subscription/Fixtures/SubscriberFixture.php (1)

39-41: Clean validation implementation.

This completes the consistent CSV validation pattern applied across all fixtures, improving error handling for malformed test data.

src/Messaging/Request/CreateBounceRegexRequest.php (1)

16-18: Verify allowed values for action and status before adding Choice constraints.

The suggestion to use Assert\Choice is sound—similar request classes in the codebase (e.g., SubscribersExportRequest) use it. However, tests show only single examples: action='delete' and status='active'. Without comprehensive coverage of all valid values, or access to the core domain model (PhpList\Core\Domain\Messaging\Model\BounceRegex), the exact choice list cannot be determined from this repository alone.

Before implementing, confirm with your domain or documentation:

  • All valid action values (e.g., 'delete', 'bounce', 'report', etc.)
  • All valid status values (e.g., 'active', 'inactive', etc.)

Then apply:

#[Assert\Choice(choices: ['delete', ...], message: 'Invalid action.')]
public ?string $action = null;

#[Assert\Choice(choices: ['active', ...], message: 'Invalid status.')]
public ?string $status = null;
src/Subscription/Controller/SubscribePageController.php (2)

145-149: Nice: flush moved into controllers after mutating ops.

Flush after create/update/delete/setData aligns with the PR goal and fixes the previously flagged persistence gap. Looks good.

Also applies to: 222-224, 276-279, 430-433


216-221: Verify owner reassignment behavior is intentional.

The owner: $admin parameter is always passed unconditionally to the manager's updatePage() method (line 220). While the SubscribePageManager is imported from an external package (PhpList\Core\Domain\Subscription\Service\Manager), confirm this unconditional reassignment aligns with business logic—if ownership should remain unchanged during updates, pass null or conditionally set the owner instead.

Comment on lines +12 to +15
#[Assert\NotBlank]
#[Assert\Type('string')]
public string $regex;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Trim and validate the regex input.

  • Trim blanks so " " fails validation.
  • Add a compile check to avoid invalid PCRE patterns causing runtime warnings later.

Apply:

-    #[Assert\NotBlank]
+    #[Assert\NotBlank(normalizer: 'trim')]
     #[Assert\Type('string')]
     public string $regex;

Add this class-level callback (and import ExecutionContextInterface) to ensure the pattern compiles:

use Symfony\Component\Validator\Context\ExecutionContextInterface;

#[Assert\Callback('validateRegexPattern')]
public function validateRegexPattern(ExecutionContextInterface $context): void
{
    if (!isset($this->regex)) {
        return;
    }
    set_error_handler(static function () { /* suppress preg warnings */ });
    $ok = @preg_match($this->regex, '');
    restore_error_handler();

    if ($ok === false) {
        $context->buildViolation('Invalid regular expression pattern.')
            ->atPath('regex')
            ->addViolation();
    }
}
🤖 Prompt for AI Agents
In src/Messaging/Request/CreateBounceRegexRequest.php around lines 12-15, the
regex field currently only has NotBlank/Type assertions but needs trimming and a
compile-time check; add the Assert\Callback attribute on the class and import
Symfony\Component\Validator\Context\ExecutionContextInterface and
Assert\Callback, then implement a validateRegexPattern(ExecutionContextInterface
$context): void method that trims $this->regex (so values like "   " become
empty and trigger NotBlank), returns if regex is not set/empty, and otherwise
suppresses preg warnings (set_error_handler/restore_error_handler or @) and
attempts preg_match on the pattern; if preg_match returns false add a violation
on 'regex' with message "Invalid regular expression pattern." to prevent invalid
PCRE from reaching runtime.

Comment on lines +101 to +105
properties: [
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'active', type: 'boolean', nullable: true),
]
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Contract mismatch: title validated as email vs docs/examples as human title.

Docs show a human-readable title, but the DTO (SubscribePageRequest) uses Assert\Email; tests send email-like strings to pass validation. Pick one and align DTO, docs, and tests. Recommended: treat title as free text.

Proposed change (in src/Subscription/Request/SubscribePageRequest.php):

 class SubscribePageRequest implements RequestInterface
 {
-    #[Assert\NotBlank]
-    #[Assert\Email]
-    public string $title;
+    #[Assert\NotBlank]
+    #[Assert\Type('string')]
+    #[Assert\Length(min: 1, max: 255)]
+    public string $title;

If you adopt this, also update the tests to use human titles (see test comments), and keep the OpenAPI examples as-is.

Also applies to: 159-163

🤖 Prompt for AI Agents
In src/Subscription/Controller/SubscribePageController.php around lines 101-105
and 159-163, the OpenAPI/docs expect a human-readable page title but the DTO
validation currently treats `title` as an email; change the DTO
(src/Subscription/Request/SubscribePageRequest.php) to validate `title` as free
text (e.g., NotBlank and/or Length constraints) instead of Assert\Email, keep
the OA\Property type as string, and update any tests to send human-readable
titles (not email strings) so docs, DTO and tests are consistent.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1)

40-40: Add validation for array_combine return value.

The array_combine() call can return false when header and data arrays have mismatched lengths, which would break the next line. This was flagged previously but the fix doesn't appear in the current code.

Apply this diff to handle the case:

             $row = array_combine($headers, $data);
+            if ($row === false) {
+                throw new RuntimeException('Malformed CSV data: header/data length mismatch.');
+            }
🧹 Nitpick comments (2)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (2)

51-57: Consider adding an idempotency check.

Unlike AdministratorFixture, this doesn't check if a SubscribePage with the given ID already exists. If the fixture loads multiple times in a test suite, you might get duplicate key errors or duplicate data. Adding a check would make this more robust.

Here's a pattern from AdministratorFixture you could adapt:

+            $pageRepository = $manager->getRepository(SubscribePage::class);
+
+            // Make fixture idempotent: skip if page with this ID already exists
+            $existing = $pageRepository->find($row['id']);
+            if ($existing instanceof SubscribePage) {
+                continue;
+            }
+
             $page = new SubscribePage();
             $this->setSubjectId($page, (int)$row['id']);
             $page->setTitle($row['title']);
             $page->setActive(filter_var($row['active'], FILTER_VALIDATE_BOOLEAN));
             $page->setOwner($owner);

             $manager->persist($page);

42-49: Cache administrator lookups for better performance.

If multiple pages share the same owner, you'll hit the database for each row. SubscriberListFixture caches admins in an $adminsById array to avoid repeated queries. Not critical for small test fixtures, but nice for consistency.

Pattern from SubscriberListFixture:

         $adminRepository = $manager->getRepository(Administrator::class);
+        $adminsById = [];

         do {
             $data = fgetcsv($handle);
             if ($data === false) {
                 break;
             }
             $row = array_combine($headers, $data);

-            $owner = $adminRepository->find($row['owner']);
+            $ownerId = (int)$row['owner'];
+            $owner = $adminsById[$ownerId] ?? $adminRepository->find($ownerId);
             if ($owner === null) {
                 $owner = new Administrator();
-                $this->setSubjectId($owner, (int)$row['owner']);
+                $this->setSubjectId($owner, $ownerId);
                 $owner->setLoginName('subscribepage_owner_' . $ownerId);
                 $owner->setSuperUser(true);
                 $owner->setDisabled(false);
                 $manager->persist($owner);
+                $adminsById[$ownerId] = $owner;
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8621bf and 502415a.

📒 Files selected for processing (6)
  • .github/PULL_REQUEST_TEMPLATE.md (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorFixture.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1 hunks)
  • tests/Integration/Messaging/Fixtures/MessageFixture.php (2 hunks)
  • tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/Integration/Identity/Fixtures/AdministratorFixture.php
  • tests/Integration/Subscription/Fixtures/SubscriberListFixture.php
  • tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php
🧰 Additional context used
🧬 Code graph analysis (1)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (3)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • load (17-66)
tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1)
  • load (18-61)
tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (1)
  • load (20-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
🔇 Additional comments (3)
.github/PULL_REQUEST_TEMPLATE.md (1)

1-1: PR template looks stripped down—was this intentional?

Line 1 contains just a CodeRabbit AI prompt (@coderabbitai summary), whereas PR templates typically provide structured guidance for contributors (e.g., description, type of change, testing, checklists). The drastic simplification suggests this might be an incomplete or accidental change.

If the goal is to simplify the template, consider whether minimal guidance is still helpful. Otherwise, this should probably be restored to provide contributor clarity.

tests/Integration/Messaging/Fixtures/MessageFixture.php (2)

62-100: Nice refactor to named arguments!

The shift from positional to named constructor arguments across all value objects and the Message entity makes the fixture data mapping explicit and easier to maintain. This reduces the risk of subtle bugs from argument reordering.


49-50: Verify null safety for admin and template lookups.

If the CSV references IDs that don't exist, find() returns null, which gets passed to the Message constructor. The Message class is imported from an external package (PhpList\Core), so I couldn't inspect its constructor signature to verify whether it accepts nullable values. Ensure the constructor can handle null values or that your fixture CSV always contains valid IDs for existing admin and template entities.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1)

42-49: Set loginName before persisting fallback Administrator.

When we create a fallback PhpList\Core\Domain\Identity\Model\Administrator, we still never assign a loginName. Doctrine will try to persist a row with a null login, tripping the not-null/unique constraint and blowing up the fixture load. Please mirror the other fixtures and give it a deterministic value before persist().

             if ($owner === null) {
                 $owner = new Administrator();
                 $this->setSubjectId($owner, (int)$row['owner']);
+                $owner->setLoginName('subscribepage_owner_' . $row['owner']);
                 $owner->setSuperUser(true);
                 $owner->setDisabled(false);
                 $manager->persist($owner);
             }
🧹 Nitpick comments (5)
src/Subscription/Request/SubscribePageRequest.php (1)

16-17: Clarify the nullable design of the active property.

The ?bool type hint with a default value of false is inconsistent. With the default, this property will never actually be null unless explicitly set to null, making the nullable type hint unnecessary.

Consider one of these approaches:

  • If active should always have a boolean value when not provided: public bool $active = false;
  • If active should be distinguishable as "not set" vs "set to false": public ?bool $active = null;

Apply this diff if active should always be a boolean:

-    #[Assert\Type(type: 'bool')]
-    public ?bool $active = false;
+    #[Assert\Type(type: 'bool')]
+    public bool $active = false;

Or apply this diff if you need to distinguish "not provided" from "false":

     #[Assert\Type(type: 'bool')]
-    public ?bool $active = false;
+    public ?bool $active = null;
src/Messaging/Request/CreateTemplateRequest.php (1)

15-17: Good addition of the trim normalizer.

Trimming whitespace on the title field before validation is a sensible improvement.

You might consider whether the content field (line 19) should also use normalizer: 'trim' for consistency, though preserving whitespace in content might be intentional.

src/Messaging/Request/CreateBounceRegexRequest.php (2)

13-13: Consider adding trim normalizer to prevent whitespace-only regex.

A past review suggested adding normalizer: 'trim' to the NotBlank assertion. Without it, whitespace-only strings like " " would pass validation even though they're not meaningful regex patterns.

Apply this diff:

-    #[Assert\NotBlank]
+    #[Assert\NotBlank(normalizer: 'trim')]

Based on past review comments.


50-52: Optional: Error handler doesn't need to return true.

The error handler here suppresses warnings, and returning true works fine, but conventionally these handlers just return nothing or have an empty body. It's a minor style preference.

If you prefer, you could simplify to:

-        set_error_handler(static function () {
-            return true;
-        });
+        set_error_handler(static function () {
+            // Suppress preg_match warnings
+        });
tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (1)

49-61: Optional: Remove redundant assertion.

The meaningful assertion here is the mock's expects($this->never()) expectation on line 55. Line 60's $this->assertTrue(true) is a no-op that doesn't add value.

You could remove line 60 entirely, or if you prefer an explicit success marker, use:

-        // if no exception and no violation calls, the test passes
-        $this->assertTrue(true);
+        // Test passes if buildViolation was never called
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 502415a and bd5bf95.

📒 Files selected for processing (13)
  • src/Identity/Request/CreateSessionRequest.php (1 hunks)
  • src/Identity/Request/UpdateAttributeDefinitionRequest.php (1 hunks)
  • src/Identity/Request/ValidateTokenRequest.php (1 hunks)
  • src/Messaging/Request/CreateBounceRegexRequest.php (1 hunks)
  • src/Messaging/Request/CreateTemplateRequest.php (1 hunks)
  • src/Subscription/Controller/SubscriberImportController.php (2 hunks)
  • src/Subscription/Request/SubscribePageRequest.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorFixture.php (1 hunks)
  • tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1 hunks)
  • tests/Integration/Messaging/Fixtures/MessageFixture.php (2 hunks)
  • tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (1 hunks)
  • tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (4 hunks)
  • tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Integration/Messaging/Fixtures/MessageFixture.php
  • tests/Integration/Identity/Fixtures/AdministratorFixture.php
🧰 Additional context used
🧬 Code graph analysis (5)
tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (1)
src/Messaging/Request/CreateBounceRegexRequest.php (2)
  • CreateBounceRegexRequest (11-63)
  • getDto (32-42)
src/Subscription/Controller/SubscriberImportController.php (1)
tests/Helpers/DummyAnalyticsController.php (1)
  • json (12-15)
src/Subscription/Request/SubscribePageRequest.php (3)
src/Identity/Request/CreateSessionRequest.php (1)
  • getDto (20-23)
src/Identity/Request/ValidateTokenRequest.php (1)
  • getDto (16-19)
src/Subscription/Request/SubscribePageDataRequest.php (1)
  • getDto (17-20)
tests/Integration/Subscription/Fixtures/SubscribePageFixture.php (2)
tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php (1)
  • load (18-61)
tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (1)
  • load (20-75)
tests/Integration/Subscription/Fixtures/SubscriberListFixture.php (2)
tests/Integration/Identity/Fixtures/AdministratorFixture.php (1)
  • AdministratorFixture (14-67)
tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php (1)
  • getDependencies (65-71)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist oldest [Build, Test]
  • GitHub Check: phpList Base Dist on PHP 8.1, with dist latest [Build, Test]
🔇 Additional comments (13)
src/Subscription/Controller/SubscriberImportController.php (2)

10-10: LGTM – Import looks good.

The addition of the CouldNotReadUploadedFileException import is necessary for the specific catch block below. Nice and clean.


148-151: Nice fix – specific exception handling added.

Good addition of the specific catch for CouldNotReadUploadedFileException before the generic Exception. This provides clearer error handling and a more informative response.

Also noting that the spacing issue from the previous review has been addressed – the concatenation now correctly includes a space after "file."

src/Subscription/Request/SubscribePageRequest.php (1)

19-22: LGTM!

The getDto() implementation follows the established pattern from other request classes in the codebase.

src/Identity/Request/UpdateAttributeDefinitionRequest.php (1)

13-13: Good addition of trim normalizer.

Trimming whitespace before validation prevents whitespace-only names from passing validation. This is a sensible improvement.

src/Identity/Request/CreateSessionRequest.php (1)

12-18: Trimming credentials improves validation.

Adding the trim normalizer to both loginName and password prevents whitespace-only values from passing validation, which is standard practice for authentication fields.

src/Identity/Request/ValidateTokenRequest.php (1)

12-12: Trimming tokens is sensible.

Adding the trim normalizer prevents accidental whitespace from causing token validation failures. Good consistency with the other request DTOs.

src/Messaging/Request/CreateTemplateRequest.php (3)

9-9: Nice cleanup!

Adding the import statement improves readability and avoids repeating the fully-qualified namespace throughout the file.


19-21: LGTM!

Using the imported constraint instead of the fully-qualified name is cleaner and consistent with the new import.


23-24: Looks good!

The refactor to use the imported constraint is consistent with the rest of the file. The constraint on the nullable text field should work fine since Symfony validators skip null values by default.

src/Messaging/Request/CreateBounceRegexRequest.php (1)

32-42: LGTM!

The DTO mapping is straightforward and includes all fields with their current values.

tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php (3)

14-32: LGTM!

Comprehensive test that validates all DTO fields are correctly mapped.


34-47: LGTM!

Good test coverage of default values for optional fields.


63-79: LGTM!

Thorough test that validates the violation is properly built and added for invalid regex patterns.

@TatevikGr TatevikGr merged commit f4c6993 into main Nov 8, 2025
9 checks passed
@TatevikGr TatevikGr deleted the dev branch November 8, 2025 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants