Skip to content

Dev - #386

Open
TatevikGr wants to merge 7 commits into
mainfrom
dev
Open

Dev#386
TatevikGr wants to merge 7 commits into
mainfrom
dev

Conversation

@TatevikGr

@TatevikGr TatevikGr commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added .env-based configuration for database, mail, security, messaging, uploads, and other application settings.
    • Installation and updates can generate a local .env file with a secure secret.
    • Added support for processing the asynchronous email queue.
    • Database table prefixes can now be configured for deployments.
    • Updated default administrator setup to use the configured application password.
  • Documentation

    • Updated setup instructions and changelog for environment-based configuration and queue processing.

Thanks for contributing to phpList!

* Refactor: env

* feat: add support for .env configuration files and dotenv integration

* feat: add parameters configuration file creation to ScriptHandler

* remove legacy public app files

* fix: correct environment variable naming for parallel usage with phplist3

---------

Co-authored-by: Tatevik <tatevikg1@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces deployment defaults with dotenv configuration, adds configurable Doctrine table prefixes, and updates migrations, messaging configuration, domain behavior, documentation, and administrator defaults.

Changes

Environment configuration migration

Layer / File(s) Summary
Dotenv template and generation
.env.dist, composer.json, src/Composer/ScriptHandler.php, .gitignore
Adds environment defaults, the Symfony Dotenv dependency, .env generation with a random secret, and environment-file ignore rules.
Bootstrap dotenv loading
src/Core/Bootstrap.php, src/Core/ApplicationKernel.php
Loads dotenv files before debugging and application-kernel configuration. Loads messenger.yaml when it exists.
Parameters configuration and documentation
config/parameters.yml, CHANGELOG.md, README.md
Adds environment-backed parameters and updates setup instructions and the asynchronous email queue example.
Default administrator configuration
src/Domain/Identity/Command/ImportDefaultsCommand.php
Injects the configured administrator password and changes the default login to test1.

Database prefixing

Layer / File(s) Summary
Entity table mappings and listener
src/Core/Doctrine/TablePrefixListener.php, config/services.yml, src/Domain/*/Model/*
Entity mappings use unprefixed table names. The Doctrine listener applies the configured prefix to eligible entities.
Migration prefix handling
src/Migrations/*
Migrations use AbstractPrefixedMigration, which replaces phplist_ with DATABASE_PREFIX in SQL.

Messaging and domain updates

Layer / File(s) Summary
Messenger routing and queue commands
config/packages/messenger.yaml, src/Domain/Messaging/Command/*
Updates campaign processor routes and uses promoted readonly command dependencies.
Message state and repository corrections
src/Domain/Messaging/Model/Message/MessageStatus.php, src/Domain/Messaging/Repository/MessageRepository.php
Removes Requeued and reads embargo values from m.schedule.embargo.
Configuration lookup correction
src/Domain/Configuration/Service/Provider/ConfigProvider.php, tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php
Changes configuration lookups from item to key and updates test expectations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Composer
  participant ScriptHandler
  participant Dotenv
  participant Bootstrap
  participant Doctrine
  Composer->>ScriptHandler: run update-configuration
  ScriptHandler->>Dotenv: create /.env from /.env.dist
  Bootstrap->>Dotenv: load environment variables
  Bootstrap->>Doctrine: configure application metadata
  Doctrine->>Doctrine: apply DATABASE_PREFIX to eligible tables
Loading

Possibly related PRs

  • phpList/core#366: Shares migration and database-prefix changes, including overlapping migration classes and entity table mappings.
  • phpList/core#375: Updates Messenger routing for the campaign processor message classes.
  • phpList/core#377: Modifies ConfigProvider::isEnabled() and overlapping configuration parameters.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 to identify the pull request's main changes. Replace "Dev" with a concise title that describes the main changes, such as environment configuration and database table prefix support.
✅ 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.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/Composer/ScriptHandler.php (1)

283-285: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Handle existing .env files during upgrades.

Preserving an existing file avoids overwriting deployment secrets, but it also prevents newly required keys from .env.dist from being added. Since config/parameters.yml now references those keys without inline fallbacks, older or partial .env files can leave required parameters unresolved. Add a merge/validation step or document an explicit upgrade procedure.

🤖 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/Composer/ScriptHandler.php` around lines 283 - 285, Update the existing
`.env` handling in the upgrade flow so an early return from the file-exists
check no longer skips required-key validation and incorporation of newly
introduced values from `.env.dist`. Preserve existing deployment secrets while
merging only missing required keys, or invoke the project’s established
validation/upgrade mechanism to ensure parameters referenced by
`config/parameters.yml` are resolved.
🤖 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 @.env.dist:
- Around line 55-57: Update the environment-loading flow around
loadEnvironmentVariables so .env.dist remains template-only and PHPLIST_SECRET
never accepts the literal “%s” placeholder. Require a non-placeholder generated
or explicitly supplied secret, and fail fast when it is missing before security
tokens are used.

In `@CHANGELOG.md`:
- Line 13: Update the changelog entry to reference the current parameters
template path, config/parameters.yml, or explicitly state that
config/parameters.yml.dist was renamed to config/parameters.yml; keep the
description of defaults moving to .env.dist unchanged.

---

Nitpick comments:
In `@src/Composer/ScriptHandler.php`:
- Around line 283-285: Update the existing `.env` handling in the upgrade flow
so an early return from the file-exists check no longer skips required-key
validation and incorporation of newly introduced values from `.env.dist`.
Preserve existing deployment secrets while merging only missing required keys,
or invoke the project’s established validation/upgrade mechanism to ensure
parameters referenced by `config/parameters.yml` are resolved.
🪄 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 Plus

Run ID: 5c64df25-bce9-461d-83d0-2f202a1f882d

📥 Commits

Reviewing files that changed from the base of the PR and between c5efbd8 and 8d28d3b.

📒 Files selected for processing (12)
  • .env.dist
  • .gitignore
  • CHANGELOG.md
  • README.md
  • composer.json
  • config/parameters.yml
  • config/parameters.yml.dist
  • public/app.php
  • public/app_dev.php
  • public/app_test.php
  • src/Composer/ScriptHandler.php
  • src/Core/Bootstrap.php
💤 Files with no reviewable changes (4)
  • public/app_test.php
  • config/parameters.yml.dist
  • public/app_dev.php
  • public/app.php

Comment thread .env.dist
Comment on lines +55 to +57
# A secret key that's used to generate certain security-related tokens
PHPLIST_SECRET=%s
VERIFY_SSL=1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(\.env\.dist|Bootstrap\.php|composer\.json)$|Dotenv' || true

echo
echo "== .env.dist relevant lines =="
if [ -f .env.dist ]; then
  nl -ba .env.dist | sed -n '45,65p'
fi

echo
echo "== composer scripts and dotenv package version =="
if [ -f composer.json ]; then
  sed -n '1,220p' composer.json | rg -n '"scripts"|"symfony/dotenv"|"require-dev":|"dotenv"' -C 4 || true
fi

echo
echo "== search for env.dist or Dotenv usage =="
rg -n 'env\.dist|Dotenv|loadEnv|PHPLIST_SECRET' -S . --glob '!vendor/**' --glob '!node_modules/**' || true

Repository: phpList/core

Length of output: 282


🌐 Web query:

Symfony Dotenv 6.4 loadEnv .env.dist fallback behavior source Dotenv.php

💡 Result:

In Symfony 6.4, the loadEnv method in the Dotenv component implements a specific fallback mechanism for the.env file. According to the source code (Dotenv.php), if the primary.env file is missing, the component will automatically attempt to load the.env.dist file if it exists [1][2][3]. The specific logic within loadEnv is implemented as follows: if (is_file($path) ||!is_file($p = "$path.dist")) { $this->doLoad($overrideExistingVars, [$path]); } else { $this->doLoad($overrideExistingVars, [$p]); } This means that if the provided path (usually.env) is not found (is_file($path) is false) AND a file with a.dist extension exists at that location (.env.dist), the Dotenv component will load the.env.dist file instead [1][2][4]. Key points regarding this behavior: - Fallback Priority: The primary.env file always takes precedence. The.env.dist file is only used as a fallback when the primary.env file is absent [1][3]. - Modern Usage: While this fallback exists for compatibility, modern Symfony best practices emphasize that.env should be committed to the repository containing non-sensitive default values, effectively replacing the historical role of.env.dist [5]. - Overriding: This loading process respects the overrideExistingVars parameter, which determines whether values from these files can override environment variables already set by the system [6][1]. System environment variables generally always have the highest precedence and cannot be overridden by Dotenv files unless explicitly configured [5][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .env.dist relevant lines =="
if [ -f .env.dist ]; then
  awk '{printf "%6d  %s\n", NR, $0}' .env.dist | sed -n '45,65p'
fi

echo
echo "== composer relevant snippets =="
if [ -f composer.json ]; then
  python3 - <<'PY'
import json, re, sys
from pathlib import Path
p=Path('composer.json')
text=p.read_text()
try: data=json.loads(text)
except Exception as e:
    print("json_parse_error", repr(e))
    for line in text.splitlines()[:240]:
        if any(s in line for s in ['scripts', 'symfony/dotenv', '"dotenv"', 'require-dev', 'require']):
            print(line)
    sys.exit(0)
print("scripts:", data.get("scripts"))
print("symfony/dotenv:", data.get("require", {}).get("symfony/dotenv"), data.get("require-dev", {}).get("symfony/dotenv"))
for key in ("require","require-dev"):
    for pk,ve in data.get(key,{}).items():
        if "dot" in pk.lower() or "symfony" in pk.lower():
            print(key, pk, ve)
PY
fi

echo
echo "== Bootstrap.php outlines/snippets =="
if [ -f src/Core/Bootstrap.php ]; then
  wc -l src/Core/Bootstrap.php
  rg -n 'Dotenv|loadEnv|env\.dist|load\\(|PHPLIST_SECRET|try|catch' src/Core/Bootstrap.php -C 8
fi

echo
echo "== repository-wide env.dist/env.load/fallback =="
rg -n 'env\.dist|loadEnv\\(|new Dotenv|Dotenv::|PHPLIST_SECRET|secret' -S src composer.json .env.dist --glob '!vendor/**' --glob '!node_modules/**' || true

Repository: phpList/core

Length of output: 2686


🌐 Web query:

Symfony Dotenv load .env.dist if .env missing loadEnv $projectRoot/.env

💡 Result:

In Symfony, the Dotenv component's loadEnv method is designed to handle environment file loading automatically, including a fallback mechanism for the.env.dist file [1][2][3]. When calling $dotenv->loadEnv($projectRoot. '/.env'), the component follows these behaviors: 1. Fallback to.env.dist: If a.env file does not exist, the component will load.env.dist if it is present [1][3]. This was implemented to allow projects to define environment variables in.env.dist when a local.env file is intentionally omitted [1]. 2. Loading Order: The loadEnv method loads files in a specific order, where subsequent files can override variables defined in previously loaded files [2][4][3]. The standard sequence (subject to file existence) is: -.env (or.env.dist if.env is missing) [2][3] -.env.local (skipped for the "test" environment) [2][4][3] -.env.$env (based on the APP_ENV variable) [2][3] -.env.$env.local [2][3] This logic is typically invoked in the project's bootstrap file (e.g., config/bootstrap.php) to ensure environment variables are populated before the application kernel or other components are initialized [5][3]. Note that actual system environment variables always take precedence over values defined in these.env files [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .env.dist relevant lines =="
if [ -f .env.dist ]; then
  awk '{printf "%6d  %s\n", NR, $0}' .env.dist | sed -n '45,65p'
fi

echo
echo "== composer relevant snippets =="
if [ - f composer.json ]; then
  python3 - <<'PY'
import json, re, sys
from pathlib import Path
p=Path('composer.json')
text=p.read_text()
try: data=json.loads(text)
except Exception as e:
    print("json_parse_error", repr(e))
    for line in text.splitlines()[:240]:
        if any(s in line for s in ['scripts', 'symfony/dotenv', '"dotenv"', 'require-dev', 'require']):
            print(line)
    sys.exit(0)
print("scripts:", data.get("scripts"))
print("symfony/dotenv:", data.get("require", {}).get("symfony/dotenv"), data.get("require-dev", {}).get("symfony/dotenv"))
for key in ("require","require-dev"):
    for pk,ve in data.get(key,{}).items():
        if "dot" in pk.lower() or "symfony" in pk.lower():
            print(key, pk, ve)
PY
fi

echo
echo "== Bootstrap.php outlines/snippets =="
if [ -f src/Core/Bootstrap.php ]; then
  wc -l src/Core/Bootstrap.php
  rg -n 'Dotenv|loadEnv|env\.dist|load\\(|PHPLIST_SECRET|try|catch' src/Core/Bootstrap.php -C 8
fi

echo
echo "== repository-wide env.dist/env.load/fallback =="
rg -n 'env\.dist|loadEnv\\(|new Dotenv|Dotenv::|PHPLIST_SECRET|secret' -S src composer.json .env.dist --glob '!vendor/**' --glob '!node_modules/**' || true

Repository: phpList/core

Length of output: 1181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Bootstrap.php Bootstrap.php matches =="
rg -n 'Dotenv|loadEnv|PHPLIST_SECRET|throw new|secret' src/Core/Bootstrap.php composer.json --glob '!vendor/**' --glob '!node_modules/**' || true

echo
echo "== src/Core/Bootstrap.php line 1-180 =="
awk '{printf "%6d  %s\n", NR, $0}' src/Core/Bootstrap.php | sed -n '1,180p'

echo
echo "== src/Core/Bootstrap.php exact Dotenv/loadEnv snippets =="
awk '{printf "%6d  %s\n", NR, $0}' src/Core/Bootstrap.php | sed -n '/Dotenv/,+8p;/loadEnv/,+12p;/PHPLIST_SECRET/,+12p'

Repository: phpList/core

Length of output: 10010


🌐 Web query:

raw github symfony dotenv 6.4 src Symfony Component Dotenv Dotenv.php is_file projectRoot .env.dist

💡 Result:

In Symfony's Dotenv component (including version 6.4), the logic regarding .env.dist is implemented within the loadEnv() method of the Dotenv.php class [1][2][3]. The relevant code snippet typically appears as follows: if (is_file($path) ||!is_file($p = "$path.dist")) { $this->doLoad($overrideExistingVars, [$path]); } else { $this->doLoad($overrideExistingVars, [$p]); } This logic performs the following behavior: 1. It checks if the primary environment file (defined by $path, usually .env) exists using is_file($path) [1][2]. 2. If the primary file exists, it loads that file. 3. If the primary file does not exist, it checks if a corresponding .dist file exists (e.g., .env.dist) using is_file($p = "$path.dist") [1][2]. 4. If the .dist file exists (and the primary file does not), it loads the .dist file instead [1][2]. This mechanism is designed to support projects that provide a template file (like .env.dist or .env.example) for developers to copy and populate while ensuring the application can still load configuration if the main file is missing [1][2].

Citations:


Don’t let .env.dist become the signed secret.

loadEnvironmentVariables() also triggers Dotenv when .env.dist exists, and Symfony Dotenv will load .env.dist if .env is absent. Since PHPLIST_SECRET=%s, anyone missing Composer script output can run with the literal template value as the signing secret. Keep .env.dist strictly template-only and fail fast unless a real generated/supplied secret exists.

🤖 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 @.env.dist around lines 55 - 57, Update the environment-loading flow around
loadEnvironmentVariables so .env.dist remains template-only and PHPLIST_SECRET
never accepts the literal “%s” placeholder. Require a non-placeholder generated
or explicitly supplied secret, and fail fast when it is missing before security
tokens are used.

Source: MCP tools

Comment thread CHANGELOG.md
- `symfony/dotenv` support: configuration values are now read from a `.env` file (generated from `.env.dist` on install/update), in addition to real environment variables (#TBD)

### Changed
- `config/parameters.yml.dist` no longer contains inline `env(VAR): default` fallbacks; defaults now live in `.env.dist` (#TBD)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the current parameters template path.

This entry names config/parameters.yml.dist, while the migrated template and ScriptHandler now use config/parameters.yml. Use the current path or explicitly describe the rename.

🤖 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 `@CHANGELOG.md` at line 13, Update the changelog entry to reference the current
parameters template path, config/parameters.yml, or explicitly state that
config/parameters.yml.dist was renamed to config/parameters.yml; keep the
description of defaults moving to .env.dist unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 @.env.dist:
- Line 19: Replace the usable PHPLIST_ADMIN_PASSWORD value in the environment
template with a non-operational placeholder, and update the bootstrap logic
around the .env fallback (the code loading .env.dist near Bootstrap
initialization) to fail fast when that placeholder or an otherwise missing
deployment password is used, ensuring deployment must provide a strong
administrator password.

In `@src/Domain/Identity/Command/ImportDefaultsCommand.php`:
- Line 26: Update the default-user existence check in ImportDefaultsCommand to
recognize both the current DEFAULT_LOGIN value “test1” and the legacy “admin”
login before prompting or creating a user. Preserve the existing behavior for
fresh installations while preventing creation of a duplicate superuser when
either default account already exists.
- Line 26: Update the fallback prompt in ImportDefaultsCommand to interpolate
the `$login` value instead of hardcoding “admin”, so it reflects the
DEFAULT_LOGIN constant and any supplied login.
🪄 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 Plus

Run ID: 6a67eb95-daed-4ef7-af7e-7f0513b09068

📥 Commits

Reviewing files that changed from the base of the PR and between 8d28d3b and d24769b.

📒 Files selected for processing (3)
  • .env.dist
  • config/parameters.yml
  • src/Domain/Identity/Command/ImportDefaultsCommand.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/parameters.yml

Comment thread .env.dist
PHPLIST_DATABASE_PASSWORD=phplist
DATABASE_PREFIX=phplist_
LIST_TABLE_PREFIX=listattr_
PHPLIST_ADMIN_PASSWORD=admin

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not ship a usable default administrator password.

When .env is absent, src/Core/Bootstrap.php:162-170 loads .env.dist. This can make admin the administrator password. Replace this value with a non-operational placeholder and fail fast until deployment provides a strong password. Alternatively, stop loading .env.dist as a runtime fallback.

Proposed template change
-PHPLIST_ADMIN_PASSWORD=admin
+PHPLIST_ADMIN_PASSWORD=__SET_DURING_DEPLOYMENT__
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 19-19: [UnorderedKey] The PHPLIST_ADMIN_PASSWORD key should go before the PHPLIST_DATABASE_DRIVER key

(UnorderedKey)

🤖 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 @.env.dist at line 19, Replace the usable PHPLIST_ADMIN_PASSWORD value in the
environment template with a non-operational placeholder, and update the
bootstrap logic around the .env fallback (the code loading .env.dist near
Bootstrap initialization) to fail fast when that placeholder or an otherwise
missing deployment password is used, ensuring deployment must provide a strong
administrator password.

class ImportDefaultsCommand extends Command
{
private const DEFAULT_LOGIN = 'admin';
private const DEFAULT_LOGIN = 'test1';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'phplist:defaults:import|ImportDefaultsCommand|DEFAULT_LOGIN|loginName|test1|admin' . || true

Repository: phpList/core

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
git ls-files | rg 'ImportDefaultsCommand|Administrator|default.*import|defaults|admin' | head -200

echo
echo "== ImportDefaultsCommand =="
if [ -f src/Domain/Identity/Command/ImportDefaultsCommand.php ]; then
  ast-grep outline src/Domain/Identity/Command/999999999.php || true
  cat -n src/Domain/Identity/Command/ImportDefaultsCommand.php
fi

echo
echo "== Targeted searches =="
rg -n 'DEFAULT_LOGIN|DEFAULT_PASSWORD|DEFAULT_EMAIL|test1|admin' src tests --glob '*.php' | head -120
rg -n 'phplist:defaults:import|defaults:import' . | head -120

Repository: phpList/core

Length of output: 25665


Check legacy default users before creating test1.

phplist:defaults:import only looks for loginName = 'test1', so an existing installation with the previous default admin will prompt and create a second superuser with the default password. Treat admin as a legacy default or only run this command on fresh/empty admin tables.

🤖 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/Identity/Command/ImportDefaultsCommand.php` at line 26, Update the
default-user existence check in ImportDefaultsCommand to recognize both the
current DEFAULT_LOGIN value “test1” and the legacy “admin” login before
prompting or creating a user. Preserve the existing behavior for fresh
installations while preventing creation of a duplicate superuser when either
default account already exists.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the fallback prompt for the new login.

DEFAULT_LOGIN is now test1, but the prompt at Line 53 still says admin. Build the prompt from $login.

Proposed fix
-                $question = new Question('Enter password for default admin (login "admin"): ');
+                $question = new Question(sprintf('Enter password for default admin (login "%s"): ', $login));
🤖 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/Identity/Command/ImportDefaultsCommand.php` at line 26, Update the
fallback prompt in ImportDefaultsCommand to interpolate the `$login` value
instead of hardcoding “admin”, so it reflects the DEFAULT_LOGIN constant and any
supplied login.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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/Core/Doctrine/TablePrefixListener.php`:
- Around line 30-32: Update TablePrefixListener so setPrimaryTable preserves and
prefixes metadata indexes and uniqueConstraints using the configured table
prefix, matching the renamed primary table and AbstractPrefixedMigration SQL
identifiers. Ensure index and unique-constraint names receive the prefix
consistently while retaining their existing definitions.

In `@src/Domain/Identity/Model/Administrator.php`:
- Line 28: Align the ORM metadata names with the prefixed migration names in
Administrator.php at line 28 and TemplateImage.php at line 13. Update the
relevant unique-constraint/index names using the DATABASE_PREFIX-aware naming
convention, or skip creating those explicitly named objects when a custom prefix
is active, so Doctrine schema tooling matches AbstractPrefixedMigration and
TablePrefixListener behavior.

In `@src/Domain/Messaging/Model/ListMessage.php`:
- Around line 17-19: Align explicit index and unique-constraint names with the
runtime table prefix, either by applying the TablePrefixListener naming pattern
or by making them database-default/unprefixed. Apply this consistently at
src/Domain/Messaging/Model/ListMessage.php:17-19, Message.php:25-26,
MessageAttachment.php:12-14, Template.php:15-16, UserMessage.php:15-20,
UserMessageBounce.php:14-18, UserMessageForward.php:14-17, and
src/Domain/Subscription/Model/SubscriberList.php:28-30; update the attributes in
each location so Doctrine’s metadata names follow the configured prefix.

In `@src/Migrations/AbstractPrefixedMigration.php`:
- Around line 17-27: Restrict prefix rewriting in
AbstractPrefixedMigration::addSql() to SQL schema identifiers rather than
applying str_replace across the entire SQL text; use explicit identifier
substitution or token-aware SQL rewriting while preserving parameters and types.
Apply this root-cause fix for the SQL consumed by
Version20251028092901MySqlInit::up(); no direct change is required in
src/Migrations/Version20251028092901MySqlInit.php:13.

In `@src/Migrations/Version20251028092902MySqlUpdate.php`:
- Line 11: Apply the custom DATABASE_PREFIX policy to both migration index names
and ORM index metadata so they remain consistent when the prefix differs from
phplist_. Update Version20251028092902MySqlUpdate.php and the index definitions
in UserMessageView.php, UserStats.php, Subscriber.php,
SubscriberAttributeDefinition.php, SubscriberAttributeValue.php,
SubscriberHistory.php, and Subscription.php at the specified ranges; use the
existing prefix-aware mechanism rather than hardcoded phplist_ names, and ensure
the migration update/down paths match the entity metadata.
🪄 Autofix

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 Plus

Run ID: 83ffa9fd-132b-4f22-9b41-db0b3536f4ff

📥 Commits

Reviewing files that changed from the base of the PR and between dd49710 and 83a7216.

📒 Files selected for processing (50)
  • config/services.yml
  • src/Core/Doctrine/TablePrefixListener.php
  • src/Domain/Analytics/Model/LinkTrack.php
  • src/Domain/Analytics/Model/LinkTrackForward.php
  • src/Domain/Analytics/Model/LinkTrackMl.php
  • src/Domain/Analytics/Model/LinkTrackUmlClick.php
  • src/Domain/Analytics/Model/LinkTrackUserClick.php
  • src/Domain/Analytics/Model/UserMessageView.php
  • src/Domain/Analytics/Model/UserStats.php
  • src/Domain/Configuration/Model/Config.php
  • src/Domain/Configuration/Model/EventLog.php
  • src/Domain/Configuration/Model/I18n.php
  • src/Domain/Configuration/Model/UrlCache.php
  • src/Domain/Identity/Model/AdminAttributeDefinition.php
  • src/Domain/Identity/Model/AdminAttributeValue.php
  • src/Domain/Identity/Model/AdminLogin.php
  • src/Domain/Identity/Model/AdminPasswordRequest.php
  • src/Domain/Identity/Model/Administrator.php
  • src/Domain/Identity/Model/AdministratorToken.php
  • src/Domain/Messaging/Model/Attachment.php
  • src/Domain/Messaging/Model/Bounce.php
  • src/Domain/Messaging/Model/BounceRegex.php
  • src/Domain/Messaging/Model/BounceRegexBounce.php
  • src/Domain/Messaging/Model/ListMessage.php
  • src/Domain/Messaging/Model/Message.php
  • src/Domain/Messaging/Model/MessageAttachment.php
  • src/Domain/Messaging/Model/MessageData.php
  • src/Domain/Messaging/Model/SendProcess.php
  • src/Domain/Messaging/Model/Template.php
  • src/Domain/Messaging/Model/TemplateImage.php
  • src/Domain/Messaging/Model/UserMessage.php
  • src/Domain/Messaging/Model/UserMessageBounce.php
  • src/Domain/Messaging/Model/UserMessageForward.php
  • src/Domain/Subscription/Model/SubscribePage.php
  • src/Domain/Subscription/Model/SubscribePageData.php
  • src/Domain/Subscription/Model/Subscriber.php
  • src/Domain/Subscription/Model/SubscriberAttributeDefinition.php
  • src/Domain/Subscription/Model/SubscriberAttributeValue.php
  • src/Domain/Subscription/Model/SubscriberHistory.php
  • src/Domain/Subscription/Model/SubscriberList.php
  • src/Domain/Subscription/Model/Subscription.php
  • src/Domain/Subscription/Model/UserBlacklist.php
  • src/Domain/Subscription/Model/UserBlacklistData.php
  • src/Migrations/AbstractPrefixedMigration.php
  • src/Migrations/Version20251028092901MySqlInit.php
  • src/Migrations/Version20251028092902MySqlUpdate.php
  • src/Migrations/Version20251031072945PostGreInit.php
  • src/Migrations/Version20260204094237.php
  • src/Migrations/_template_migration.php.tpl
  • tests/Unit/Domain/Configuration/Service/Provider/ConfigProviderTest.php

Comment on lines +30 to +32
$metadata->setPrimaryTable([
'name' => $this->tablePrefix . $metadata->getTableName(),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo '== Prefix implementation =='
rg -n -C 4 'str_replace|DATABASE_PREFIX|AbstractPrefixedMigration|setPrimaryTable' \
  src/Migrations src/Core/Doctrine

echo
echo '== Static index and constraint names =='
rg -n -C 2 'ORM\\(Index|ORM\\(UniqueConstraint|phplist_' \
  src/Domain --glob '*.php'

Repository: phpList/core

Length of output: 6708


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo '== Candidate files =='
git ls-files | rg '(^|/)(TablePrefixListener|AbstractPrefixedMigration)\.php$|composer\.lock$|composer\.json$' || true

echo
echo '== TablePrefixListener =="
sed -n '1,100p' src/Core/Doctrine/TablePrefixListener.php

echo
echo '== AbstractPrefixedMigration =="
sed -n '1,120p' src/Migrations/AbstractPrefixedMigration.php

echo
echo '== Static index/constraint annotations and phplist_ references in source =="
rg -n -C 2 'Index\(|UniqueConstraint\(|phplist_|index=|constraint=' src --glob '*.php' --exclude-dir=vendor || true

echo
echo '== Doctrine ORM source reference if available locally =="
rg -n -C 3 'function setPrimaryTable|primaryTable|indexes|uniqueConstraints' -g 'ClassMetadata.php' vendor || true

echo
echo '== Required composer versions =="
python3 - <<'PY'
import json, sys, pathlib
for path in [pathlib.Path(p) for p in [pathlib.Path('composer.lock'), pathlib.Path('composer.json')] if pathlib.Path(p).exists()]:
    print(f'-- {path} --')
    data=json.loads(path.read_text())
    if path.name=='composer.lock':
        packages=data.get('packages',[])+data.get('packages-dev',[])
        for pkg in packages:
            if pkg.get('name') in {'doctrine/orm','phpstan/phpstan'}:
                print(pkg['name'], pkg.get('version'), pkg.get('source',{}).get('reference',''))
    else:
        print(data.get('require'), data.get('require-dev'))
PY

Repository: phpList/core

Length of output: 647


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo '== TablePrefixListener =='
sed -n '1,120p' src/Core/Doctrine/TablePrefixListener.php

echo
echo '== AbstractPrefixedMigration =='
sed -n '1,120p' src/Migrations/AbstractPrefixedMigration.php

echo
echo '== Static index/constraint annotations and phplist_ references in source =='
rg -n -C 2 'Index\(|UniqueConstraint\(|phplist_|index=|constraint=' src --glob '*.php' || true

echo
echo '== Doctrine ORM source reference if available locally =='
rg -n -C 3 'function setPrimaryTable|primaryTable|indexes|uniqueConstraints' -g 'ClassMetadata.php' vendor || true

echo
echo '== Required composer versions =='
python3 - <<'PY'
import json, pathlib
for path in [pathlib.Path('composer.lock'), pathlib.Path('composer.json')]:
    if not path.exists():
        continue
    print(f'-- {path} --')
    data=json.loads(path.read_text())
    if path.name == 'composer.lock':
        packages=data.get('packages',[])+data.get('packages-dev',[])
        for pkg in packages:
            if pkg.get('name') in {'doctrine/orm','phpstan/phpstan'}:
                print(pkg['name'], pkg.get('version'), pkg.get('source',{}).get('reference',''))
    else:
        print(data.get('require'), data.get('require-dev'))
PY

Repository: phpList/core

Length of output: 50369


🌐 Web query:

Doctrine ORM 3.3 ClassMetadata setPrimaryTable only name indexes uniqueConstraints metadata

💡 Result:

In Doctrine ORM, including version 3.3, the setPrimaryTable method in the ClassMetadata class is used to define the primary table mapping for an entity [1][2][3]. The method accepts an associative array as its argument [2]. You can use this array to specify the table name, as well as configure indexes and unique constraints for that table [2]. The supported structure for the array passed to setPrimaryTable is as follows [2]: name: A string specifying the table name. indexes: An array of index definitions. uniqueConstraints: An array of unique constraint definitions. Example usage: $metadata->setPrimaryTable([ 'name' => 'my_table_name', 'indexes' => [ ['name' => 'idx_example', 'columns' => ['column1', 'column2']] ], 'uniqueConstraints' => [ ['name' => 'uniq_example', 'columns' => ['column3']] ] ]); Note that if you only provide a subset of these keys, the existing values for the omitted keys in the ClassMetadata instance are preserved [2]. Additionally, in recent versions of Doctrine ORM, it is recommended to use setPrimaryTable(['name' =>...]) instead of the legacy setTableName method [4][2].

Citations:


Align index and unique-constraint names with the configured prefix.

TablePrefixListener only updates the primary table name. Doctrine ORM preserves omitted indexes and uniqueConstraints in setPrimaryTable(), while AbstractPrefixedMigration::addSql() replaces phplist_ globally in migration SQL. A custom DATABASE_PREFIX can therefore leave the database with tenant_xxx_midindex while Doctrine metadata still declares phplist_linktrack_midindex, causing schema diff/validation drift. Prefix the metadata index and unique-constraint names in the listener, or avoid global phplist_ replacement for SQL identifier names.

🤖 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/Core/Doctrine/TablePrefixListener.php` around lines 30 - 32, Update
TablePrefixListener so setPrimaryTable preserves and prefixes metadata indexes
and uniqueConstraints using the configured table prefix, matching the renamed
primary table and AbstractPrefixedMigration SQL identifiers. Ensure index and
unique-constraint names receive the prefix consistently while retaining their
existing definitions.

Source: MCP tools

*/
#[ORM\Entity(repositoryClass: AdministratorRepository::class)]
#[ORM\Table(name: 'phplist_admin')]
#[ORM\Table(name: 'admin')]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'src/(Domain|.*Migration.*|.*Listener.*)|phpunit|doctrine' | sed -n '1,200p'

echo "== target files =="
for f in src/Domain/Identity/Model/Administrator.php src/Domain/Messaging/Model/TemplateImage.php; do
  if [ -f "$f" ]; then
    echo "--- $f"
    nl -ba "$f" | sed -n '1,220p'
  fi
done

echo "== searches for prefixes/listeners/migrations/attributes =="
rg -n "TablePrefixListener|AbstractPrefixedMigration|primaryTable|Table\\(|indexes|options|id=|unique|index|phplist_" . -g '!vendor/**' -g '!node_modules/**' | sed -n '1,260p'

Repository: phpList/core

Length of output: 11699


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target relevant files =="
for f in src/Domain/Identity/Model/Administrator.php src/Domain/Messaging/Model/TemplateImage.php src/Core/Doctrine/TablePrefixListener.php; do
  if [ -f "$f" ]; then
    echo "--- $f"
    awk '{printf "%3d\t%s\n", NR, $0}' "$f" | sed -n '1,220p'
  fi
done

echo "== migration files outline =="
fd -a 'migration|Migration' src . | sed -n '1,200p' | while IFS= read -r f; do
  case "$f" in
    *.php) echo "--- $f"; awk '{printf "%3d\t%s\n", NR, $0}' "$f" | sed -n '1,260p';;
  esac
done

echo "== searches =="
rg -n "AbstractPrefixedMigration|TablePrefixListener|primaryTable|Table\\(|indexes|uniqueConstraints|Options|phplist_admin_loginnameidx|phplist_templateimage_templateidx|doctrine:generate|schema:generate|ORM\\:Table|ORM\\:Index|ORM\\:UniqueConstraint|ORM\\:JoinColumn" . --glob '*.php' --glob '*.yml' --glob '*.yaml' | sed -n '1,500p'

Repository: phpList/core

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration contexts =="
for f in src/Migrations/Version20251028092901MySqlInit.php src/Migrations/Version20251028092902MySqlUpdate.php src/Migrations/Version20251031072945PostGreInit.php; do
  echo "--- $f"
  awk '{printf "%3d\t%s\n", NR, $0}' "$f" | sed -n '1,240p'
done

echo "== migrations that add/admin/template indexes around up/down =="
rg -n "admin_login|templateimage_template|CREATE INDEX|CREATE UNIQUE|DROP INDEX|RENAME INDEX|phplist_admin_loginnameidx|phplist_templateimage_templateidx" src/Migrations tests -g '*.php' | sed -n '1,260p'

Repository: phpList/core

Length of output: 50369


🌐 Web query:

Doctrine ORM tablePrefix attribute unique constraint index name setPrimaryTable only table name

💡 Result:

There is no built-in "tablePrefix" attribute in Doctrine ORM [1][2]. To implement table prefixing, the recommended approach is to use a LoadClassMetadata listener that modifies the metadata at runtime [1][3]. For implementing a prefix via a listener: 1. You can hook into the loadClassMetadata event [1][3]. 2. Inside the listener, you use the ClassMetadata::setPrimaryTable method to prepend your prefix to the table name [1][4][3]. 3. Note that this listener must be registered before the EntityManager is initialized to ensure it is applied to all entities [1][3]. Regarding "setPrimaryTable" and table names: * setPrimaryTable is the correct method for updating the primary table configuration [4]. * It accepts an array where you can define the 'name' key to set the desired table name [1][3]. Regarding unique constraints and index names: * Doctrine ORM does not automatically apply table prefixes to index or unique constraint names defined in your entity configuration [5]. * If you need unique constraint or index names to also be prefixed, you must manually include the prefix in the 'name' attribute of the #[UniqueConstraint] or #[Index] attributes [5]. * Please note that defining uniqueConstraints and indexes inside the #[Table] attribute is deprecated and often ignored in modern Doctrine versions; it is recommended to define these as separate class-level attributes instead [6][7]. For a robust, automated solution that handles indexes, unique constraints, and sequences, community bundles such as DoctrinePrefixBundle are often used to avoid manual implementation [5].

Citations:


🌐 Web query:

Doctrine ORM tablePrefix setPrimaryTable indexedColumns indexes uniqueConstraints

💡 Result:

In Doctrine ORM, configuration of table-level metadata such as indexes and unique constraints is handled through class-level attributes, annotations, or XML/YAML mapping [1][2][3]. Table Prefixes Doctrine does not provide a native configuration option for global table prefixes. The authoritative approach for implementing them is to use a loadClassMetadata event listener [4][5]. By registering this listener before the EntityManager is initialized, you can dynamically modify the class metadata to prepend a prefix to the table name and, if necessary, join table names [4][6]. Indexes and Unique Constraints Indexes and unique constraints are defined at the entity class level using the #[Index] and #[UniqueConstraint] attributes (or their annotation/XML equivalents) [1][2][7][3]. 1. Usage: You apply these attributes to the entity class [1][7]. They provide hints to the SchemaTool to generate the corresponding database structures [1][3]. 2. Configuration: - Indexes: Require a fields or columns definition and optionally accept a name and platform-specific options (e.g., where clause for partial indexes) [1][7]. - Unique Constraints: Require a fields or columns definition [1][3]. 3. Mapping Note: While some earlier documentation or implementations suggested nested configuration within the #[Table] attribute, current best practice and driver behavior emphasize defining these as independent class-level attributes [8]. Defining them directly on the class ensures they are correctly picked up by the attribute driver, avoiding potential issues where nested definitions are ignored [8]. Internal Metadata (setPrimaryTable) The setPrimaryTable method in ClassMetadata is used to define the primary table's metadata programmatically [9][10]. While primarily used internally, it is the mechanism employed by the table prefix listener to update the table name dynamically [4][5].

Citations:


Prefix the ORM meta-index/constraint names to match the SQL migration names.

TablePrefixListener adds the configured prefix only to the table name (primaryTable.name). AbstractPrefixedMigration::* will rename phplist_admin_loginnameidx/phplist_templateimage_templateidx according to migration SQL, but Doctrine schema tooling still sees #ADMINISTRATOR_UNIQUE_CONSTRAINT_NAME/#TEMPLATEIMAGE_INDEX_NAME. Use the same DATABASE_PREFIX-aware names here, or avoid creating those named objects in the custom-prefix path.

📍 Affects 2 files
  • src/Domain/Identity/Model/Administrator.php#L28-L28 (this comment)
  • src/Domain/Messaging/Model/TemplateImage.php#L13-L13
🤖 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/Identity/Model/Administrator.php` at line 28, Align the ORM
metadata names with the prefixed migration names in Administrator.php at line 28
and TemplateImage.php at line 13. Update the relevant unique-constraint/index
names using the DATABASE_PREFIX-aware naming convention, or skip creating those
explicitly named objects when a custom prefix is active, so Doctrine schema
tooling matches AbstractPrefixedMigration and TablePrefixListener behavior.

Comment on lines +17 to 19
#[ORM\Table(name: 'listmessage')]
#[ORM\UniqueConstraint(name: 'phplist_listmessage_messageid', columns: ['messageid', 'listid'])]
#[ORM\Index(name: 'phplist_listmessage_listmessageidx', columns: ['listid', 'messageid'])]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'phplist_|DATABASE_PREFIX|str_replace' \
  src/Migrations/AbstractPrefixedMigration.php \
  src/Migrations

rg -n 'phplist_(listmessage|message|message_attachment|template|usermessage|user_message_bounce|user_message_forward|list)_' \
  src/Domain/Messaging/Model \
  src/Domain/Subscription/Model

Repository: phpList/core

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AbstractPrefixedMigration =="
cat -n src/Migrations/AbstractPrefixedMigration.php

echo "== relevant model annotations =="
for f in \
  src/Domain/Messaging/Model/ListMessage.php \
  src/Domain/Messaging/Model/Message.php \
  src/Domain/Messaging/Model/MessageAttachment.php \
  src/Domain/Messaging/Model/Template.php \
  src/Domain/Messaging/Model/UserMessage.php \
  src/Domain/Messaging/Model/UserMessageBounce.php \
  src/Domain/Messaging/Model/UserMessageForward.php \
  src/Domain/Subscription/Model/SubscriberList.php
do
  echo "--- $f"
  sed -n '1,80p' "$f" | cat -n
done

echo "== migration versions mentioning these tables/indexes =="
rg -n "class Version|\\$this->addSql|RENAME INDEX|CREATE INDEX|CREATE TABLE|phplist_(listmessage|message|message_attachment|template|usermessage|user_message_bounce|user_message_forward|list)(_|$)" src/Migrations \
  | awk '/class Version/ {print; flag=1} flag && /phplist_(listmessage|message|message_attachment|template|usermessage|user_message_bounce|user_message_forward|list)(_|$)/ {print} flag && /^[[:space:]]*};?$/ {flag=0}'

echo "== PHP files containing Doctrine prefix listener code =="
rg -n "TablePrefixLister|TablePrefixesSchemaManager|prefix|prefixes|PrefixesSchemaManager" src -g '*.php'

Repository: phpList/core

Length of output: 24589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

indexes = [
    ("src/Domain/Messaging/Model/ListMessage.php", ["phplist_listmessage_messageid", "phplist_listmessage_listmessageidx"]),
    ("src/Domain/Messaging/Model/Message.php", ["phplist_message_uuididx"]),
    ("src/Domain/Messaging/Model/MessageAttachment.php", ["phplist_message_attachment_messageattidx", "phplist_message_attachment_messageidx"]),
    ("src/Domain/Messaging/Model/Template.php", ["phplist_template_title"]),
    ("src/Domain/Messaging/Model/UserMessage.php", ["phplist_usermessage_enteredindex", "phplist_usermessage_messageidindex", "phplist_usermessage_statusidx", "phplist_usermessage_useridindex", "phplist_usermessage_viewedidx"]),
    ("src/Domain/Messaging/Model/UserMessageBounce.php", ["phplist_user_message_bounce_bounceidx", "phplist_user_message_bounce_msgidx", "phplist_user_message_bounce_umbindex", "phplist_user_message_bounce_useridx"]),
    ("src/Domain/Messaging/Model/UserMessageForward.php", ["phplist_user_message_forward_messageidx", "phplist_user_message_forward_useridx", "phplist_user_message_forward_usermessageidx"]),
    ("src/Domain/Subscription/Model/SubscriberList.php", ["phplist_list_nameidx", "phplist_list_listorderidx"]),
]

class_mapping = {
    "src/Domain/Messaging/Model/ListMessage.php": "listmessage",
    "src/Domain/Messaging/Model/Message.php": "message",
    "src/Domain/Messaging/Model/MessageAttachment.php": "message_attachment",
    "src/Domain/Messaging/Model/Template.php": "template",
    "src/Domain/Messaging/Model/UserMessage.php": "usermessage",
    "src/Domain/Messaging/Model/UserMessageBounce.php": "user_message_bounce",
    "src/Domain/Messaging/Model/UserMessageForward.php": "user_message_forward",
    "src/Domain/Subscription/Model/SubscriberList.php": "list",
}

prefix = "phplist_"
non_default = "myapp_"
for path, names in indexes:
    cls = class_mapping[path]
    derived_non_default = [re.sub(r'^'+re.escape(prefix), non_default, n, count=1) for n in names]
    print(f"{path}:{cls}")
    for i, (n, nn) in enumerate(zip(names, derived_non_default), 1):
        print(f"  {i}. {n} -> {nn}")

for file in Path("src/Migrations/Version*.php").glob("*"):
    text = file.read_text(errors="ignore")
    hits = []
    for name, _ in ((n,a) for _,names in indexes for n,a in zip(names, class_mapping.values()) for _ in names):
        pass
    full_names = [n for _,names in indexes for n in names]
    for line in text.splitlines():
        for n in full_names:
            if n in line:
                hits.append((file.name, line))
                break
    if hits:
        print(f"--- {file.name}")
        for h in hits[:8]:
            print(f"{h[0]}:{h[1]}")
PY

Repository: phpList/core

Length of output: 2068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "class (Doctrine.*Prefix|.*TablePrefix|PrefixesSchema|TablePrefixesSchema|TablePrefixLister|TablePrefixes.*Schema|PrefixSchemaManager|*Prefix)" src -g '*.php' || true

mig_files=$(find src/Migrations -maxdepth 1 \( -name 'Version*.php' -o -name 'AbstractPrefixedMigration.php' \) -printf '%f\n' | sort)
echo "== first migration matches around listed tables/indexes =="
python3 - <<'PY'
from pathlib import Path
import re

targets = {
    "src/Domain/Messaging/Model/ListMessage.php": ["phplist_listmessage_messageid", "phplist_listmessage_listmessageidx"],
    "src/Domain/Messaging/Model/Message.php": ["phplist_message_uuididx"],
    "src/Domain/Messaging/Model/MessageAttachment.php": ["phplist_message_attachment_messageattidx", "phplist_message_attachment_messageidx"],
    "src/Domain/Messaging/Model/Template.php": ["phplist_template_title"],
    "src/Domain/Messaging/Model/UserMessage.php": ["phplist_usermessage_enteredindex", "phplist_usermessage_messageidindex", "phplist_usermessage_statusidx", "phplist_usermessage_useridindex", "phplist_usermessage_viewedidx"],
    "src/Domain/Messaging/Model/UserMessageBounce.php": ["phplist_user_message_bounce_bounceidx", "phplist_user_message_bounce_msgidx", "phplist_user_message_bounce_umbindex", "phplist_user_message_bounce_useridx"],
    "src/Domain/Messaging/Model/UserMessageForward.php": ["phplist_user_message_forward_messageidx", "phplist_user_message_forward_useridx", "phplist_user_message_forward_usermessageidx"],
    "src/Domain/Subscription/Model/SubscriberList.php": ["phplist_list_nameidx", "phplist_list_listorderidx"],
}
names = {n for names in targets.values() for n in names}

paths = sorted(Path("src/Migrations").glob("Version*.php") | filter(lambda p: "Version20251028092902MySqlUpdate.php" not in str(p)))[-10:]
for path in paths:
    text = path.read_text(errors="ignore")
    if any(n in text for n in names):
        print(f"--- {path.name}")
        for i,line in enumerate(text.splitlines(),1):
            if any(n in line for n in names):
                print(f"{i:04d}: {line}")
PY

Repository: phpList/core

Length of output: 690


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prefix-related classes in src =="
rg -n "class .*Prefix|class .*Prefixes.*Schema|TablePrefixLister|TablePrefixesSchema|PrefixesSchemaManager|TablePrefix" src -g '*.php' || true

echo "== all ORM mapping files with explicit index/constraint names for listed entities =="
for f in \
  src/Domain/Messaging/Model/ListMessage.php \
  src/Domain/Messaging/Model/Message.php \
  src/Domain/Messaging/Model/MessageAttachment.php \
  src/Domain/Messaging/Model/Template.php \
  src/Domain/Messaging/Model/UserMessage.php \
  src/Domain/Messaging/Model/UserMessageBounce.php \
  src/Domain/Messaging/Model/UserMessageForward.php \
  src/Domain/Subscription/Model/SubscriberList.php
do
  echo "--- $f"
  rg -n "#\\[ORM\\.(Index|UniqueConstraint)\\(" "$f" && echo
done

Repository: phpList/core

Length of output: 3157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TablePrefixListener =="
cat -n src/Core/Doctrine/TablePrefixListener.php

echo "== ORM index/constraint annotations in listed files =="
for f in \
  src/Domain/Messaging/Model/ListMessage.php \
  src/Domain/Messaging/Model/Message.php \
  src/Domain/Messaging/Model/MessageAttachment.php \
  src/Domain/Messaging/Model/Template.php \
  src/Domain/Messaging/Model/UserMessage.php \
  src/Domain/Messaging/Model\UserMessageBounce.php \
  src/Domain/Messaging/Model\UserMessageForward.php \
  src/Domain/Subscription/Model/SubscriberList.php
do
  echo "--- $f"
  rg -n "#\\\\\\[ORM\\.(Index|UniqueConstraint)\\(" "$f" || true
done

echo "== migration matches for listed indexes =="
python3 - <<'PY'
from pathlib import Path
import re

files = sorted(Path("src/Migrations").glob("Version*.php"))
names = {
    "phplist_listmessage_messageid",
    "phplist_listmessage_listmessageidx",
    "phplist_message_uuididx",
    "phplist_message_attachment_messageattidx",
    "phplist_message_attachment_messageidx",
    "phplist_template_title",
    "phplist_usermessage_enteredindex",
    "phplist_usermessage_messageidindex",
    "phplist_usermessage_statusidx",
    "phplist_usermessage_useridindex",
    "phplist_usermessage_viewedidx",
    "phplist_user_message_bounce_bounceidx",
    "phplist_user_message_bounce_msgidx",
    "phplist_user_message_bounce_umbindex",
    "phplist_user_message_bounce_useridx",
    "phplist_user_message_forward_messageidx",
    "phplist_user_message_forward_useridx",
    "phplist_user_message_forward_usermessageidx",
    "phplist_list_nameidx",
    "phplist_list_listorderidx",
}
for path in files:
    text = path.read_text(errors="ignore")
    hits = [(i, line.strip()) for i, line in enumerate(text.splitlines(),1) if any(n in line for n in names)]
    if hits:
        print(f"--- {path.name}")
        for i,line in hits:
            print(f"{i}: {line}")
        print()
PY

Repository: phpList/core

Length of output: 9048


Align entity index and constraint names with the table prefix.

TablePrefixListener changes the primary table name at runtime, but explicit #[ORM\Index(..., name: 'phplist_*')] and #[ORM\UniqueConstraint(..., name: 'phplist_*')] names do not change. This can make Doctrine detect changed indexes/constraints with non-default DATABASE_PREFIX. Apply the same listener pattern to explicit index/constraint names, or keep these names database-default/unprefixed.

📍 Affects 8 files
  • src/Domain/Messaging/Model/ListMessage.php#L17-L19 (this comment)
  • src/Domain/Messaging/Model/Message.php#L25-L26
  • src/Domain/Messaging/Model/MessageAttachment.php#L12-L14
  • src/Domain/Messaging/Model/Template.php#L15-L16
  • src/Domain/Messaging/Model/UserMessage.php#L15-L20
  • src/Domain/Messaging/Model/UserMessageBounce.php#L14-L18
  • src/Domain/Messaging/Model/UserMessageForward.php#L14-L17
  • src/Domain/Subscription/Model/SubscriberList.php#L28-L30
🤖 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/Messaging/Model/ListMessage.php` around lines 17 - 19, Align
explicit index and unique-constraint names with the runtime table prefix, either
by applying the TablePrefixListener naming pattern or by making them
database-default/unprefixed. Apply this consistently at
src/Domain/Messaging/Model/ListMessage.php:17-19, Message.php:25-26,
MessageAttachment.php:12-14, Template.php:15-16, UserMessage.php:15-20,
UserMessageBounce.php:14-18, UserMessageForward.php:14-17, and
src/Domain/Subscription/Model/SubscriberList.php:28-30; update the attributes in
each location so Doctrine’s metadata names follow the configured prefix.

Source: MCP tools

Comment on lines +17 to +27
protected function addSql(string $sql, array $params = [], array $types = []): void
{
parent::addSql(
str_replace(
self::DEFAULT_PREFIX,
$this->getTablePrefix(),
$sql
),
$params,
$types
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)AbstractPrefixedMigration\.php$|(^|/)Version20251028092901MySqlInit\.php$|initial_schema\.sql$' || true

echo
echo "AbstractPrefixedMigration.php outline:"
ast-grep outline src/Migrations/AbstractPrefixedMigration.php --view expanded || true

echo
echo "AbstractPrefixedMigration.php:"
cat -n src/Migrations/AbstractPrefixedMigration.php

echo
echo "Version20251028092901MySqlInit.php:"
cat -n src/Migrations/Version20251028092901MySqlInit.php

echo
echo "initial_schema.sql lines count and sample:"
wc -l src/Migrations/initial_schema.sql || true
sed -n '1,220p' src/Migrations/initial_schema.sql | nl -ba

echo
echo "grep for phplist_ and DEFAULT_PREFIX refs in Migrations:"
rg -n "DEFAULT_PREFIX|phplist_|Version20251028092901MySqlInit|initial_schema.sql|addSql" src/Migrations || true

echo
echo "Read-only probe: demonstrate PHP-like full-text replacement behavior for non-identifier tokens"
python3 - <<'PY'
sql = "INSERT INTO users (name) VALUES ('prefix_phplist_abc');"
prefix = "phplist_"
print(sql.replace(prefix, ""))
PY

Repository: phpList/core

Length of output: 3628


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "initial_schema.sql occurrences:"
rg -n "phplist_" src/Migrations/initial_schema.sql | head -n 120 || true
echo
echo "initial_schema.sql total matches:"
rg -c "phplist_" src/Migrations/initial_schema.sql || true

echo
echo "initial_schema.sql line count:"
wc -l src/Migrations/initial_schema.sql || true

echo
echo "initial_schema.sql sample around occurrences:"
python3 - <<'PY'
from pathlib import Path
lines = Path('src/Migrations/initial_schema.sql').read_text().splitlines()
matches = {i for i,l in enumerate(lines,1) if 'phplist_' in l}
for i in sorted(matches)[:40]:
    lo = max(1, i-3); hi = min(len(lines), i+3)
    print(f"\n--- lines {lo}-{hi} ---")
    for n in range(lo, hi+1):
        print(f"{n:4}: " + lines[n-1])
PY

echo
echo "Behavioral probe: full-text replacement matches non-identifier tokens"
python3 - <<'PY'
sql = """
-- phplist legacy comment
INSERT INTO users (name) VALUES ('prefix_phplist_abc');
SELECT 'phplist_value';
"""
print(sql.replace('phplist_', ''))
PY

Repository: phpList/core

Length of output: 16111


Scope DATABASE_PREFIX replacement to schema identifiers.

AbstractPrefixedMigration::addSql() replaces phplist_ across the full SQL text, so Version20251028092901MySqlInit::up() can unintentionally change comments or any non-identifier phplist_ token in initial_schema.sql. Use explicit identifier rewriting or token-aware SQL rewriting instead.

[data_Integrity_and_integration]

📍 Affects 2 files
  • src/Migrations/AbstractPrefixedMigration.php#L17-L27 (this comment)
  • src/Migrations/Version20251028092901MySqlInit.php#L13-L13
🤖 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/Migrations/AbstractPrefixedMigration.php` around lines 17 - 27, Restrict
prefix rewriting in AbstractPrefixedMigration::addSql() to SQL schema
identifiers rather than applying str_replace across the entire SQL text; use
explicit identifier substitution or token-aware SQL rewriting while preserving
parameters and types. Apply this root-cause fix for the SQL consumed by
Version20251028092901MySqlInit::up(); no direct change is required in
src/Migrations/Version20251028092901MySqlInit.php:13.

use Doctrine\DBAL\Schema\Schema;

final class Version20251028092902MySqlUpdate extends AbstractMigration
final class Version20251028092902MySqlUpdate extends AbstractPrefixedMigration

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'Version20251028|AbstractPrefixedMigration|TablePrefixListener|.*\.php$' . | head -200

echo
echo "Target files existence and sizes:"
for f in \
  src/Migrations/Version20251028092902MySqlUpdate.php \
  src/Migrations/AbstractPrefixedMigration.php \
  src/Migrations/Version20251028092901MySqlInit.php \
  src/Domain/Analytics/Model/UserMessageView.php \
  src/Domain/Analytics/Model/UserStats.php \
  src/Domain/Subscription/Model/Subscriber.php \
  src/Domain/Subscription/Model/SubscriberAttributeDefinition.php \
  src/Domain/Subscription/Model/SubscriberAttributeValue.php \
  src/Domain/Subscription/Model/SubscriberHistory.php \
  src/Domain/Subscription/Model/Subscription.php
do
  if [ -f "$f" ]; then wc -l "$f"; else echo "MISSING $f"; fi
done

Repository: phpList/core

Length of output: 14861


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Specific target files:"
for f in \
  src/Migrations/Version20251028092902MySqlUpdate.php \
  src/Migrations/AbstractPrefixedMigration.php \
  src/Migrations/Version20251028092901MySqlInit.php \
  src/Core/Doctrine/TablePrefixListener.php \
  src/Domain/Analytics/Model/UserMessageView.php \
  src/Domain/Analytics/Model/UserStats.php \
  src/Domain/Subscription/Model/Subscriber.php \
  src/Domain/Subscription/Model/SubscriberAttributeDefinition.php \
  src/Domain/Subscription/Model/SubscriberAttributeValue.php \
  src/Domain/Subscription/Model/SubscriberHistory.php \
  src/Domain/Subscription/Model/Subscription.php
do
  if [ -f "$f" ]; then
    echo "--- $f"
    wc -l "$f"
    cat -n "$f"
  else
    echo "MISSING $f"
  fi
done

Repository: phpList/core

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Listener and env wiring:"
rg -n "TablePrefixListener|DATABASE_PREFIX|tablePrefix|loadClassMetadata" src -g '*.php'

echo
echo "Entity metadata annotations (target files, relevant attrs only):"
python3 - <<'PY'
import re, pathlib
for path in [
 pathlib.Path("src/Domain/Analytics/Model/UserMessageView.php"),
 pathlib.Path("src/Domain/Analytics/Model/UserStats.php"),
 pathlib.Path("src/Domain/Subscription/Model/Subscriber.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberAttributeDefinition.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberAttributeValue.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberHistory.php"),
 pathlib.Path("src/Domain/Subscription/Model/Subscription.php")]:
    text=pathlib.Path(path).read_text()
    if path.name=="Subscriber.php":
        lines=text.splitlines()
        for i,line in enumerate(lines[:50],1):
            if ("ORM\Table" in line or "ORM\Index" in line or "ORM\UniqueConstraint" in line or "ORM\JoinColumn" in line or "ORM\JoinTable" in line) and ("name:" in line):
                print(f"{path}:{i}:{line}")
        continue
    for i,line in enumerate(text.splitlines(),1):
        if ("ORM\Table" in line or "ORM\Index" in line or "ORM\UniqueConstraint" in line or "ORM\JoinColumn" in line or "ORM\JoinTable" in line) and ("name:" in line):
            print(f"{path}:{i}:{line}")
PY

echo
echo "Migration update SQL names with prefix (TargetUpdate.php first 165 lines):"
python3 - <<'PY'
import subprocess, pathlib, re
text = pathlib.Path("src/Migrations/Version20251028092902MySqlUpdate.php").read_text()
m = re.search(r"public function up\(Schema \$schema\): void\s*{(.*?)\n    public function down", text, re.S)
up = m.group(1) if m else ""
hits = []
for i,line in enumerate(up.splitlines(), 1):
    if re.search(r"[A-Za-z_][A-Za-z0-9_]*(?:idx|[Ff][Kk]|PK|idx|[Uu][Nn][Ii][Qq](?:[Ii]D)?|[Ss]tatus|[Ee]ntry|[Dd]ateindex|[Ll]istindex|[Ii]temindex)|\bINDEX\b", line) and ("phplist" in line or "IDX_" in line or "FK_" in line or "foreignkey" in line.lower() or "`" in line):
        hits.append((i,line))
print(f"{len(hits)} named schema-object occurrences")
prefix_re = re.compile(r"phplist_|^idx|uniqueindex|_?[A-Za-z]\w*idx|[Ff]oreign[A-Za-z]*key|[Uu]nique[A-Za-z0-9]*")
for i,line in hits[:80]:
    print(f"{i}: {line.strip()}")
PY

echo
echo "Search for hardcoded unprefixed names in target entity files:"
python3 - <<'PY'
import pathlib, re
for path in [
 pathlib.Path("src/Domain/Subscription/Model/Subscriber.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberAttributeDefinition.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberAttributeValue.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberHistory.php"),
 pathlib.Path("src/Domain/Subscription/Model/Subscription.php")]:
    text=pathlib.Path(path).read_text()
    # print ORM Table/Index/Unique/ForeignKey line numbers where name field is present
    for i,line in enumerate(text.splitlines(),1):
        if 'name:' in line and any(s in line for s in ("ORM\\Table", "ORM\\Index", "ORM\\UniqueConstraint", "ORM\\JoinColumn", "ORM\\JoinTable")):
            print(f"{path}:{i}:{line.strip()}")
PY

Repository: phpList/core

Length of output: 1276


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Dependency versions/config snippets:"
for f in composer.json composer.lock config/* dbconfig.php app/config/services.php src/Core/DoctrineMappingPass.php; do
  [ -f "$f" ] && { echo "--- $f"; if [ "$f" = "composer.json" ]; then jq '.require' "$f"; else sed -n '1,220p' "$f"; fi; }
done

echo
echo "Target entity named schema mappings (selected attrs only):"
python3 - <<'PY'
import pathlib
paths = [
 pathlib.Path("src/Domain/Subscription/Model/Subscriber.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberAttributeDefinition.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberAttributeValue.php"),
 pathlib.Path("src/Domain/Subscription/Model/SubscriberHistory.php"),
 pathlib.Path("src/Domain/Subscription/Model/Subscription.php")
]
for path in paths:
    text = path.read_text()
    print(f"--- {path}")
    for i, line in enumerate(text.splitlines(), 1):
        if "name:" in line and any(s in line for s in ("ORM\\Table", "ORM\\Index", "ORM\\UniqueConstraint", "ORM\\JoinColumn", "ORM\\JoinTable")):
            print(f"{i}: {line.strip()}")
PY

echo
echo "Behavioral code-property probe: parse fixed migration SQL up statements into schema names and compare with hardcoded ORM names in target files."
python3 - <<'PY'
import pathlib, re

def schema_objs(sql: str):
    # Capture names without applying prefix rewrite, only for token-level insight
    names = []
    for m in re.finditer(r'''# Rename/drop/alter/add references: CREATE INDEX|REN *?*|DROP INDEX|ADD CONSTRAINT|CREATE INDEX|CREATE UNIQUE INDEX|FOREIGN KEY|INDEX|CONSTR''' ...): pass
    return names

update = pathlib.Path("src/Migrations/Version20251028092902MySqlUpdate.php").read_text()
main = re.search(r"public function up\\$schema\\): void\\s*{(.*?)}\\s*public function down", update, re.S).group(1)
print("Migration up line count:", len(main.splitlines()))
for i, line in enumerate(main.splitlines(), 1):
    if "rename" in line.lower() or "renameindex" in line.lower() or "create index" in line.lower() or "add constraint" in line.lower() or "drop index" in line.lower() or "foreignkey" in line.lower():
        if "phplist" in line:
            print(f"{i}: {line.strip()}")
PY

Repository: phpList/core

Length of output: 19370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Doctrine/onlyorm TablePrefixListener usages:"
rg -n "TablePrefixListener|OnlyOrmTablesFilter|loadClassMetadata|database_prefix|PHPLIST_DATABASE_PREFIX|DATABASE_PREFIX" src config -g '*.php' -g '*.yml' -g '*.yaml' -g '*.xml'

echo
echo "SQL named object extraction from target migration:"
python3 - <<'PY'
import pathlib, re

text = pathlib.Path('src/Migrations/Version20251028092902MySqlUpdate.php').read_text()
main = re.search(r'public function up\(Schema \\\$schema\): void\s*{(.*?)\n    public function down', text, re.S).group(1)
for i, line in enumerate(main.splitlines(), 1):
    line = line.strip()
    sql = re.sub(r'#.*', '', line)
    if any(name in line.lower() or 'index' in line.lower() or 'constraint' in line.lower() or 'foreign' in line.lower() for name in [
        'phplist_', 'idx_', 'fk_', 'unique', 'key'
    ]):
        print(f'{i}: {sql}')
PY

echo
echo "Read-only static summary of target entity/schema name policy:"
python3 - <<'PY'
import pathlib, re
target = [
 'src/Domain/Analytics/Model/UserMessageView.php',
 'src/Domain/Analytics/Model/UserStats.php',
 'src/Domain/Subscription/Model/Subscriber.php',
 'src/Domain/Subscription/Model/SubscriberAttributeDefinition.php',
 'src/Domain/Subscription/Model/SubscriberAttributeValue.php',
 'src/Domain/Subscription/Model/SubscriberHistory.php',
 'src/Domain/Subscription/Model/Subscription.php'
]
for path in target:
    lines = pathlib.Path(path).read_text().splitlines()
    print(f'--- {path}')
    for i, line in enumerate(lines, 1):
        if any(n in line for n in ["ORM\\Table", "ORM\\Index", "ORM\\UniqueConstraint", 'ORM\\JoinColumn', "ORM\\JoinTable"]) and 'name:' in line:
            print(f'{i}: {line.strip()}')
PY

Repository: phpList/core

Length of output: 1293


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target migration SQL named schema objects:"
python3 - <<'PY'
from pathlib import Path
text = Path('src/Migrations/Version20251028092902MySqlUpdate.php').read_text()

# Simple extraction for occurrences in lines that name indexes/constraints/aliases.
for i, line in enumerate(text.splitlines(), 1):
    sql = line.split('addSql', 1)[-1] if 'addSql' in line else ''
    if any(part in sql for part in ['phplist_', 'IDX_', 'FK_', 'unique', 'foreignkey', 'msgidx', 'useridx', 'usermsgidx', 'dateindex', 'listindex', 'itemindex', 'email', 'foreignkey', 'uuididx']):
        print(f'{i}: {line.strip()}')
PY

echo
echo "Existing migration down renamed index examples for target entity:"
rg -n "phplist_user_(message_view|stats)|subscribpage|user_message_view|userstats|subscription|subscribpage|message_view|usermsgidx|dateindex|listindex|itemindex" src/Migrations src/Core src/Domain -g '*.php' -g '*.yml' -g '*.yaml' | head -200

Repository: phpList/core

Length of output: 50368


Make the custom prefix policy apply consistently.

AbstractPrefixedMigration replaces phplist_ in tables and indexes, but TablePrefixListener only prefixes the ORM table name. Run Doctrine schema validation with DATABASE_PREFIX set to something other than phplist_; the update/down path assumes the index names in Version20251028092902MySqlUpdate.php, while the listed entity metadata uses the same hardcoded names.

📍 Affects 8 files
  • src/Migrations/Version20251028092902MySqlUpdate.php#L11-L11 (this comment)
  • src/Domain/Analytics/Model/UserMessageView.php#L14-L14
  • src/Domain/Analytics/Model/UserStats.php#L13-L13
  • src/Domain/Subscription/Model/Subscriber.php#L27-L27
  • src/Domain/Subscription/Model/SubscriberAttributeDefinition.php#L15-L15
  • src/Domain/Subscription/Model/SubscriberAttributeValue.php#L12-L12
  • src/Domain/Subscription/Model/SubscriberHistory.php#L14-L14
  • src/Domain/Subscription/Model/Subscription.php#L25-L25
🤖 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/Migrations/Version20251028092902MySqlUpdate.php` at line 11, Apply the
custom DATABASE_PREFIX policy to both migration index names and ORM index
metadata so they remain consistent when the prefix differs from phplist_. Update
Version20251028092902MySqlUpdate.php and the index definitions in
UserMessageView.php, UserStats.php, Subscriber.php,
SubscriberAttributeDefinition.php, SubscriberAttributeValue.php,
SubscriberHistory.php, and Subscription.php at the specified ranges; use the
existing prefix-aware mechanism rather than hardcoded phplist_ names, and ensure
the migration update/down paths match the entity metadata.

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