Skip to content

Fix: mailing#382

Open
TatevikGr wants to merge 2 commits intomainfrom
dev
Open

Fix: mailing#382
TatevikGr wants to merge 2 commits intomainfrom
dev

Conversation

@TatevikGr
Copy link
Copy Markdown
Contributor

@TatevikGr TatevikGr commented Apr 1, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Corrected list resolution in message permission checks.
    • Normalized boolean handling in message precaching.
  • New Features

    • Added ability to copy messages as drafts for reuse.
  • Improvements

    • Ensured message data is persisted earlier during send processing.
    • Expanded allowed message status transitions for finer workflow control.
  • Tests

    • Added and updated unit tests for message cloning and data loading.
  • Documentation

    • Strengthened type hints for message list collections.

Thanks for contributing to phpList!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 625f7022-fb1a-4207-9b80-51804e0bd4e9

📥 Commits

Reviewing files that changed from the base of the PR and between d7fa96a and ef1fabc.

📒 Files selected for processing (8)
  • src/Domain/Analytics/Service/LinkTrackService.php
  • src/Domain/Identity/Service/PermissionChecker.php
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Messaging/Model/Message.php
  • src/Domain/Messaging/Model/Message/MessageStatus.php
  • src/Domain/Messaging/Service/Manager/MessageManager.php
  • tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php
  • tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php
✅ Files skipped from review due to trivial changes (3)
  • tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php
  • src/Domain/Messaging/Model/Message.php
  • tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Identity/Service/PermissionChecker.php
  • src/Domain/Analytics/Service/LinkTrackService.php
  • src/Domain/Messaging/Service/Manager/MessageManager.php

📝 Walkthrough

Walkthrough

This PR applies constructor property promotion, fixes list relationship accessors, adds boolean casting for googleTrack, introduces MessageManager::copyAsDraftMessage with tests, updates MessageStatus transitions, adds PHPDoc generics for Message list relations, and inserts an explicit EntityManager->flush() during email sending.

Changes

Cohort / File(s) Summary
Constructor Property Promotion
src/Domain/Analytics/Service/LinkTrackService.php, src/Domain/Messaging/Service/Manager/MessageManager.php
Constructors converted to use PHP constructor property promotion with private readonly parameters, removing manual property declarations and assignments.
List Relationship Accessor Updates
src/Domain/Messaging/Service/MessageDataLoader.php, src/Domain/Identity/Service/PermissionChecker.php, tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php
Replaced usages of ListMessage::getListId()/getSubscriberList() with ListMessage::getList()->getId()/getList(), updated test mocks to return SubscriberList via getList().
Draft Message Copying
src/Domain/Messaging/Service/Manager/MessageManager.php, tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php
Added copyAsDraftMessage() to clone a Message as a Draft (copy format/content/options, new UUID, new Draft metadata) and persist via MessageRepository; unit test asserts persisted clone differs and has Draft status.
Type Casting & PHPDoc
src/Domain/Messaging/Service/MessagePrecacheService.php, src/Domain/Messaging/Model/Message.php
Cast google_track to boolean when building precache DTO; added @var/@return generics Collection<int, ListMessage> for Message::$listMessages and getListMessages().
EntityManager Flush in Email Flow
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
Inserted EntityManagerInterface::flush() immediately after link processing and before the email build/send try block.
Message Status Transitions
src/Domain/Messaging/Model/Message/MessageStatus.php
Adjusted allowed state transitions: Draft now allows Prepared and Submitted; Submitted now allows Prepared, InProcess, Suspended; Suspended transitions clarified.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix: mailing' is extremely vague and does not clearly convey what was actually fixed in the changeset. Consider a more descriptive title that captures the main changes, such as 'Refactor message handling and add draft copy functionality' or 'Update message service constructors and fix list ID resolution'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php (1)

33-37: These test updates only satisfy constructor arity.

The production delta is the new conditional finalization branch, but both constructors pass throwaway third-arg mocks, so the suite never proves that branch runs for link-creation cases and is skipped for no-op paths. If this dependency stays, keep that mock on the fixture and add those expectations.

Also applies to: 217-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php` around lines 33
- 37, The tests currently only satisfy constructor arity by passing a throwaway
third-arg mock (EntityManagerInterface) into LinkTrackService but do not assert
the new conditional finalization branch; keep the EntityManagerInterface mock as
a fixture property and in the tests that exercise link-creation set expectations
on its methods (e.g., expect persist() and flush() to be called once on the
EntityManager mock when LinkTrackService triggers finalization), and in the
tests that represent no-op paths assert those methods are never called; apply
the same expectation changes to the other affected test cases (the ones around
the 217-221 region) so the suite verifies both the branch executes for creations
and is skipped for no-ops.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Domain/Analytics/Service/LinkTrackService.php`:
- Around line 16-20: The LinkTrackService currently injects
EntityManagerInterface and calls flush() (via the $newLinksPersisted flag);
remove EntityManagerInterface from the constructor signature, delete the
$newLinksPersisted flag and the conditional flush block (the flush() call), and
leave only repository persist() calls in methods where new LinkTrack entities
are scheduled; ensure constructor only accepts LinkTrackRepository and
ParameterProvider and do not perform any transaction finalization in
LinkTrackService.

In `@src/Domain/Messaging/Service/MessageDataLoader.php`:
- Around line 148-153: The test mock is outdated: update
MessageDataLoaderTest.php so the ListMessage mock defines getList() and returns
a SubscriberList mock (or stub) whose getId() returns the expected int instead
of the old getListId(); also add a defensive null-check in
MessageDataLoader::populateTargetLists to verify $list = $listMessage->getList()
and $id = $list?->getId() are not null before assigning
$messageData['targetlist'][$id] = 1 to avoid potential null dereferences.

---

Nitpick comments:
In `@tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php`:
- Around line 33-37: The tests currently only satisfy constructor arity by
passing a throwaway third-arg mock (EntityManagerInterface) into
LinkTrackService but do not assert the new conditional finalization branch; keep
the EntityManagerInterface mock as a fixture property and in the tests that
exercise link-creation set expectations on its methods (e.g., expect persist()
and flush() to be called once on the EntityManager mock when LinkTrackService
triggers finalization), and in the tests that represent no-op paths assert those
methods are never called; apply the same expectation changes to the other
affected test cases (the ones around the 217-221 region) so the suite verifies
both the branch executes for creations and is skipped for no-ops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 38b7cafc-c8e3-4baa-9609-4d9c74b242da

📥 Commits

Reviewing files that changed from the base of the PR and between f8de2e3 and a6502a4.

📒 Files selected for processing (4)
  • src/Domain/Analytics/Service/LinkTrackService.php
  • src/Domain/Messaging/Service/MessageDataLoader.php
  • src/Domain/Messaging/Service/MessagePrecacheService.php
  • tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php (1)

29-65: Solid test for the happy path.

Good structure - uses a real Message object and verifies the clone is a distinct instance with Draft status and preserved format settings.

One thing to consider: adding assertions that modifications to the cloned message's $schedule, $content, or $options don't affect the original would help catch shallow-clone regressions. This ties into the implementation concern flagged in the main code.

Optional: Add shallow-clone isolation assertion
// After $result = $manager->copyAsDraftMessage($message);
// Verify embedded objects are independent copies (if implementation is fixed)
$this->assertNotSame($message->getSchedule(), $result->getSchedule());
$this->assertNotSame($message->getContent(), $result->getContent());
$this->assertNotSame($message->getOptions(), $result->getOptions());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php` around
lines 29 - 65, The test should also assert that the cloned message is a deep
copy for embedded objects to catch shallow-clone regressions: after calling
MessageManager::copyAsDraftMessage($message) (method copyAsDraftMessage on class
MessageManager), add assertions that $result->getSchedule(),
$result->getContent(), and $result->getOptions() are not the same instances as
the original $message->getSchedule(), $message->getContent(), and
$message->getOptions() respectively; if those assertions fail the implementation
must be updated to deep-clone those properties when creating the draft (inspect
the cloning logic in MessageManager::copyAsDraftMessage and ensure it creates
new MessageSchedule, MessageContent, and MessageOptions instances rather than
copying references).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Domain/Messaging/Service/Manager/MessageManager.php`:
- Around line 32-44: The current copyAsDraftMessage in MessageManager uses clone
which performs a shallow copy and leaves embedded objects ($schedule, $content,
$options) and identity fields ($id, $uuid) shared with the original; update the
code to avoid cloning or implement a __clone() on Message that: deep-clones
embedded value objects ($schedule, $content, $options and any DateTime inside
them), re-generates a new $uuid, and clears $id (or ensures a new entity is
constructed so $id is null) before persisting; specifically modify
MessageManager::copyAsDraftMessage to either construct a fresh Message instance
copying scalar/immutable fields and constructing new MessageMetadata(status:
Draft) and Message\MessageFormat(...) or add Message::__clone to deep-copy the
embedded properties and reset identifiers so MessageRepository->persist will
INSERT rather than conflict on unique uuid or treat as detached entity.

---

Nitpick comments:
In `@tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php`:
- Around line 29-65: The test should also assert that the cloned message is a
deep copy for embedded objects to catch shallow-clone regressions: after calling
MessageManager::copyAsDraftMessage($message) (method copyAsDraftMessage on class
MessageManager), add assertions that $result->getSchedule(),
$result->getContent(), and $result->getOptions() are not the same instances as
the original $message->getSchedule(), $message->getContent(), and
$message->getOptions() respectively; if those assertions fail the implementation
must be updated to deep-clone those properties when creating the draft (inspect
the cloning logic in MessageManager::copyAsDraftMessage and ensure it creates
new MessageSchedule, MessageContent, and MessageOptions instances rather than
copying references).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 91c2b3ae-63c5-4539-babd-c4cf12116d49

📥 Commits

Reviewing files that changed from the base of the PR and between a6502a4 and b3d36dc.

📒 Files selected for processing (2)
  • src/Domain/Messaging/Service/Manager/MessageManager.php
  • tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php

@TatevikGr TatevikGr force-pushed the dev branch 2 times, most recently from 0416fe4 to d7fa96a Compare April 1, 2026 19:11
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Domain/Identity/Service/PermissionChecker.php`:
- Line 74: The returned array from PermissionChecker:: (the expression using
$resource->getListMessages()->map(fn(ListMessage $lm) =>
$lm->getList())->toArray()) can include null because ListMessage::getList() is
nullable; update this to filter out null list relations before returning so
checkRelatedResources() and the owns(...) checks never receive nulls — e.g.,
only include non-null results of getList() (or call ->filter(...) on
getListMessages() or apply array_filter after mapping) so all elements passed
into owns(...) are valid list objects.

In `@src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php`:
- Around line 208-210: The flush() call currently occurs before the try/catch in
CampaignProcessorMessageHandler (inside the message handling method), so
exceptions from $this->entityManager->flush() bypass the catch handlers; move
the $this->entityManager->flush() invocation into the existing try block (so it
executes inside the try that contains the recovery/catch logic) to ensure any
flush exceptions are caught and the existing catch handlers and user-message
recovery path run as intended.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 290a7202-6a54-45ab-a259-5c9831081651

📥 Commits

Reviewing files that changed from the base of the PR and between b3d36dc and d7fa96a.

📒 Files selected for processing (7)
  • src/Domain/Analytics/Service/LinkTrackService.php
  • src/Domain/Identity/Service/PermissionChecker.php
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Messaging/Model/Message.php
  • src/Domain/Messaging/Service/Manager/MessageManager.php
  • tests/Unit/Domain/Messaging/Service/Manager/MessageManagerTest.php
  • tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php
✅ Files skipped from review due to trivial changes (3)
  • tests/Unit/Domain/Messaging/Service/MessageDataLoaderTest.php
  • src/Domain/Messaging/Model/Message.php
  • src/Domain/Analytics/Service/LinkTrackService.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Domain/Messaging/Service/Manager/MessageManager.php

// todo: check which one is correct
// return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
return $resource->getListMessages()->map(fn($lm) => $lm->getSubscriberList())->toArray();
return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Filter nullable list relations before ownership checks.

At Line 74, ListMessage::getList() is nullable, so this array may contain null. That can break owns(...) checks in checkRelatedResources() or cause false negatives.

Suggested patch
-            return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
+            return $resource->getListMessages()
+                ->map(fn (ListMessage $lm) => $lm->getList())
+                ->filter(fn (?SubscriberList $list) => $list !== null)
+                ->toArray();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
return $resource->getListMessages()
->map(fn (ListMessage $lm) => $lm->getList())
->filter(fn (?SubscriberList $list) => $list !== null)
->toArray();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Domain/Identity/Service/PermissionChecker.php` at line 74, The returned
array from PermissionChecker:: (the expression using
$resource->getListMessages()->map(fn(ListMessage $lm) =>
$lm->getList())->toArray()) can include null because ListMessage::getList() is
nullable; update this to filter out null list relations before returning so
checkRelatedResources() and the owns(...) checks never receive nulls — e.g.,
only include non-null results of getList() (or call ->filter(...) on
getListMessages() or apply array_filter after mapping) so all elements passed
into owns(...) are valid list objects.

Comment on lines +208 to 210
$this->entityManager->flush();

try {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Put the new flush inside the try block.

At Line 208, flush() can throw before control reaches the catch handlers, so the user-message recovery path won’t run.

Suggested patch
-        $this->entityManager->flush();
-
-        try {
+        try {
+            $this->entityManager->flush();
             $result = $this->campaignEmailBuilder->buildCampaignEmail(
                 messageId: $campaign->getId(),
                 data: $processed,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->entityManager->flush();
try {
try {
$this->entityManager->flush();
$result = $this->campaignEmailBuilder->buildCampaignEmail(
messageId: $campaign->getId(),
data: $processed,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php`
around lines 208 - 210, The flush() call currently occurs before the try/catch
in CampaignProcessorMessageHandler (inside the message handling method), so
exceptions from $this->entityManager->flush() bypass the catch handlers; move
the $this->entityManager->flush() invocation into the existing try block (so it
executes inside the try that contains the recovery/catch logic) to ensure any
flush exceptions are caught and the existing catch handlers and user-message
recovery path run as intended.

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.

2 participants