Skip to content

Conversation

@TatevikGr
Copy link
Contributor

@TatevikGr TatevikGr commented Dec 7, 2025

Summary by CodeRabbit

  • New Features

    • Campaigns are suspended when composed emails exceed a configurable maximum size.
    • Per-recipient personalization and message precaching improve rendering and delivery.
    • Message metadata is persisted and HTML content is sanitized before sending.
  • Chores

    • Added configuration for maximum mail size and an HTML sanitizer dependency.
    • Downgraded a linter rule severity and removed a docstring-coverage override.
  • Tests

    • Tests updated to reflect personalization, precaching, and size-checking flows.

✏️ Tip: You can customize this high-level summary in your review settings.

Thanks for contributing to phpList!

@coderabbitai
Copy link

coderabbitai bot commented Dec 7, 2025

📝 Walkthrough

Walkthrough

Adds message-size enforcement and content precaching: new config params and services (HTMLPurifier, MessageDataManager, MessagePrecacheService), a MessageSizeLimitExceededException, size checks in campaign handler that can suspend campaigns, and API changes to use MessageContent and subscriber-aware personalization.

Changes

Cohort / File(s) Summary
Configuration & DI
config/PhpCodeSniffer/ruleset.xml, config/parameters.yml.dist, config/services.yml, config/services/managers.yml, config/services/repositories.yml, config/services/messenger.yml
Added messaging.max_mail_size param (env-sourced). Registered services: HTMLPurifier_Config, HTMLPurifier, MessageDataManager, MessageDataRepository, and configured CampaignProcessorMessageHandler with $maxMailSize. PHPCS rule Generic.Commenting.Todo now includes <severity>0</severity>.
Dependencies
composer.json
Added runtime dependency ezyang/htmlpurifier (^4.19).
Exception & Repository
src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php, src/Domain/Messaging/Repository/MessageDataRepository.php
New MessageSizeLimitExceededException (stores actual/max sizes). MessageDataRepository gains findByIdAndName(int,string): ?MessageData.
Message data manager
src/Domain/Messaging/Service/Manager/MessageDataManager.php
New MessageDataManager to normalize/sanitize and persist message metadata via setMessageData, using HTMLPurifier, repository and EntityManager.
Precache & caching
src/Domain/Messaging/Service/MessagePrecacheService.php
New MessagePrecacheService that builds/caches base MessageContent (subject, html, text, footer), supports remote [URL:...] token fetching, and returns a MessageContent object.
Handler & processing logic
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php, src/Domain/Messaging/Service/MessageProcessingPreparator.php, src/Domain/Messaging/Service/RateLimitedCampaignMailer.php
CampaignProcessorMessageHandler: added deps (CacheInterface, EventLogManager, MessageDataManager, MessagePrecacheService), maxMailSize param, checkMessageSizeOrSuspendCampaign and calculateEmailSize logic; throws MessageSizeLimitExceededException and suspends on violation. MessageProcessingPreparator: injects UserPersonalizer, processMessageLinks now accepts (int $campaignId, MessageContent $content, Subscriber $subscriber) and personalizes HTML/footer; link endpoint constant renamed. RateLimitedCampaignMailer::composeEmail now accepts a MessageContent third arg.
Analytics link tracking
src/Domain/Analytics/Service/LinkTrackService.php
extractAndSaveLinks signature changed to (MessageContent $content, int $userId, ?int $messageId = null) and now operates on passed content and messageId.
Subscription repo & manager
src/Domain/Subscription/Repository/SubscriberListRepository.php, src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php
Added SubscriberListRepository::getAllActive(). Removed a phpcs:ignore Generic.Commenting.Todo comment.
Tests
tests/Unit/.../CampaignProcessorMessageHandlerTest.php, tests/Unit/.../MessageProcessingPreparatorTest.php, tests/Unit/.../RateLimitedCampaignMailerTest.php, tests/Unit/.../LinkTrackServiceTest.php
Tests updated for new constructor deps and signatures: use MessageContent and Subscriber where applicable, pass $maxMailSize in handler ctor, and adapt mocks to precache, cache and message-data flows.
Tooling & CI
.coderabbit.yaml
Removed explicit disable for docstring_coverage (reverted to default).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Subscriber
    participant Prep as MessageProcessingPreparator
    participant Precache as MessagePrecacheService
    participant Handler as CampaignProcessorMessageHandler
    participant Mailer as RateLimitedCampaignMailer
    participant Cache as CacheInterface
    participant DataMgr as MessageDataManager
    participant EventLog as EventLogManager

    Subscriber->>Prep: processMessageLinks(campaignId, content, subscriber)
    Prep->>Prep: personalize content (UserPersonalizer)
    Prep->>Precache: getOrCacheBaseMessageContent(campaign)
    Precache-->>Prep: base MessageContent
    Prep-->>Handler: prepared MessageContent

    Handler->>Mailer: composeEmail(campaign, subscriber, content)
    Mailer-->>Handler: Email (rendered)

    Handler->>Cache: get(sizeKey)
    alt cache miss
        Handler->>Handler: calculateEmailSize(Email)
    else cache hit
        Handler-->>Handler: use cached size
    end

    Handler->>Handler: compare size vs maxMailSize
    alt size > maxMailSize
        Handler->>DataMgr: setMessageData(campaign,"size",size)
        Handler->>EventLog: record suspension/warning
        Handler-->>Handler: throw MessageSizeLimitExceededException
    else within limit
        Handler->>Cache: set(sizeKey,size)
        Handler->>EventLog: optional size info
        Handler->>Mailer: proceed sending / ack
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas to focus:
    • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php: size calculation, cache key/TTL, exception handling, campaign suspension side effects.
    • src/Domain/Messaging/Service/Manager/MessageDataManager.php: sanitization rules, serialization, entity lifecycle.
    • src/Domain/Messaging/Service/MessagePrecacheService.php: remote fetch handling, timeouts, cache shape and reconstruction.
    • API surface changes: ensure all call sites updated for MessageContent and changed signatures (Preparator, LinkTrackService, RateLimitedCampaignMailer).
    • DI/config: messenger/service wiring and new HTMLPurifier configuration; composer dependency.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/check max mail size' clearly summarizes the main change: adding functionality to check and enforce maximum email size limits for campaigns.
✨ 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 feat/check-max-mail-size

📜 Recent 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 525741a and cad6ebb.

📒 Files selected for processing (3)
  • src/Domain/Identity/Service/PermissionChecker.php (1 hunks)
  • src/Domain/Messaging/Service/MessagePrecacheService.php (1 hunks)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (7 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Messaging/Service/MessagePrecacheService.php
  • src/Domain/Identity/Service/PermissionChecker.php
🧬 Code graph analysis (1)
tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (1)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (2)
  • MessageProcessingPreparator (17-107)
  • processMessageLinks (64-95)
⏰ 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). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (3)
src/Domain/Identity/Service/PermissionChecker.php (1)

73-76: [rewritten comment]
[classification tag]

tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (2)

35-41: Nice addition of the personalizer mock.

The UserPersonalizer mock is properly configured to return the original text, which keeps the link replacement assertions clean and focused.


196-196: Mock expectation now matches the 3-argument signature.

The test correctly expects extractAndSaveLinks($content, 123, 1) with the campaign ID, aligning with the implementation changes.


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

@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: 5

🧹 Nitpick comments (5)
src/Domain/Subscription/Repository/SubscriberListRepository.php (1)

49-55: Implementation looks good; consider adding explicit element type to the return.

The query is clean and stays read-only, which fits the domain-layer repository constraints. To help static analysis and keep consistency with getListsByMessage(), you could document the element type of the returned array:

-    public function getAllActive(): array
-    {
+    /** @return SubscriberList[] */
+    public function getAllActive(): array
+    {
         return $this->createQueryBuilder('l')
             ->where('l.active = true')
             ->getQuery()
             ->getResult();
     }
src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1)

37-37: TODO noted for HTML2Text functionality.

This is a reasonable reminder. The text message is currently set directly from getTextMessage(), but if you need auto-conversion from HTML, that'd be the place to wire it in.

Want me to open an issue to track implementing HTML2Text conversion here?

src/Domain/Messaging/Service/Manager/MessageDataManager.php (1)

27-29: Potential side effect from session_name() call.

Calling session_name() when no session is active may return an empty string or the default name, which could lead to unexpected behavior. If the session isn't started, this check might not work as intended.

Consider guarding the check or documenting the expected session state:

-        if ($name === 'PHPSESSID' || $name === session_name()) {
+        if ($name === 'PHPSESSID' || (session_status() === PHP_SESSION_ACTIVE && $name === session_name())) {
             return;
         }
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (2)

185-194: Minor inefficiency: redundant cache read.

When the cache key doesn't exist, you calculate and cache $size (line 186-188), but then immediately read it back from the cache on line 191. You could just reuse the local $size variable.

+        $size = null;
         if (!$this->cache->has($cacheKey)) {
             $size = $this->calculateEmailSize($email);
             $this->messageDataManager->setMessageData($campaign, $sizeName, $size);
             $this->cache->set($cacheKey, $size);
         }
 
-        $size = $this->cache->get($cacheKey);
+        $size ??= $this->cache->get($cacheKey);

217-226: Email size calculation approach.

Using $email->toIterable() to compute the size is reasonable. The TODO comment on line 224 seems outdated since setMessageData is already called in checkMessageSizeOrSuspendCampaign.

         foreach ($email->toIterable() as $line) {
             $size += strlen($line);
         }
-        // todo: setMessageData($messageid, $sizename, $mail->mailsize);
         return $size;
📜 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 158fac6 and fbd4922.

📒 Files selected for processing (15)
  • config/PhpCodeSniffer/ruleset.xml (1 hunks)
  • config/parameters.yml.dist (1 hunks)
  • config/services.yml (1 hunks)
  • config/services/managers.yml (1 hunks)
  • config/services/repositories.yml (1 hunks)
  • src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php (1 hunks)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (5 hunks)
  • src/Domain/Messaging/Repository/MessageDataRepository.php (1 hunks)
  • src/Domain/Messaging/Service/Manager/MessageDataManager.php (1 hunks)
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php (4 hunks)
  • src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1 hunks)
  • src/Domain/Subscription/Repository/SubscriberListRepository.php (1 hunks)
  • src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php (0 hunks)
  • tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (6 hunks)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (8 hunks)
💤 Files with no reviewable changes (1)
  • src/Domain/Subscription/Service/Manager/SubscriberAttributeManager.php
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Subscription/Repository/SubscriberListRepository.php
  • src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php
  • src/Domain/Messaging/Repository/MessageDataRepository.php
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Messaging/Service/RateLimitedCampaignMailer.php
  • src/Domain/Messaging/Service/Manager/MessageDataManager.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (5)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (1)
src/Domain/Analytics/Service/LinkTrackService.php (3)
  • LinkTrackService (13-98)
  • isExtractAndSaveLinksApplicable (30-33)
  • extractAndSaveLinks (41-80)
tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (1)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (1)
  • MessageDataManager (11-111)
tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (1)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (2)
  • MessageProcessingPreparator (16-104)
  • processMessageLinks (63-92)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (3)
src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php (2)
  • MessageSizeLimitExceededException (9-31)
  • __construct (11-20)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (3)
  • MessageDataManager (11-111)
  • __construct (13-16)
  • setMessageData (25-67)
src/Domain/Messaging/Message/SubscriberConfirmationMessage.php (1)
  • hasHtmlEmail (39-42)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (2)
src/Domain/Messaging/Repository/MessageDataRepository.php (2)
  • MessageDataRepository (12-20)
  • findByIdAndName (16-19)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)
  • __construct (42-60)
⏰ 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). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (15)
config/PhpCodeSniffer/ruleset.xml (1)

33-35: Why disable the TODO comment rule?

Disabling the Generic.Commenting.Todo rule (by setting severity to 0) will suppress PHP CodeSniffer warnings for TODO comments throughout the codebase. This could allow incomplete work or deferred issues to slip through code review undetected.

Before merging, please clarify the rationale:

  • Are there legitimate TODOs in the newly added messaging/size-limit code that require this suppression?
  • If so, consider documenting why these TODOs cannot be addressed before merge.
  • If not, this rule should remain enabled to catch unfinished work.
config/services.yml (1)

54-57: Service wiring looks good.

The CampaignProcessorMessageHandler is properly configured with autowiring and the maxMailSize parameter injection. This aligns with the new size-limiting functionality.

config/services/repositories.yml (1)

144-147: Repository declaration follows the established pattern.

Looks consistent with the other repository definitions in this file. Good to go.

config/parameters.yml.dist (1)

90-91: Default max mail size is 200MB — verify this is intentional.

The default 209715200 bytes equals 200MB. That's pretty generous for email! Typical email size limits are much smaller (10-25MB for most providers). If this is meant to be a safety net rather than a strict limit, it's fine, but you might want to document the reasoning or consider a more conservative default.

Also, environment variables come in as strings. Make sure the handler casts this to int before size comparisons.

config/services/managers.yml (1)

114-116: Manager service follows the existing pattern.

Consistent with other manager declarations in this file. Nice and clean.

src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php (1)

9-31: Clean exception implementation.

Good use of constructor property promotion, informative error message, and getters for programmatic access to the size values. This will make debugging and logging much easier when size limits are exceeded.

tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (3)

35-41: Smart mock setup for UserPersonalizer.

Returning the input text unchanged ensures the link replacement assertions stay valid. Good thinking — this isolates the link processing behavior from personalization side effects.


132-146: Test correctly updated for Subscriber-based signature.

The switch from int $userId to Subscriber $subscriber is properly reflected here. The mock provides both getId() and getEmail() which aligns with the implementation's usage.


175-218: Integration test covers the happy path well.

The test verifies link replacement with multiple links, and the assertions for the updated endpoint format (with leading slash) match the implementation in replaceLinks().

src/Domain/Messaging/Repository/MessageDataRepository.php (1)

16-19: Read-only query method — approved.

This is a clean, side-effect-free query on the composite primary key (id + name) of the MessageData entity. No domain purity concerns.

tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (2)

64-80: Test setup properly updated for new dependencies.

The constructor invocation now includes all the new dependencies (cache, eventLogManager, messageDataManager, maxMailSize). The mocks are appropriately created and the test setup aligns with the handler's updated signature.


184-187: Method signature update correctly reflected.

The test now passes a Subscriber object to processMessageLinks instead of a plain ID, matching the updated API. Nice.

src/Domain/Messaging/Service/MessageProcessingPreparator.php (2)

18-19: Good catch on the typo fix.

Renaming LINT_TRACK_ENDPOINT to LINK_TRACK_ENDPOINT — solid correction.


63-92: Method signature update and personalization integration look good.

The switch from int $userId to Subscriber $subscriber provides better type safety and enables direct access to subscriber properties for personalization. The personalization step is cleanly integrated after link replacement.

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

40-60: Constructor and dependency injection look solid.

The new dependencies (cache, eventLogManager, messageDataManager) are appropriately injected. The maxMailSize parameter with nullable type and default fallback to 0 is a clean pattern for optional configuration.

Per coding guidelines for MessageHandlers, persistence operations are allowed here since this is the orchestration boundary.

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)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

156-163: Size check still occurs after sending the email.

The previous review flagged this: the size check on line 159 happens after $this->mailer->send($email) on line 158. This means the first oversized email for a campaign will be delivered before suspension kicks in.

If the goal is to prevent oversized emails from going out at all, move the size check before the send() call.

🧹 Nitpick comments (3)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (3)

186-195: Potential type mismatch from cache retrieval.

On line 192, $this->cache->get($cacheKey) returns mixed. While the cache was just set on line 189, if the cache implementation fails silently or has TTL issues, this could return null, leading to a comparison null <= $this->maxMailSize on line 193.

Consider using the value already computed or adding a type guard:

-        $size = $this->cache->get($cacheKey);
+        $size = (int) $this->cache->get($cacheKey);

Or reuse the computed value:

+        $size = null;
         if (!$this->cache->has($cacheKey)) {
             $size = $this->calculateEmailSize($email);
             $this->messageDataManager->setMessageData($campaign, $sizeName, $size);
             $this->cache->set($cacheKey, $size);
         }

-        $size = $this->cache->get($cacheKey);
+        $size ??= (int) $this->cache->get($cacheKey);

204-213: Consider consolidating duplicate event log entries.

Two separate eventLogManager->log() calls are made for the same suspension event. You could combine them into a single, more informative log entry:

-        $this->eventLogManager->log('send', sprintf(
-            'Message too large (%d is over %d), suspending',
-            $size,
-            $this->maxMailSize
-        ));
-
-        $this->eventLogManager->log('send', sprintf(
-            'Campaign %d suspended. Message too large',
-            $campaign->getId()
-        ));
+        $this->eventLogManager->log('send', sprintf(
+            'Campaign %d suspended: message too large (%d bytes exceeds limit of %d bytes)',
+            $campaign->getId(),
+            $size,
+            $this->maxMailSize
+        ));

218-227: Stale TODO comment.

The TODO on line 225 (// todo: setMessageData($messageid, $sizename, $mail->mailsize);) appears to reference functionality that's already implemented on line 188 in checkMessageSizeOrSuspendCampaign. Consider removing it to avoid confusion.

-        // todo: setMessageData($messageid, $sizename, $mail->mailsize);
         return $size;
📜 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 fbd4922 and d37ecc7.

📒 Files selected for processing (2)
  • .coderabbit.yaml (0 hunks)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (5 hunks)
💤 Files with no reviewable changes (1)
  • .coderabbit.yaml
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (1)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (5)
src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php (2)
  • MessageSizeLimitExceededException (9-31)
  • __construct (11-20)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (3)
  • MessageDataManager (11-111)
  • __construct (13-16)
  • setMessageData (25-67)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (3)
  • MessageProcessingPreparator (16-104)
  • __construct (21-28)
  • processMessageLinks (63-92)
src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (4)
  • RateLimitedCampaignMailer (13-51)
  • __construct (17-21)
  • composeEmail (23-40)
  • send (45-50)
src/Domain/Messaging/Message/SubscriberConfirmationMessage.php (1)
  • hasHtmlEmail (39-42)
⏰ 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). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (1)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

40-60: LGTM on constructor setup.

The property promotion pattern is clean, and the null coalescing to 0 for $maxMailSize effectively disables the size check when not configured. Nice and tidy.

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 (3)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (2)

85-88: Regex backreferences won't match as intended.

The \2 and \1 backreferences inside character classes don't work the way you'd expect in PCRE. This was flagged in a prior review and still needs attention.


101-112: Domain-layer persistence call flagged previously.

The persist() call here was already called out in a past review. Per the coding guidelines, domain-layer code should avoid direct persistence - that belongs in the orchestration boundary (MessageHandler).

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

156-166: First oversized email still sent before suspension kicks in.

This was flagged before - the size check happens after send(), so the first oversized email goes out. If that's intentional (measure actual sent size), cool - just want to confirm that's the design choice here.

🧹 Nitpick comments (4)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (1)

35-61: Consider removing commented-out code before merging.

Large commented blocks like the targetlist handling add noise. If it's needed later, it can be retrieved from version control. If there's a specific reason to keep it, maybe add a TODO issue reference.

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (2)

179-219: Size checking and suspension logic looks reasonable.

The caching prevents repeated calculations, logging provides good visibility, and the exception flow handles campaign suspension cleanly. One thought: consider whether both event log entries (lines 207-216) are necessary or could be consolidated.


40-60: Constructor is getting hefty with 16 dependencies.

Not blocking, but worth considering whether some of these could be grouped or if the handler is taking on too much. The PHPMD suppression hints at this already.

src/Domain/Messaging/Service/MessageProcessingPreparator.php (1)

78-78: TODO noted - worth tracking.

If there's other configured data that affects mail formatting, might be worth opening an issue to track this so it doesn't get lost.

📜 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 d37ecc7 and 1cefd9d.

📒 Files selected for processing (4)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (5 hunks)
  • src/Domain/Messaging/Service/Manager/MessageDataManager.php (1 hunks)
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php (4 hunks)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (8 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Messaging/Service/Manager/MessageDataManager.php
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (3)
tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (1)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (2)
  • MessageProcessingPreparator (16-104)
  • processMessageLinks (63-92)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (3)
src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php (2)
  • MessageSizeLimitExceededException (9-31)
  • __construct (11-20)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (3)
  • MessageDataManager (12-113)
  • __construct (14-18)
  • setMessageData (27-69)
src/Domain/Messaging/Message/SubscriberConfirmationMessage.php (1)
  • hasHtmlEmail (39-42)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (2)
src/Domain/Messaging/Repository/MessageDataRepository.php (2)
  • MessageDataRepository (12-20)
  • findByIdAndName (16-19)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)
  • __construct (42-60)
⏰ 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). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (8)
tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (3)

35-41: Nice mock setup for the personalization passthrough.

This callback approach ensures your link replacement assertions stay valid while the personalization dependency is satisfied. Clean and clear.


129-149: Test coverage for non-applicable link tracking looks solid.

Properly verifies that when link tracking isn't applicable, the message is returned unchanged and no downstream methods are called.


175-218: Good integration test for the link extraction and replacement flow.

The test covers the full path with multiple links, HTML content, and footer processing. Using stringContains with the updated LINK_TRACK_ENDPOINT constant is appropriate.

src/Domain/Messaging/Service/Manager/MessageDataManager.php (1)

63-69: Serialization of arrays/objects uses serialize() - watch for unserialize vulnerabilities.

When this data is later read and potentially unserialized, ensure proper handling to avoid object injection. Just something to keep in mind for the consuming code.

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

221-230: toIterable() returns the raw MIME stream chunks.

This gives you the actual serialized email size including headers and encoding overhead, which is probably what you want for mail server limits. Just noting that this will be larger than just body content size.

src/Domain/Messaging/Service/MessageProcessingPreparator.php (3)

19-19: Typo fix in constant name - nice catch.

LINT_TRACK_ENDPOINTLINK_TRACK_ENDPOINT. Much better.


96-100: Track URL construction looks correct now.

The previous double-slash issue appears to be resolved - no extra / prepended to the endpoint constant.


63-92: Clean refactor to use Subscriber object instead of raw ID.

Passing the full Subscriber enables both ID access for link tracking and email access for personalization without additional lookups. The personalization calls after link replacement make sense for the content flow.

@TatevikGr TatevikGr force-pushed the feat/check-max-mail-size branch from 1cefd9d to 6a5c209 Compare December 8, 2025 06:48
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)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

156-167: Size check should happen before send, and UserMessageStatus on size error is likely wrong

Right now the flow is:

$email = $this->mailer->composeEmail($processed, $subscriber);
$this->mailer->send($email);
$this->checkMessageSizeOrSuspendCampaign(...);

which means the first oversized email is still sent before the campaign is suspended. If the max-mail-size is intended as a hard guard, this is a functional bug: oversize detection only kicks in after successfully delivering one oversize mail.

Also, in the MessageSizeLimitExceededException catch you mark the UserMessage as Sent, which will become incorrect once the check is moved before send() (the mail won’t have been sent at all).

Concrete suggestion:

  • Run the size check before sending.
  • On MessageSizeLimitExceededException, mark the UserMessage as NotSent (or introduce a dedicated status) so reporting stays truthful.

For example:

         try {
             $email = $this->mailer->composeEmail($processed, $subscriber);
-            $this->mailer->send($email);
-            $this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());
+            $this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());
+            $this->mailer->send($email);
             $this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
         } catch (MessageSizeLimitExceededException $e) {
             // stop after the first message if size is exceeded
             $this->updateMessageStatus($campaign, MessageStatus::Suspended);
-            $this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
-
-            throw $e;
+            $this->updateUserMessageStatus($userMessage, UserMessageStatus::NotSent);
+
+            throw $e;

You may also want to revisit whether rethrowing is desired (it will surface to Messenger and potentially trigger retries/failure transport) or whether it’s enough to suspend the campaign and return normally.

🧹 Nitpick comments (4)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (1)

78-78: Track or address the TODO comment.

The TODO suggests there might be other configuration data needed for mail formatting or creation. This should be investigated to ensure complete functionality.

Would you like me to open an issue to track this TODO, or do you want to address it in this PR?

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (3)

8-29: DI changes and maxMailSize default look reasonable, with one behavioral nuance

The added dependencies (CacheInterface, EventLogManager, MessageDataManager) and the new ?int $maxMailSize property wired through the constructor are consistent with this handler’s orchestration role and keep concerns in the right layer.

One subtle behavioral point: defaulting ?int $maxMailSize to 0 (treated later as “no limit”) is fine as long as config/docs clearly state “0 or null = unlimited”. If the intent is that an unset value should fall back to some other global/default limit, you may want to handle that here instead of hard-wiring 0 as the sentinel.

Also applies to: 40-60


179-219: Size-check helper is solid; consider tightening cache handling and typing

The checkMessageSizeOrSuspendCampaign helper cleanly encapsulates the “compute once, cache per campaign & channel, log, and throw” logic. That’s a good separation of concerns for this handler.

A couple of small robustness tweaks you might consider:

  • When reading from cache, explicitly cast to int and guard against unexpected types/nulls (e.g., cache eviction between has() and get()), so comparisons against $this->maxMailSize are always numeric and intentional.

    For example, inside the method:

  •    $size = $this->cache->get($cacheKey);
    
  •    if ($size <= $this->maxMailSize) {
    
  •    $size = $this->cache->get($cacheKey);
    
  •    if (!is_int($size)) {
    
  •        $size = $this->calculateEmailSize($email);
    
  •        $this->messageDataManager->setMessageData($campaign, $sizeName, $size);
    
  •        $this->cache->set($cacheKey, $size);
    
  •    }
    
  •    if ($size <= $this->maxMailSize) {
           return;
       }
    
    
    (Or any equivalent pattern you prefer.)
    
    
  • If MessageDataManager is the canonical store for these size values, you could, in future, consider checking it first when cache is cold instead of recomputing, though that’s more of an optimization than a necessity.

Overall, behavior and logging/event log entries here look on point.


221-230: Email size calculation is fine; the TODO is now misleading

The calculateEmailSize(Email $email) implementation using foreach ($email->toIterable()) and strlen($line) is a reasonable approximation of raw message size in bytes given Symfony’s Email implementation.

The inline TODO:

// todo: setMessageData($messageid, $sizename, $mail->mailsize);

is effectively already handled by checkMessageSizeOrSuspendCampaign via MessageDataManager::setMessageData(...). Keeping this TODO around is likely to confuse future readers into thinking something is still missing. I’d recommend removing or rewording it to reflect the current design.

📜 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 1cefd9d and 6a5c209.

📒 Files selected for processing (4)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (5 hunks)
  • src/Domain/Messaging/Service/Manager/MessageDataManager.php (1 hunks)
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php (4 hunks)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Domain/Messaging/Service/Manager/MessageDataManager.php
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (1)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (1)
src/Domain/Analytics/Service/LinkTrackService.php (3)
  • LinkTrackService (13-98)
  • isExtractAndSaveLinksApplicable (30-33)
  • extractAndSaveLinks (41-80)
⏰ 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). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (6)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (5)

21-28: Nice refactor to property promotion!

The use of private readonly with constructor property promotion is clean and modern. This eliminates boilerplate while ensuring immutability of dependencies.


19-19: Good catch on the typo!

Renaming LINT_TRACK_ENDPOINT to LINK_TRACK_ENDPOINT fixes the obvious typo.


80-87: Verify the ordering of link replacement and personalization.

Personalization is performed after link replacement. If UserPersonalizer could potentially insert or modify URLs as part of personalization (e.g., replacing placeholders with URLs), those new or modified URLs won't be tracked. Confirm this ordering aligns with the intended behavior.

Additionally, consider whether error handling is needed around the personalization calls—if personalization fails, the entire processing flow could break.


98-98: Double slash issue resolved!

The track URL is now correctly constructed without the double slash issue mentioned in previous reviews. The constant is used directly without prepending an extra /.


63-63: All callers of processMessageLinks have been properly updated to pass the Subscriber object. Three test cases and one production caller (CampaignProcessorMessageHandler) all correctly pass $subscriber instead of an integer user ID. The method correctly extracts the ID via $subscriber->getId() when needed for downstream operations.

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

153-154: Subscriber-based processMessageLinks usage looks good

Switching processMessageLinks to receive the full Subscriber object rather than just the ID aligns with more flexible per-subscriber personalization. As long as the preparator’s signature has been updated correspondingly (as suggested by the PR summary/tests), this change is clean.

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: 3

Caution

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

⚠️ Outside diff range comments (2)
tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (1)

167-222: Stub precacheService and hasHtmlEmail in tests that exercise sending

A couple of subtle mocking issues will bite once these tests hit the new code paths:

  1. getOrCacheBaseMessageContent() returning null

CampaignProcessorMessageHandler::__invoke() now always does:

$messageContent = $this->precacheService->getOrCacheBaseMessageContent($campaign);
...
$this->handleEmailSending($campaign, $subscriber, $userMessage, $messageContent);

but in:

  • testInvokeWithValidSubscriberEmail
  • testInvokeWithMultipleSubscribers

$this->precacheService is not stubbed for those campaigns, so the default mock return is null. Because handleEmailSending requires Message\MessageContent for its 4th parameter, this will cause a TypeError.

You already stub precache correctly in testInvokeWithMailerException; do the same in the other tests, e.g.:

public function testInvokeWithValidSubscriberEmail(): void
{
    $campaign = $this->createMock(Message::class);
    $content = $this->createContentMock();
    $campaign->method('getContent')->willReturn($content);
+   $this->precacheService->expects($this->once())
+       ->method('getOrCacheBaseMessageContent')
+       ->with($campaign)
+       ->willReturn($content);

    ...
}

and similarly in testInvokeWithMultipleSubscribers (likely expects($this->once()) with the $campaign from createCampaignMock()).

  1. hasHtmlEmail() needs a concrete boolean

checkMessageSizeOrSuspendCampaign() is declared as:

private function checkMessageSizeOrSuspendCampaign(
    Message $campaign,
    Email $email,
    bool $hasHtmlEmail
): void

and handleEmailSending calls it as:

$this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());

The Subscriber mocks in testInvokeWithValidSubscriberEmail, testInvokeWithMailerException, and testInvokeWithMultipleSubscribers don’t stub hasHtmlEmail(), so the mock will return null. With strict_types=1, passing null into a bool parameter will also throw a TypeError.

Quick fix: stub hasHtmlEmail() to a sensible value wherever a valid subscriber email leads to sending, e.g.:

$subscriber = $this->createMock(Subscriber::class);
$subscriber->method('getEmail')->willReturn('[email protected]');
$subscriber->method('getId')->willReturn(1);
+ $subscriber->method('hasHtmlEmail')->willReturn(true);

and similarly for the other tests’ valid subscribers (text‑only cases can use false if you want to exercise that branch).

With those adjustments, the tests should align cleanly with the new handler behavior.

Also applies to: 226-275, 279-320, 325-343

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

76-82: Don’t reuse the same mutable MessageContent instance across subscribers

getOrCacheBaseMessageContent() returns a MessageContent that MessageProcessingPreparator::processMessageLinks() then mutates in place (link replacements + personalization):

$messageContent = $this->precacheService->getOrCacheBaseMessageContent($campaign);
...
foreach ($subscribers as $subscriber) {
    ...
    $this->handleEmailSending($campaign, $subscriber, $userMessage, $messageContent);
}

Because the same $messageContent instance is reused for every subscriber, later subscribers will see content that has already been link‑tracked and personalized for earlier subscribers (and link extraction will then operate on already‑tracked URLs).

Given that MessagePrecacheService is already caching a snapshot and reconstructing a fresh MessageContent on each getOrCacheBaseMessageContent() call, it’s safer to fetch a new base instance per subscriber:

-        $messageContent = $this->precacheService->getOrCacheBaseMessageContent($campaign);
-        ...
-        foreach ($subscribers as $subscriber) {
+        foreach ($subscribers as $subscriber) {
             ...
-            $this->handleEmailSending($campaign, $subscriber, $userMessage, $messageContent);
+            $precachedContent = $this->precacheService->getOrCacheBaseMessageContent($campaign);
+            $this->handleEmailSending($campaign, $subscriber, $userMessage, $precachedContent);

That way each subscriber starts from the same unpersonalized base, and per‑subscriber link tracking + personalization stay isolated.

Also applies to: 86-107, 155-162

♻️ Duplicate comments (1)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

163-174: Size check runs after send; confirm if first oversized email should be blocked or allowed

Right now handleEmailSending() does:

$email = $this->mailer->composeEmail($campaign, $subscriber, $processed);
$this->mailer->send($email);
$this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());
$this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);

and on MessageSizeLimitExceededException:

$this->updateMessageStatus($campaign, MessageStatus::Suspended);
$this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
throw $e;

So the first oversized email is still delivered, and the corresponding UserMessage is marked as Sent; only then is the campaign suspended.

If the product requirement is “never send emails larger than the configured limit”, you probably want to:

  • Run checkMessageSizeOrSuspendCampaign() before send(), and
  • Avoid marking the UserMessage as Sent in the oversize exception path.

Something along these lines:

        try {
             $email = $this->mailer->composeEmail($campaign, $subscriber, $processed);
-            $this->mailer->send($email);
-            $this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());
-            $this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
+            $this->checkMessageSizeOrSuspendCampaign($campaign, $email, $subscriber->hasHtmlEmail());
+            $this->mailer->send($email);
+            $this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
         } catch (MessageSizeLimitExceededException $e) {
-            // stop after the first message if size is exceeded
+            // stop after the first message if size is exceeded (and don’t mark as sent)
             $this->updateMessageStatus($campaign, MessageStatus::Suspended);
-            $this->updateUserMessageStatus($userMessage, UserMessageStatus::Sent);
+            $this->updateUserMessageStatus($userMessage, UserMessageStatus::NotSent);
 
             throw $e;

If, on the other hand, the intended semantics are “allow the first send, then suspend further sends once size is known”, the current implementation matches that, but it might be worth documenting that behavior explicitly given how subtle it is (and that it was previously raised in review).

Also applies to: 186-237

📜 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 4492e02 and 525741a.

📒 Files selected for processing (9)
  • src/Domain/Analytics/Service/LinkTrackService.php (2 hunks)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (8 hunks)
  • src/Domain/Messaging/Service/MessagePrecacheService.php (1 hunks)
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php (2 hunks)
  • src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (2 hunks)
  • tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php (7 hunks)
  • tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (11 hunks)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (7 hunks)
  • tests/Unit/Domain/Messaging/Service/RateLimitedCampaignMailerTest.php (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Analytics/Service/LinkTrackService.php
  • src/Domain/Messaging/Service/RateLimitedCampaignMailer.php
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php
  • src/Domain/Messaging/Service/MessagePrecacheService.php
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (7)
tests/Unit/Domain/Messaging/Service/RateLimitedCampaignMailerTest.php (1)
src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1)
  • composeEmail (23-40)
tests/Unit/Domain/Analytics/Service/LinkTrackServiceTest.php (1)
src/Domain/Analytics/Service/LinkTrackService.php (1)
  • extractAndSaveLinks (42-78)
tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (3)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (1)
  • MessageDataManager (13-96)
src/Domain/Messaging/Service/MessagePrecacheService.php (1)
  • MessagePrecacheService (10-136)
src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1)
  • RateLimitedCampaignMailer (13-51)
tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (1)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (2)
  • MessageProcessingPreparator (17-107)
  • processMessageLinks (64-95)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (2)
src/Domain/Analytics/Service/LinkTrackService.php (4)
  • __construct (19-23)
  • LinkTrackService (14-96)
  • isExtractAndSaveLinksApplicable (31-34)
  • extractAndSaveLinks (42-78)
src/Domain/Messaging/Model/Message.php (2)
  • __construct (67-87)
  • getId (89-92)
src/Domain/Messaging/Service/MessagePrecacheService.php (2)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)
  • __construct (43-62)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (1)
  • __construct (22-29)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (3)
src/Domain/Messaging/Exception/MessageSizeLimitExceededException.php (1)
  • MessageSizeLimitExceededException (9-31)
src/Domain/Messaging/Service/Manager/MessageDataManager.php (2)
  • MessageDataManager (13-96)
  • setMessageData (29-71)
src/Domain/Messaging/Service/MessagePrecacheService.php (2)
  • MessagePrecacheService (10-136)
  • getOrCacheBaseMessageContent (21-60)
⏰ 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). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (9)
src/Domain/Analytics/Service/LinkTrackService.php (1)

12-78: Refactor to MessageContent + explicit messageId looks consistent

Using MessageContent and an explicit ?int $messageId keeps the service focused on content while still enforcing the “message must have an ID” invariant. Text/footer handling and deduped persistence behavior are preserved, so this change looks good from a correctness and domain-purity standpoint.

tests/Unit/Domain/Messaging/Service/RateLimitedCampaignMailerTest.php (1)

49-57: Tests correctly adapted to new composeEmail signature

Passing $message->getContent() into composeEmail matches the new mailer API, and the existing assertions still verify subject, from/reply-to, and bodies. No issues here.

Also applies to: 73-79

src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1)

23-40: Mailer now cleanly separates headers from content

Using MessageContent for subject/text/html while still sourcing from/reply-to from the Message options matches the refactored flow and keeps composeEmail straightforward. Looks solid.

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

39-50: Tests line up with the new content‑based LinkTrackService API

Updating the tests to pass MessageContent and an explicit $messageId (including the null‑ID case) keeps coverage aligned with the refactored service. Expectations around persist calls and returned LinkTrack instances still look correct.

Also applies to: 74-79, 88-108, 116-135, 143-162, 169-179, 205-228

src/Domain/Messaging/Service/MessageProcessingPreparator.php (5)

8-8: LGTM! Clean imports and typo fix.

The new imports support the refactored method signature and personalization feature. The constant rename from LINT_TRACK_ENDPOINT to LINK_TRACK_ENDPOINT fixes an obvious typo.

Also applies to: 10-10, 12-12, 20-20


22-28: Nice refactor with property promotion.

Constructor property promotion makes the code cleaner and the readonly modifiers prevent accidental mutation. The new UserPersonalizer dependency supports the personalization feature.


64-68: Method signature refactored for per-subscriber processing.

The new signature processMessageLinks(int $campaignId, MessageContent $content, Subscriber $subscriber) is more focused and aligns with processing content per subscriber rather than per message. This is a breaking change, but appears intentional for the size-checking feature.


101-101: Previous issue resolved.

The double-slash URL issue flagged in the previous review has been correctly addressed. The track URL is now properly constructed without duplicating the leading slash.


73-73: No action needed. The parameter named $campaignId is actually the ID of a Message entity (campaigns and messages are the same concept in phpList). Passing it to extractAndSaveLinks as the messageId parameter is correct. This is a naming inconsistency without functional impact.

@TatevikGr TatevikGr merged commit 83431b1 into main Dec 10, 2025
8 checks passed
@TatevikGr TatevikGr deleted the feat/check-max-mail-size branch December 10, 2025 08:12
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