Skip to content

fix(DynamicContentSubscriber): port Mautic 5 migration to Mautic 6#2

Open
edouard-mangel wants to merge 17 commits into6.xfrom
fix/dynamic-content-subscriber-mautic6
Open

fix(DynamicContentSubscriber): port Mautic 5 migration to Mautic 6#2
edouard-mangel wants to merge 17 commits into6.xfrom
fix/dynamic-content-subscriber-mautic6

Conversation

@edouard-mangel
Copy link
Copy Markdown

Summary

Port of PR fix/dynamic-content-subscriber-mautic5 to Mautic 6.

  • Completes the DynamicContentSubscriber migration for Mautic 6 (uses ContactFiltersEvaluateEvent, ContactFilterMatcher, and the polyfill trait)
  • Refactors ContactFilterMatcher to remove class_alias and use the polyfill trait directly
  • Removes explicit service definitions in config.php, relying on autowiring via services.php
  • Replaces deprecated DBAL execute() with executeQuery()
  • Fixes empty-operator handling in MatchFilterForLeadTrait when no custom items are linked
  • Adds unit tests for ContactFilterMatcher and DynamicContentSubscriber
  • Applies Rector rules (constructor promotion, str_starts_with, non-capturing catches)
  • Updates PHPStan baseline for Mautic 6

Conflict resolution

One cherry-pick conflict in DynamicContentSubscriberTest.php: the 5.x branch used ->will($this->onConsecutiveCalls(...)) (older PHPUnit API) while 6.x already used ->willReturnOnConsecutiveCalls(...) (PHPUnit 10+ API). Kept the 6.x version.

CI

Updated workflow to test against PHP 8.2 and Mautic 6.x branch (previously targeting PHP 8.0 / Mautic 5.1).

Test plan

  • Code Style (php-cs-fixer)
  • PHPStan
  • Rector dry-run
  • Twig Lint
  • PHPUnit (ContactFilterMatcherTest, DynamicContentSubscriberTest)

🤖 Generated with Claude Code

edouard-mangel and others added 13 commits March 12, 2026 00:14
…evaluation

The 5.x branch left DynamicContentSubscriber in the old Mautic 4 state:
traits (MatchFilterForLeadTrait, DbalQueryTrait) that no longer exist in
mautic/core-lib ^5.0, and a QueryBuilder-based filter evaluation path.

Changes:
- Remove MatchFilterForLeadTrait and DbalQueryTrait (removed from core in 5.x)
- Remove QueryFilterHelper and LoggerInterface from constructor
- Wire ContactFilterMatcher as the delegate in evaluateFilters()
- Simplify hasCustomObjectFilters() — returns true on first non-throwing filter
- Rewrite DynamicContentSubscriberTest: correct collaborators, 6 tests / 21 assertions
- Add docs/adr/0001-mautic5-migration-strategy.md

Verified on Mautic 5.2.9 / Symfony 5.4 / PHP 8.3 via DDEV:
- cache:clear succeeds
- mautic:plugins:reload installs the plugin (13 DB tables created)
- Custom Objects UI fully functional at /s/custom/object/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ill trait directly

The class_alias trick (selecting between Mautic core MatchFilterForLeadTrait
and the polyfill at runtime) is not statically analysable by PHPStan, causing
8 false-positive errors. Since this branch targets Mautic 5 where the core
trait no longer has transformFilterDataForLead(), the polyfill branch always
executes — making the class_alias logic dead code.

- Remove class_alias block and Mautic\EmailBundle\EventListener\MatchFilterForLeadTrait import
- Use MatchFilterForLeadTraitPolyfill directly with a named alias
- Add 2 legitimate PHPStan baseline entries (MAUTIC_TABLE_PREFIX global constant
  and transformFilterDataForLead trait-override detection limitation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DynamicContentSubscriber and ContactFilterMatcher are already discovered
by Config/services.php via ->load(). Add ->bind() for the scalar
$leadCustomItemFetchLimit parameter so autowiring can resolve it without
explicit config.php entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: no CO filters, NotFoundException/InvalidCustomObjectFormatListException
handling, no linked items, name match/mismatch, multi-item match, custom
item caching per object+lead, company enrichment, tag enrichment.
…ems linked

When a contact has no linked custom items, the lead value array is empty
and the foreach never executes, leaving the group result as false. For the
'empty' operator this is semantically wrong — no items means the field IS
empty, so the condition should match.
…ing in services.php

The #[Autowire] attribute is not reliably processed in Mautic 5.1 environments,
causing a container compilation failure. Use a specific service definition with
->arg() in services.php instead — this is scoped to ContactFilterMatcher only
and works across all supported Mautic/Symfony versions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 11, 2026 23:21
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Ports the Dynamic Content subscriber/filter-matching integration from Mautic 5 to Mautic 6 by switching to the new evaluation event, refactoring ContactFilterMatcher to rely on the polyfill trait, and updating CI/static-analysis/test coverage accordingly.

Changes:

  • Migrate dynamic content filter evaluation to Mautic 6 APIs (ContactFiltersEvaluateEvent) and adjust exception handling.
  • Refactor ContactFilterMatcher to remove class_alias, use constructor promotion, and update DBAL calls (executeQuery()).
  • Add new unit tests, update service wiring for autowiring/binding, and extend PHPStan baseline + CI matrix for Mautic 6 / PHP 8.2.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
EventListener/DynamicContentSubscriber.php Update to Mautic 6 event API and modernize catch blocks.
Helper/ContactFilterMatcher.php Refactor trait usage/constructor promotion; update DBAL query execution; custom-object filter integration.
Polyfill/EventListener/MatchFilterForLeadTrait.php Add empty-linked-items handling and modern string helpers in filter matching.
Config/services.php Rely on autowiring; bind the fetch-limit constructor arg.
Tests/Unit/Helper/ContactFilterMatcherTest.php Add unit coverage for matcher behavior, caching, company/tags data fetching.
.github/workflows/tests.yml Move CI to PHP 8.2 and Mautic 6.x.
phpstan-baseline.neon Add new suppressions for the refactor/polyfill changes.
Config/config.php Minor formatting-only change.
Comments suppressed due to low confidence (1)

Polyfill/EventListener/MatchFilterForLeadTrait.php:201

  • transformFilterDataForLead() currently returns null unconditionally while its signature advertises ?array. This mismatch is also reflected by the new PHPStan baseline suppressions. If this method is no longer needed, remove it; otherwise, implement the actual transformation (e.g., by aliasing/delegating to Mautic’s MatchFilterForLeadTrait::transformFilterDataForLead()) and add a precise phpdoc like @return array<mixed>|null to avoid baseline growth.
    /**
     * @param mixed[] $data
     * @param mixed[] $lead
     */
    private function transformFilterDataForLead(array $data, array $lead): ?array
    {
        return null;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 172 to 180
/**
* @param mixed[] $data
* @param mixed[] $lead
*
* @return ?mixed[]
*/
private function transformFilterDataForLead(array $data, array $lead): ?array
{
if ('custom_object' === $data['object']) {
return $lead[$data['field']];
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

transformFilterDataForLead() is private and (per the added PHPStan baseline) currently considered unused. If matchFilterForLead() no longer calls into this hook, consider removing this method to reduce dead code and avoid needing a baseline entry for it.

Copilot uses AI. Check for mistakes.
Comment on lines 181 to 183

return $this->transformFilterDataForLeadAlias($data, $lead);
return $this->transformFilterDataForLeadPolyfill($data, $lead);
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

transformFilterDataForLead() delegates to the aliased polyfill method, but the polyfill transformFilterDataForLead() currently returns null unconditionally. Since this method is also marked unused by PHPStan, consider removing the alias + this wrapper entirely, or implementing a real delegation to Mautic’s trait so the method is correct if it becomes used again.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +30
$services->set(ContactFilterMatcher::class)
->arg('$leadCustomItemFetchLimit', '%mautic.custom_item_fetch_limit_per_lead%');
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The default bind('int $leadCustomItemFetchLimit', ...) already supplies the constructor argument for autowired services, and the explicit set(ContactFilterMatcher::class)->arg(...) repeats the same configuration. Consider keeping just one of these (preferably the default bind) to avoid config drift if the parameter name/type changes later.

Suggested change
$services->set(ContactFilterMatcher::class)
->arg('$leadCustomItemFetchLimit', '%mautic.custom_item_fetch_limit_per_lead%');

Copilot uses AI. Check for mistakes.
$this->buildLeadFilter('companycountry', '=', 'US'),
];

$this->matcher->match($filters, ['id' => 42]);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

testMatchFetchesCompanyDataWhenFilterFieldStartsWithCompany() only asserts that the repository call happens, but it doesn’t assert the overall match result for a company-prefixed filter. Adding an assertion that the company filter actually influences the return value would better protect against regressions (especially since company filter evaluation is easy to accidentally skip when relying on flattened vs companies data).

Suggested change
$this->matcher->match($filters, ['id' => 42]);
$result = $this->matcher->match($filters, ['id' => 42]);
$this->assertFalse($result);

Copilot uses AI. Check for mistakes.
Comment on lines +22 to 24
use MatchFilterForLeadTraitPolyfill {
transformFilterDataForLead as transformFilterDataForLeadPolyfill;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

ContactFilterMatcher now always uses the polyfill MatchFilterForLeadTrait. That trait’s matchFilterForLead() currently reads $lead[$data['field']] directly and does not appear to use the fetched $lead['companies'] data, so segment filters like companycountry may be silently skipped even though match() eagerly loads companies. Consider delegating to Mautic’s native matchFilterForLead() / transformFilterDataForLead() (or implementing equivalent company-field extraction) so company-prefixed filters are actually evaluated.

Copilot uses AI. Check for mistakes.
Comment on lines 69 to +74
if (is_array($leadValues)) {
if ('custom_object' === $data['object'] && [] === $leadValues) {
// No custom items linked to this contact: only 'empty' is true.
$groups[$groupNum] = 'empty' === $data['operator'];
continue;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In matchFilterForLead(), $leadValues is taken directly from $lead[$data['field']], so filters that rely on derived data (notably company-prefixed fields that typically come from $lead['companies']) won’t match unless the flattened field keys already exist on $lead. Since ContactFilterMatcher fetches companies into $lead['companies'], consider using/adding a transformFilterDataForLead() step (or delegating to Mautic’s original trait implementation) so company filters are evaluated correctly.

Copilot uses AI. Check for mistakes.
edouard-mangel and others added 3 commits March 12, 2026 00:30
Symfony 6.4 is strict about unused _defaults bindings. Replace the
->bind()/_defaults approach with #[Autowire(param:...)] on the
ContactFilterMatcher constructor, which is fully supported on Symfony 6.4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bulk fix of trailing_comma_in_multiline, new_with_parentheses,
fully_qualified_strict_types, ordered_imports, and phpdoc_separation
violations across the codebase. These are pre-existing issues
exposed when testing against Mautic 6's php-cs-fixer ruleset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in PHPStan 2.0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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