Skip to content

Dev#384

Open
TatevikGr wants to merge 5 commits into
mainfrom
dev
Open

Dev#384
TatevikGr wants to merge 5 commits into
mainfrom
dev

Conversation

@TatevikGr
Copy link
Copy Markdown
Contributor

@TatevikGr TatevikGr commented May 20, 2026

Summary by CodeRabbit

  • New Features

    • Page objects store structured page data and can be synced (create/update/delete) from supplied data.
    • Optional migration sync between site configuration and page data is available behind a feature flag.
    • Repository can fetch a single page with its data and supports paginated filtered queries.
  • Refactor

    • Manager API: strict retrieval replaced by a nullable finder; translation dependency removed; migration toggle added.
  • Tests

    • Expanded tests covering finder behavior, full sync scenarios and config migration; old retrieval/not-found tests removed.

Review Change Stack

Thanks for contributing to phpList!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds SubscribePage.data and accessors; repository adds findPageWithData and cursor pagination; new SubscribePageConfigMigrationService migrates config↔page-data; SubscribePageManager gains findPage and syncPageData and drops Translator; ConfigProvider accepts string namespaced keys; services/parameters wiring and expanded unit tests added.

Changes

Subscription page model, repository, manager, migration, config, and tests

Layer / File(s) Summary
Model: add data property and accessors
src/Domain/Subscription/Model/SubscribePage.php
Adds private array $data = [];, getData(): array, and setData(array $data): self to store SubscribePageData[].
Repository: single-page load and cursor pagination
src/Domain/Subscription/Repository/SubscriberPageRepository.php
Adds imports for FilterRequestInterface and PaginatedResult, findPageWithData(int): ?SubscribePage, getFilteredAfterId(FilterRequestInterface): PaginatedResult, and a private loadPageData(array): SubscribePage helper to hydrate joined rows.
New service: SubscribePageConfigMigrationService
src/Domain/Subscription/Service/SubscribePageConfigMigrationService.php
Introduces migration service with SUBSCRIBE_PAGE_SUFFIXES, copyToPageData(SubscribePage): bool to import config into page-data, and copyToConfig(SubscribePage, array): void to write page-data back into Config rows.
Manager: constructor, findPage, syncPageData
src/Domain/Subscription/Service/Manager/SubscribePageManager.php
Removes TranslatorInterface from constructor, adds SubscribePageConfigMigrationService and feature flag, replaces getPage(int): SubscribePage with findPage(int): ?SubscribePage (optionally invoking config→page migration), adds syncPageData(array, SubscribePage): void to diff/create/update/remove SubscribePageData and optionally copyToConfig, and scopes getPageData to private.
Config provider, parameters, and services wiring
src/Domain/Configuration/Service/Provider/ConfigProvider.php, config/parameters.yml.dist, config/services/services.yml
ConfigProvider::getValueWithNamespace now accepts `ConfigOption
Tests: SubscribePageManager findPage and syncPageData
tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php
Removes Translator/NotFoundHttpException-based getPage tests; adds tests for findPage() refetch/no-refetch/missing behavior and extensive syncPageData() create/update/delete/preserve scenarios and feature-flagged config-copy assertions.
Tests: SubscribePageConfigMigrationService
tests/Unit/Domain/Subscription/Service/SubscribePageConfigMigrationServiceTest.php
Adds unit tests covering copyToPageData() and copyToConfig() behaviors including skip-on-no-id, no-config, syncing existing/adding missing page-data, creating missing Config rows, and updating existing Config rows.
Tests: ConfigProvider namespaced string key
tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php
Rewrites namespaced-key test to call getValueWithNamespace('subscribemessage:1'), stubbing cache/repository and asserting fallback to parent value and cache set.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.26% 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 'Dev' is too vague and generic. It doesn't convey what the PR actually does—it lacks specificity about the substantive refactoring and new features being introduced. Consider a more descriptive title like 'Refactor SubscribePageManager with config migration support' or 'Add SubscribePageConfigMigrationService and update SubscribePageManager' to better reflect the actual changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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: 4

🧹 Nitpick comments (2)
src/Domain/Subscription/Repository/SubscriberPageRepository.php (1)

41-49: 💤 Low value

Heads-up on total semantics + lastId parameter usage.

Two things worth a glance:

  1. $countQb = clone $queryBuilder; is cloned before the andWhere('p.id > :afterId') is attached, so $total ends up being the count of all pages — not the count after the cursor. That's actually fine for cursor pagination if you want the consumer to know the total set size, but it's easy to misread; a short comment would help.
  2. $filter->getLastId() is called repeatedly — minor, but caching it in a local would also avoid drift if the filter object ever mutates between calls.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Repository/SubscriberPageRepository.php` around lines
41 - 49, In getFilteredAfterId: cache $filter->getLastId() into a local (e.g.
$afterId) and use that variable for all subsequent calls to avoid repeated
method calls and potential mutation drift; also add a short comment next to the
$countQb = clone $queryBuilder (or adjust cloning placement) clarifying whether
$total is intended to be the overall count or the post-cursor count—if you want
total after the cursor, move the clone to after applying andWhere('p.id >
:afterId'), otherwise keep current clone and document the semantics.
src/Domain/Subscription/Service/Manager/SubscribePageManager.php (1)

40-66: 💤 Low value

Mixing pageDataRepository->persist(...) with entityManager->remove(...) reads oddly.

syncPageData schedules creates through the repository ($this->pageDataRepository->persist($pageData)) but schedules deletes directly against $this->entityManager->remove($pageData). Both are fine from a domain-purity standpoint (no flush, no DDL — that's allowed by the guidelines), but it's a little jarring to read. If SubscriberPageDataRepository already has (or could grow) a remove() method, going through the repo for both keeps the abstraction consistent and the entityManager dependency a bit less load-bearing.

#!/bin/bash
ast-grep --pattern $'class SubscriberPageDataRepository {
  $$$
}'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Service/Manager/SubscribePageManager.php` around
lines 40 - 66, Replace direct use of $this->entityManager->remove(...) in
syncPageData with a repository-level deletion to keep persistence abstraction
consistent: add a remove(SubscribePageData $pageData): void method to
SubscriberPageDataRepository (or ensure it exists) and call
$this->pageDataRepository->remove($pageData) inside
SubscribePageManager::syncPageData instead of
$this->entityManager->remove($pageData); leave persist calls as-is and do not
change flushing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Domain/Subscription/Repository/SubscriberPageRepository.php`:
- Around line 92-106: The loadPageData method is dropping the first row's data
because array_shift($result)['page'] removes that row; update loadPageData
(method loadPageData, class SubscriberPageRepository, working with SubscribePage
and setData) to obtain the SubscribePage without mutating the $result array
(e.g. read $result[0]['page'] or otherwise extract 'page' from the first
element) and then iterate over the full $result to collect every 'data' entry
before calling $page->setData($data) so the first row's data is preserved.
- Around line 66-82: The grouping in getFilteredAfterId is creating
duplicate/invalid buckets by indexing both $page->getId() and $data->getId();
change the grouping so each bucket is keyed only by the page id (use
$page->getId()) and aggregate associated data entries into that same bucket
(append ['data'=> $data] to the page's array) while skipping data-only entries
when no page is present, so loadPageData always receives a group starting with a
'page' element; update getFilteredAfterId's loop to only create/append to
$grouped[$page->getId()] and stop creating $grouped[$data->getId()], ensuring
compatibility with SubscribePageData/SubscribePageManager::setPageData and
syncPageData expectations.

In `@src/Domain/Subscription/Service/Manager/SubscribePageManager.php`:
- Around line 55-60: The call to SubscribePageData::setId uses $page->getId()
which is ?int and can be null for a new page; update syncPageData to mirror the
existing setPageData behavior by passing (int)$page->getId() into
SubscribePageData::setId (or add an explicit guard/assert that the page is
persisted before calling syncPageData) so setId never receives null and avoids a
TypeError.

In `@tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php`:
- Around line 347-350: The test uses the deprecated withConsecutive on the
entity manager mock; replace that expectation by capturing the actual arguments
passed to EntityManagerInterface::remove via a willReturnCallback and then
assert the captured calls equal [$pageData1, $pageData2]. Concretely, in
SubscribePageManagerTest replace the ->withConsecutive([...]) on the
$this->entityManager->expects(...)->method('remove') mock with
->willReturnCallback(function($arg) use (&$calls) { $calls[] = $arg; }) and
after exercising the SUT assert that $calls contains $pageData1 then $pageData2.

---

Nitpick comments:
In `@src/Domain/Subscription/Repository/SubscriberPageRepository.php`:
- Around line 41-49: In getFilteredAfterId: cache $filter->getLastId() into a
local (e.g. $afterId) and use that variable for all subsequent calls to avoid
repeated method calls and potential mutation drift; also add a short comment
next to the $countQb = clone $queryBuilder (or adjust cloning placement)
clarifying whether $total is intended to be the overall count or the post-cursor
count—if you want total after the cursor, move the clone to after applying
andWhere('p.id > :afterId'), otherwise keep current clone and document the
semantics.

In `@src/Domain/Subscription/Service/Manager/SubscribePageManager.php`:
- Around line 40-66: Replace direct use of $this->entityManager->remove(...) in
syncPageData with a repository-level deletion to keep persistence abstraction
consistent: add a remove(SubscribePageData $pageData): void method to
SubscriberPageDataRepository (or ensure it exists) and call
$this->pageDataRepository->remove($pageData) inside
SubscribePageManager::syncPageData instead of
$this->entityManager->remove($pageData); leave persist calls as-is and do not
change flushing behavior.
🪄 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: d7ca3a94-7f3e-4081-b51b-c76c40753239

📥 Commits

Reviewing files that changed from the base of the PR and between 16d3809 and 4a1084b.

📒 Files selected for processing (4)
  • src/Domain/Subscription/Model/SubscribePage.php
  • src/Domain/Subscription/Repository/SubscriberPageRepository.php
  • src/Domain/Subscription/Service/Manager/SubscribePageManager.php
  • tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php

Comment on lines +66 to +82
$grouped = [];
foreach ($rows as $row) {
/** @var SubscribePage $page */
$page = $row['page'] ?? null;
$data = $row['data'] ?? null;
if ($page !== null) {
$grouped[$page->getId()][] = $row;
}
if ($data !== null) {
$grouped[$data->getId()][] = ['data' => $data];
}
}

$pages = [];
foreach ($grouped as $group) {
$pages[] = $this->loadPageData($group);
}
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 | 🔴 Critical | ⚡ Quick win

Grouping in getFilteredAfterId double-buckets rows and can crash loadPageData.

Every row is appended twice into $grouped: once under $page->getId() with the full row, and once under $data->getId() with a ['data' => ...]-only entry. Because SubscribePageData::setId(...) is called with $page->getId() elsewhere (see SubscribePageManager::setPageData / syncPageData), $data->getId() collides with $page->getId(), so every page's bucket ends up with duplicated data items (each SubscribePageData gets counted twice).

And if data.id ever doesn't equal page.id, the second branch creates a group that has no 'page' key, and loadPageData()'s array_shift($result)['page'] then triggers an undefined-index access on a non-page entry.

🩹 Suggested rewrite — single bucket per page, aggregate data inline
-        $grouped = [];
-        foreach ($rows as $row) {
-            /** `@var` SubscribePage $page */
-            $page = $row['page'] ?? null;
-            $data = $row['data'] ?? null;
-            if ($page !== null) {
-                $grouped[$page->getId()][] = $row;
-            }
-            if ($data !== null) {
-                $grouped[$data->getId()][] = ['data' => $data];
-            }
-        }
-
-        $pages = [];
-        foreach ($grouped as $group) {
-            $pages[] = $this->loadPageData($group);
-        }
+        /** `@var` array<int, array{page: SubscribePage, data: SubscribePageData[]}> $grouped */
+        $grouped = [];
+        foreach ($rows as $row) {
+            $page = $row['page'] ?? null;
+            if ($page === null) {
+                continue;
+            }
+            $pageId = $page->getId();
+            if (!isset($grouped[$pageId])) {
+                $grouped[$pageId] = ['page' => $page, 'data' => []];
+            }
+            if (!empty($row['data'])) {
+                $grouped[$pageId]['data'][] = $row['data'];
+            }
+        }
+
+        $pages = [];
+        foreach ($grouped as $entry) {
+            $entry['page']->setData($entry['data']);
+            $pages[] = $entry['page'];
+        }
📝 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
$grouped = [];
foreach ($rows as $row) {
/** @var SubscribePage $page */
$page = $row['page'] ?? null;
$data = $row['data'] ?? null;
if ($page !== null) {
$grouped[$page->getId()][] = $row;
}
if ($data !== null) {
$grouped[$data->getId()][] = ['data' => $data];
}
}
$pages = [];
foreach ($grouped as $group) {
$pages[] = $this->loadPageData($group);
}
/** `@var` array<int, array{page: SubscribePage, data: SubscribePageData[]}> $grouped */
$grouped = [];
foreach ($rows as $row) {
$page = $row['page'] ?? null;
if ($page === null) {
continue;
}
$pageId = $page->getId();
if (!isset($grouped[$pageId])) {
$grouped[$pageId] = ['page' => $page, 'data' => []];
}
if (!empty($row['data'])) {
$grouped[$pageId]['data'][] = $row['data'];
}
}
$pages = [];
foreach ($grouped as $entry) {
$entry['page']->setData($entry['data']);
$pages[] = $entry['page'];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Repository/SubscriberPageRepository.php` around lines
66 - 82, The grouping in getFilteredAfterId is creating duplicate/invalid
buckets by indexing both $page->getId() and $data->getId(); change the grouping
so each bucket is keyed only by the page id (use $page->getId()) and aggregate
associated data entries into that same bucket (append ['data'=> $data] to the
page's array) while skipping data-only entries when no page is present, so
loadPageData always receives a group starting with a 'page' element; update
getFilteredAfterId's loop to only create/append to $grouped[$page->getId()] and
stop creating $grouped[$data->getId()], ensuring compatibility with
SubscribePageData/SubscribePageManager::setPageData and syncPageData
expectations.

Comment on lines +92 to 106
private function loadPageData(array $result): SubscribePage
{
/** @var SubscribePage $page */
$page = array_shift($result)['page'];

$data = [];
foreach ($result as $row) {
if (isset($row['data'])) {
$data[] = $row['data'];
}
}
$page->setData($data);

return $page;
}
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 | 🔴 Critical | ⚡ Quick win

loadPageData drops the first row's data.

array_shift($result)['page'] removes the entire first row and only keeps its page; the data value that came on that same row is silently discarded. The loop then continues from the second row, so any page that has ≥1 SubscribePageData will return missing exactly the first one. For a page with a single data row, getData() ends up as [].

🩹 Suggested fix — keep the whole result set when collecting `data`
-    private function loadPageData(array $result): SubscribePage
-    {
-        /** `@var` SubscribePage $page */
-        $page = array_shift($result)['page'];
-
-        $data = [];
-        foreach ($result as $row) {
-            if (isset($row['data'])) {
-                $data[] = $row['data'];
-            }
-        }
-        $page->setData($data);
-
-        return $page;
-    }
+    private function loadPageData(array $result): SubscribePage
+    {
+        /** `@var` SubscribePage $page */
+        $page = $result[0]['page'];
+
+        $data = [];
+        foreach ($result as $row) {
+            if (!empty($row['data'])) {
+                $data[] = $row['data'];
+            }
+        }
+        $page->setData($data);
+
+        return $page;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Repository/SubscriberPageRepository.php` around lines
92 - 106, The loadPageData method is dropping the first row's data because
array_shift($result)['page'] removes that row; update loadPageData (method
loadPageData, class SubscriberPageRepository, working with SubscribePage and
setData) to obtain the SubscribePage without mutating the $result array (e.g.
read $result[0]['page'] or otherwise extract 'page' from the first element) and
then iterate over the full $result to collect every 'data' entry before calling
$page->setData($data) so the first row's data is preserved.

Comment on lines +55 to +60
$pageData = (new SubscribePageData())
->setId($page->getId())
->setName($pageDataKey)
->setData($value);

$this->pageDataRepository->persist($pageData);
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 | 🟡 Minor | ⚡ Quick win

$page->getId() is ?int; type may bite if the page is fresh.

SubscribePage::getId() returns ?int. The pre-existing setPageData() method already guards against this with (int)$page->getId() (line 110). The new syncPageData() passes it raw, so if setId() is typed int, calling this for a not-yet-persisted page hands it null and throws a TypeError. If the contract is "page must already be persisted", a small assert/early return makes that explicit; otherwise just mirror the existing cast.

🩹 Suggested fix — mirror the existing cast
             $pageData = (new SubscribePageData())
-                ->setId($page->getId())
+                ->setId((int)$page->getId())
                 ->setName($pageDataKey)
                 ->setData($value);

Quick check on SubscribePageData::setId's signature so we know which behavior to lean into:

#!/bin/bash
ast-grep --pattern $'class SubscribePageData {
  $$$
  public function setId($_) { $$$ }
  $$$
}'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Service/Manager/SubscribePageManager.php` around
lines 55 - 60, The call to SubscribePageData::setId uses $page->getId() which is
?int and can be null for a new page; update syncPageData to mirror the existing
setPageData behavior by passing (int)$page->getId() into
SubscribePageData::setId (or add an explicit guard/assert that the page is
persisted before calling syncPageData) so setId never receives null and avoids a
TypeError.

Comment on lines +347 to +350
$this->entityManager
->expects($this->exactly(2))
->method('remove')
->withConsecutive([$pageData1], [$pageData2]);
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

PHPUnit withConsecutive deprecation replacement

💡 Result:

The withConsecutive method was deprecated in PHPUnit 9.6 and removed in PHPUnit 10 due to fundamental design limitations regarding rigid sequential matching and difficulties with dynamic scenarios [1]. To replace withConsecutive, you should move away from verifying sequences using a single method call and instead use one of the following approaches depending on your specific testing needs: 1. Use standard PHPUnit constraints: For simple cases, you can often replace withConsecutive with multiple individual expectation setups or by using with combined with more flexible argument constraints. 2. Use willReturnCallback: This is the most robust way to replace withConsecutive for complex scenarios. You can use this to inspect arguments dynamically and return values based on the invocation number or the arguments received [2]. Example: $matcher = $this->exactly(2); $mock->expects($matcher) ->method('yourMethod') ->willReturnCallback(function ($arg) use ($matcher) { match ($matcher->numberOfInvocations) { 1 => $this->assertEquals('expected1', $arg), 2 => $this->assertEquals('expected2', $arg), }; return 'someResponse'; }); 3. Use dedicated libraries: If you rely heavily on this functionality, third-party extensions such as inspirum/phpunit-extension provide wrappers that re-implement the withConsecutive functionality for newer versions of PHPUnit [3]. 4. Leverage newer PHPUnit features: For modern PHPUnit versions, focus on writing tests that are expressive and maintainable. If you find yourself needing to mock complex sequences, consider whether the design of the code under test can be simplified to avoid the need for such brittle mock configurations [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show phpunit version constraints from composer.json/composer.lock if present
ls -la
if [ -f composer.json ]; then
  echo "---- composer.json ----"
  rg -n '"phpunit/phpunit"' composer.json || true
  rg -n 'phpunit' composer.json || true
  cat composer.json | sed -n '1,120p'
fi

if [ -f composer.lock ]; then
  echo "---- composer.lock (phpunit entries) ----"
  rg -n '"name": "phpunit/phpunit"' composer.lock | head -n 20 || true
  # Try to extract the version near that entry
  python3 - <<'PY'
import json,sys
p='composer.lock'
try:
  with open(p,'r',encoding='utf-8') as f:
    data=json.load(f)
except Exception as e:
  print("Could not parse composer.lock:",e); sys.exit(0)
pkgs=[]
for sec in ('packages','packages-dev'):
  for pkg in data.get(sec,[]):
    if pkg.get('name')=='phpunit/phpunit':
      pkgs.append(pkg)
print("phpunit/phpunit packages found:",len(pkgs))
for pkg in pkgs:
  print(" - version:",pkg.get('version'),"requires:",pkg.get('require',{}).get('php','(n/a)'))
PY
fi

Repository: phpList/core

Length of output: 5370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -f composer.lock ]; then
  echo "---- composer.lock exists ----"
  python3 - <<'PY'
import json
p='composer.lock'
data=json.load(open(p,'r',encoding='utf-8'))
for sec in ('packages','packages-dev'):
  for pkg in data.get(sec,[]):
    if pkg.get('name')=='phpunit/phpunit':
      print("phpunit/phpunit:",pkg.get('version'))
PY
else
  echo "---- composer.lock not found ----"
fi

Repository: phpList/core

Length of output: 90


withConsecutive() is heading for a future PHPUnit 10 break 😅

In tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php (lines 347-350), the test relies on ->withConsecutive(...), which PHPUnit deprecated in 9.6 and removed in 10. The repo currently allows PHPUnit via phpunit/phpunit: "^9.5" (no composer.lock), so a major bump to 10 will break this test.

🩹 Sketch — callback-based equivalent
-        $this->entityManager
-            ->expects($this->exactly(2))
-            ->method('remove')
-            ->withConsecutive([$pageData1], [$pageData2]);
+        $removed = [];
+        $this->entityManager
+            ->expects($this->exactly(2))
+            ->method('remove')
+            ->willReturnCallback(function ($arg) use (&$removed): void {
+                $removed[] = $arg;
+            });
+        // ... after the call:
+        // $this->assertSame([$pageData1, $pageData2], $removed);
PHPUnit withConsecutive deprecation replacement
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php`
around lines 347 - 350, The test uses the deprecated withConsecutive on the
entity manager mock; replace that expectation by capturing the actual arguments
passed to EntityManagerInterface::remove via a willReturnCallback and then
assert the captured calls equal [$pageData1, $pageData2]. Concretely, in
SubscribePageManagerTest replace the ->withConsecutive([...]) on the
$this->entityManager->expects(...)->method('remove') mock with
->willReturnCallback(function($arg) use (&$calls) { $calls[] = $arg; }) and
after exercising the SUT assert that $calls contains $pageData1 then $pageData2.

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

Caution

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

⚠️ Outside diff range comments (1)
src/Domain/Subscription/Service/Manager/SubscribePageManager.php (1)

55-84: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep SubscribePage::$data in sync with the diff you just applied.

syncPageData() updates persistence state, but it never rebuilds the new in-memory SubscribePage::getData() container. After a create/delete path, callers can keep using $page and still see stale entries until they reload it.

💡 Minimal fix sketch
     public function syncPageData(array $data, SubscribePage $page): void
     {
         $existingPageData = [];
+        $syncedPageData = [];

         foreach ($this->getPageData($page) as $pageData) {
             $existingPageData[$pageData->getName()] = $pageData;
         }

         foreach ($data as $pageDataKey => $value) {
             if (isset($existingPageData[$pageDataKey])) {
                 $pageData = $existingPageData[$pageDataKey];
                 $pageData->setData($value);
                 unset($existingPageData[$pageDataKey]);
+                $syncedPageData[] = $pageData;
                 continue;
             }

             $pageData = (new SubscribePageData())
                 ->setId($page->getId())
                 ->setName($pageDataKey)
                 ->setData($value);

             $this->pageDataRepository->persist($pageData);
+            $syncedPageData[] = $pageData;
         }

         foreach ($existingPageData as $pageData) {
             $this->entityManager->remove($pageData);
         }
+
+        $page->setData($syncedPageData);

         if ($this->subscribePageConfigMigrationEnabled) {
             $this->configMigrationService->copyToConfig(page: $page, data: $data);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Service/Manager/SubscribePageManager.php` around
lines 55 - 84, syncPageData updates DB entries but never updates the in-memory
SubscribePage::$data, so callers see stale data; after applying
creates/updates/deletes in syncPageData (the method in SubscribePageManager)
update the SubscribePage instance's data container to reflect the new state —
e.g. rebuild the data array from the final $data input (or from the adjusted
$existingPageData plus newly created entries) and call $page->setData(...) so
SubscribePage::getData() returns the current values; ensure this happens just
before calling configMigrationService->copyToConfig.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Domain/Configuration/Service/Provider/ConfigProvider.php`:
- Around line 102-103: In ConfigProvider::getValue path where you call
getValue($parentKey), guard against boolean-style parent keys by wrapping the
call in a try/catch that specifically catches InvalidArgumentException and
returns null (or delegates to isEnabled handling) instead of letting the
exception bubble; locate the getValue($parentKey) invocation in ConfigProvider
and add the try/catch around it so boolean-like inputs such as
"maintenancemode:1" do not cause a runtime exception.

In `@src/Domain/Subscription/Service/SubscribePageConfigMigrationService.php`:
- Around line 82-84: The Domain service SubscribePageConfigMigrationService
currently calls $this->entityManager->flush() inside its migration method;
remove that flush so the domain service remains side-effect free
(prepare/persist/remove only) and instead return or expose the $hasChanges /
scheduled-operations result (e.g., boolean or array of changed entities) so the
Application/handler layer can call flush(); update the method in
SubscribePageConfigMigrationService to stop finalizing the DB and adjust tests
to assert the returned change state and that persistence was scheduled rather
than expecting flush() to have been invoked.

---

Outside diff comments:
In `@src/Domain/Subscription/Service/Manager/SubscribePageManager.php`:
- Around line 55-84: syncPageData updates DB entries but never updates the
in-memory SubscribePage::$data, so callers see stale data; after applying
creates/updates/deletes in syncPageData (the method in SubscribePageManager)
update the SubscribePage instance's data container to reflect the new state —
e.g. rebuild the data array from the final $data input (or from the adjusted
$existingPageData plus newly created entries) and call $page->setData(...) so
SubscribePage::getData() returns the current values; ensure this happens just
before calling configMigrationService->copyToConfig.
🪄 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: 7dde6c6f-30e3-4851-9b36-e4c6f478cac1

📥 Commits

Reviewing files that changed from the base of the PR and between 4a1084b and dbb43ac.

📒 Files selected for processing (8)
  • config/parameters.yml.dist
  • config/services/services.yml
  • src/Domain/Configuration/Service/Provider/ConfigProvider.php
  • src/Domain/Subscription/Service/Manager/SubscribePageManager.php
  • src/Domain/Subscription/Service/SubscribePageConfigMigrationService.php
  • tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php
  • tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php
  • tests/Unit/Domain/Subscription/Service/SubscribePageConfigMigrationServiceTest.php

Comment thread src/Domain/Configuration/Service/Provider/ConfigProvider.php
Comment on lines +82 to +84
if ($hasChanges) {
$this->entityManager->flush();
}
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 | 🏗️ Heavy lift

Move transaction finalization out of the Domain service.

$this->entityManager->flush() at Line 83 performs DB finalization in a non-DynamicListAttr domain service. Keep this method side-effect free (prepare/schedule changes only) and flush in the Application/handler layer.

Suggested change
-        if ($hasChanges) {
-            $this->entityManager->flush();
-        }
-
         return $hasChanges;

Downstream impact: tests currently asserting flush() here should be updated to assert only returned change state and scheduled persistence behavior.

As per coding guidelines, "src/Domain/**: ❌ Do not allow, flag ANY DB write / finalization: $entityManager->flush(...) … For non-DynamicListAttr code … move this to Infrastructure/Application layer."

📝 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
if ($hasChanges) {
$this->entityManager->flush();
}
return $hasChanges;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Subscription/Service/SubscribePageConfigMigrationService.php`
around lines 82 - 84, The Domain service SubscribePageConfigMigrationService
currently calls $this->entityManager->flush() inside its migration method;
remove that flush so the domain service remains side-effect free
(prepare/persist/remove only) and instead return or expose the $hasChanges /
scheduled-operations result (e.g., boolean or array of changed entities) so the
Application/handler layer can call flush(); update the method in
SubscribePageConfigMigrationService to stop finalizing the DB and adjust tests
to assert the returned change state and that persistence was scheduled rather
than expecting flush() to have been invoked.

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.

♻️ Duplicate comments (1)
src/Domain/Configuration/Service/Provider/ConfigProvider.php (1)

94-103: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle boolean parent keys in namespace fallback to prevent runtime exceptions.

At Line 102, getValue($parentKey) can still throw InvalidArgumentException for boolean parent keys (for example, maintenancemode:1) and currently bubbles out.

Proposed fix
         if (str_contains($keyValue, ':')) {
             [$parent] = explode(':', $keyValue, 2);
             try {
                 $parentKey = ConfigOption::from($parent);
             } catch (ValueError) {
                 return null;
             }

-            return $this->getValue($parentKey);
+            try {
+                return $this->getValue($parentKey);
+            } catch (InvalidArgumentException) {
+                return null;
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Domain/Configuration/Service/Provider/ConfigProvider.php` around lines 94
- 103, The namespace fallback branch in ConfigProvider tries to convert the
parent token via ConfigOption::from($parent) and then calls
$this->getValue($parentKey), but getValue can throw InvalidArgumentException for
boolean-like parent keys (e.g., "maintenancemode:1") and currently bubbles up;
wrap the $this->getValue($parentKey) call in a try/catch that catches
InvalidArgumentException and returns null on failure so the method gracefully
falls back, keeping the existing catch for ValueError around ConfigOption::from
intact (refer to ConfigOption::from and ConfigProvider::getValue to locate the
code).
🧹 Nitpick comments (1)
tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php (1)

71-76: 💤 Low value

withConsecutive() is deprecated in PHPUnit 9.6 and removed in 10.

This new test uses the same deprecated pattern flagged elsewhere. Consider using a callback-based approach to future-proof the test.

Suggested refactor
+        $callCount = 0;
         $this->pageRepository
             ->expects($this->exactly(2))
             ->method('findPageWithData')
-            ->withConsecutive([123], [123])
-            ->willReturnOnConsecutiveCalls($page, $refetchedPage);
+            ->with(123)
+            ->willReturnCallback(function () use (&$callCount, $page, $refetchedPage) {
+                return ++$callCount === 1 ? $page : $refetchedPage;
+            });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php`
around lines 71 - 76, The test currently uses the deprecated withConsecutive()
on the mock for pageRepository->findPageWithData; replace this with a
callback-based stub: remove withConsecutive() and use willReturnCallback on the
mock for findPageWithData, implement a small closure that captures a local
counter (e.g., $call = 0) and increments on each invocation, inspects the
incoming argument(s) to ensure it's the expected page id (123) and returns $page
on the first call and $refetchedPage on the second; keep the
->expects($this->exactly(2)) expectation and ensure the closure signature
matches findPageWithData so the test behavior is identical without using
withConsecutive().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/Domain/Configuration/Service/Provider/ConfigProvider.php`:
- Around line 94-103: The namespace fallback branch in ConfigProvider tries to
convert the parent token via ConfigOption::from($parent) and then calls
$this->getValue($parentKey), but getValue can throw InvalidArgumentException for
boolean-like parent keys (e.g., "maintenancemode:1") and currently bubbles up;
wrap the $this->getValue($parentKey) call in a try/catch that catches
InvalidArgumentException and returns null on failure so the method gracefully
falls back, keeping the existing catch for ValueError around ConfigOption::from
intact (refer to ConfigOption::from and ConfigProvider::getValue to locate the
code).

---

Nitpick comments:
In `@tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php`:
- Around line 71-76: The test currently uses the deprecated withConsecutive() on
the mock for pageRepository->findPageWithData; replace this with a
callback-based stub: remove withConsecutive() and use willReturnCallback on the
mock for findPageWithData, implement a small closure that captures a local
counter (e.g., $call = 0) and increments on each invocation, inspects the
incoming argument(s) to ensure it's the expected page id (123) and returns $page
on the first call and $refetchedPage on the second; keep the
->expects($this->exactly(2)) expectation and ensure the closure signature
matches findPageWithData so the test behavior is identical without using
withConsecutive().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 85869e0d-e1b8-4472-8c17-e3523cddc4d3

📥 Commits

Reviewing files that changed from the base of the PR and between dbb43ac and 41df932.

📒 Files selected for processing (8)
  • config/parameters.yml.dist
  • config/services/services.yml
  • src/Domain/Configuration/Service/Provider/ConfigProvider.php
  • src/Domain/Subscription/Service/Manager/SubscribePageManager.php
  • src/Domain/Subscription/Service/SubscribePageConfigMigrationService.php
  • tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php
  • tests/Unit/Domain/Subscription/Service/Manager/SubscribePageManagerTest.php
  • tests/Unit/Domain/Subscription/Service/SubscribePageConfigMigrationServiceTest.php

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