Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f49fc5d
fix(DynamicContentSubscriber): complete Mautic 5 migration of filter …
edouard-mangel Mar 9, 2026
6f22f41
refactor(ContactFilterMatcher): remove class_alias pattern, use polyf…
edouard-mangel Mar 9, 2026
8c8f300
refactor(config): remove explicit service definitions, use autowiring
edouard-mangel Mar 10, 2026
e49bc8d
refactor(ContactFilterMatcher): use #[Autowire] for scalar parameter
edouard-mangel Mar 11, 2026
442f0af
test(ContactFilterMatcher): add unit tests
edouard-mangel Mar 11, 2026
3a9335a
fix(ContactFilterMatcher): replace deprecated DBAL execute() with exe…
edouard-mangel Mar 11, 2026
7227295
fix(MatchFilterForLeadTrait): handle empty operator when no custom it…
edouard-mangel Mar 11, 2026
63639a8
fix(ContactFilterMatcher): replace #[Autowire] with explicit arg bind…
edouard-mangel Mar 11, 2026
57cf15a
ci: add fix/** branch trigger and workflow_dispatch for local testing
edouard-mangel Mar 11, 2026
4878ed9
fix(cs): remove extra blank line, extra parentheses, and PHP 8.1 inte…
edouard-mangel Mar 11, 2026
ecd58ed
fix(tests): remove MAUTIC_TABLE_PREFIX define from test file (defined…
edouard-mangel Mar 11, 2026
c7302cb
fix(phpstan): add baseline entry for transformFilterDataForLeadPolyfi…
edouard-mangel Mar 11, 2026
6002c35
refactor: apply Rector rules (constructor promotion, str_starts_with,…
edouard-mangel Mar 11, 2026
99974b5
fix(phpstan): suppress array element type errors for transformFilterD…
edouard-mangel Mar 11, 2026
6e6ce5b
refactor(dynamic-content): replace MatchFilterForLeadTrait polyfill w…
edouard-mangel Apr 15, 2026
528977d
fix(cs): apply PHP CS Fixer rules for new files and modified classes
edouard-mangel Apr 15, 2026
60fcb53
fix(tests): update ContactFilterMatcherTest for new constructor signa…
edouard-mangel Apr 15, 2026
9fb226e
fix(di): use string service IDs for FilterEvaluator and ContactFilter…
edouard-mangel Apr 15, 2026
2d81055
fix(dynamic-content): include contact id in lead array and fix any-ma…
edouard-mangel Apr 15, 2026
803c0cb
fix(test): update unit test expectation to include id in lead array f…
edouard-mangel Apr 15, 2026
de7cc71
test: add FilterEvaluatorTest unit tests and fill functional test gaps
edouard-mangel Apr 15, 2026
b6fdc86
fix(test): use number-test-field alias for int type (translates to Nu…
edouard-mangel Apr 15, 2026
6ef4427
fix(evaluator): handle int type key in coerceTypes, use int type in f…
edouard-mangel Apr 15, 2026
02abca2
docs(evaluator): clarify flat vs nested filter value shape
edouard-mangel Jul 20, 2026
aa281e3
fix(install): create plugin schema via the supported event API
edouard-mangel Jul 20, 2026
c053e91
docs: add design for verifying plugin against fresh official Mautic 6
edouard-mangel Jul 29, 2026
3bf4e93
chore: ignore .superpowers/ SDD scratch workspace
edouard-mangel Jul 29, 2026
9cf66d2
docs: record Mautic 6 official-instance verification findings
edouard-mangel Jul 30, 2026
baa8603
docs: correct mautic6-official-verify report and document unit-test gap
edouard-mangel Jul 30, 2026
8263593
docs: record headline-feature testing, TokenSubscriber DI fix, and fu…
edouard-mangel Jul 30, 2026
3db4983
docs: record functional test suite fixes and remaining known gaps
edouard-mangel Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ on:
- '[0-9]+\.[0-9]+'
- development
- beta
- 'fix/**'
pull_request:
workflow_dispatch:

env:
PLUGIN_DIR: plugins/CustomObjectsBundle # Same as extra.install-directory-name in composer.json
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor/*
/Tests/Coverage/*
.idea
.idea
.superpowers/
17 changes: 17 additions & 0 deletions Config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@
'mautic.campaign.model.event',
'event_dispatcher',
'custom_object.helper.token_formatter',
'custom_object.helper.filter_evaluator',
'%mautic.custom_item_fetch_limit_per_lead%',
],
],
Expand Down Expand Up @@ -636,6 +637,22 @@
'custom_object.helper.token_formatter' => [
'class' => MauticPlugin\CustomObjectsBundle\Helper\TokenFormatter::class,
],
'custom_object.helper.filter_evaluator' => [
'class' => MauticPlugin\CustomObjectsBundle\Helper\FilterEvaluator::class,
],
'custom_object.helper.contact_filter_matcher' => [
'class' => MauticPlugin\CustomObjectsBundle\Helper\ContactFilterMatcher::class,
'arguments' => [
'mautic.custom.model.field',
'mautic.custom.model.object',
'mautic.custom.model.item',
'mautic.lead.repository.company',
'doctrine.dbal.default_connection',
'custom_object.helper.filter_evaluator',
'%mautic.custom_item_fetch_limit_per_lead%',
],
],

'custom_object.data_persister.custom_item' => [
'class' => MauticPlugin\CustomObjectsBundle\DataPersister\CustomItemDataPersister::class,
'tag' => 'api_platform.data_persister',
Expand Down
9 changes: 9 additions & 0 deletions Config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
declare(strict_types=1);

use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
use MauticPlugin\CustomObjectsBundle\Helper\ContactFilterMatcher;
use MauticPlugin\CustomObjectsBundle\Helper\FilterEvaluator;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $configurator): void {
Expand All @@ -17,10 +19,17 @@
'Report/ReportColumnsBuilder.php',
'Serializer/ApiNormalizer.php',
'Extension/CustomItemListeningExtension.php',
// Registered explicitly in config.php so the int $leadCustomItemFetchLimit arg can be set
'Helper/ContactFilterMatcher.php',
'Helper/FilterEvaluator.php',
];

$services->load('MauticPlugin\\CustomObjectsBundle\\', '../')
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');

$services->load('MauticPlugin\\CustomObjectsBundle\\Repository\\', '../Repository/*Repository.php');

// Aliases so autowiring resolves the config.php-registered services by class name
$services->alias(ContactFilterMatcher::class, 'custom_object.helper.contact_filter_matcher')->public();
$services->alias(FilterEvaluator::class, 'custom_object.helper.filter_evaluator')->public();
};
13 changes: 0 additions & 13 deletions CustomObjectsBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace MauticPlugin\CustomObjectsBundle;

use Doctrine\DBAL\Schema\Schema;
use Mautic\CoreBundle\Factory\MauticFactory;
use Mautic\IntegrationsBundle\Bundle\AbstractPluginBundle;
use MauticPlugin\CustomObjectsBundle\DependencyInjection\Compiler\CustomFieldTypePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -19,15 +17,4 @@ public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new CustomFieldTypePass());
}

/**
* In some rare cases it can happen that the plugin tables weren't created on plugin install.
* Create them on plugin update if they are missing.
*/
protected static function installAllTablesIfMissing(Schema $schema, string $tablePrefix, MauticFactory $factory, array $metadata = null): void
{
if (!$schema->hasTable($tablePrefix.'custom_object')) {
self::installPluginSchema($metadata, $factory, null);
}
}
}
70 changes: 26 additions & 44 deletions EventListener/DynamicContentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,23 @@

use Mautic\DynamicContentBundle\DynamicContentEvents;
use Mautic\DynamicContentBundle\Event\ContactFiltersEvaluateEvent;
use Mautic\EmailBundle\EventListener\MatchFilterForLeadTrait;
use MauticPlugin\CustomObjectsBundle\Exception\InvalidArgumentException;
use MauticPlugin\CustomObjectsBundle\Exception\InvalidSegmentFilterException;
use MauticPlugin\CustomObjectsBundle\Exception\NotFoundException;
use MauticPlugin\CustomObjectsBundle\Helper\QueryFilterHelper;
use MauticPlugin\CustomObjectsBundle\Helper\ContactFilterMatcher;
use MauticPlugin\CustomObjectsBundle\Provider\ConfigProvider;
use MauticPlugin\CustomObjectsBundle\Repository\DbalQueryTrait;
use MauticPlugin\CustomObjectsBundle\Segment\Query\Filter\QueryFilterFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class DynamicContentSubscriber implements EventSubscriberInterface
{
use MatchFilterForLeadTrait;
use DbalQueryTrait;

public function __construct(
private QueryFilterFactory $queryFilterFactory,
private QueryFilterHelper $queryFilterHelper,
private ContactFilterMatcher $contactFilterMatcher,
private ConfigProvider $configProvider,
Comment thread
edouard-mangel marked this conversation as resolved.
private LoggerInterface $logger
) {
}

/**
* @return mixed[]
* @return array<string,array{string,int}>
*/
public static function getSubscribedEvents(): array
{
Expand All @@ -40,47 +31,38 @@ public static function getSubscribedEvents(): array
];
}

/**
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function evaluateFilters(ContactFiltersEvaluateEvent $event): void
{
if (!$this->configProvider->pluginIsEnabled()) {
if ($event->isEvaluated()
|| !$this->configProvider->pluginIsEnabled()
|| !$this->hasCustomObjectFilters($event->getFilters())
) {
return;
}

$eventFilters = $event->getFilters();

if ($event->isEvaluated()) {
return;
}

foreach ($eventFilters as $key => $eventFilter) {
$queryAlias = "filter_{$key}";

try {
$filterQueryBuilder = $this->queryFilterFactory->configureQueryBuilderFromSegmentFilter($eventFilter, $queryAlias);
} catch (InvalidSegmentFilterException $e) {
continue;
}

$this->queryFilterHelper->addContactIdRestriction($filterQueryBuilder, $queryAlias, (int) $event->getContact()->getId());
$event->setIsEvaluated(true);
$event->stopPropagation();
$contact = $event->getContact();
$event->setIsMatched($this->contactFilterMatcher->match(
$event->getFilters(),
array_merge(['id' => $contact->getId()], $contact->getProfileFields())
));
}

/**
* @param mixed[] $filters
*/
private function hasCustomObjectFilters(array $filters): bool
{
foreach ($filters as $filter) {
try {
if ($this->executeSelect($filterQueryBuilder)->rowCount()) {
$event->setIsEvaluated(true);
$event->setIsMatched(true);
} else {
$event->setIsEvaluated(true);
}
} catch (\PDOException $e) {
$this->logger->error('Failed to evaluate dynamic content for custom object '.$e->getMessage());
$this->queryFilterFactory->configureQueryBuilderFromSegmentFilter($filter, 'filter');

throw $e;
return true;
} catch (InvalidSegmentFilterException) {
}

$event->stopPropagation(); // The filter is ours, we won't allow no more processing
}

return false;
}
}
62 changes: 62 additions & 0 deletions EventListener/PluginSchemaSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace MauticPlugin\CustomObjectsBundle\EventListener;

use Mautic\PluginBundle\Event\PluginInstallEvent;
use Mautic\PluginBundle\Event\PluginUpdateEvent;
use Mautic\PluginBundle\PluginEvents;
use MauticPlugin\CustomObjectsBundle\Helper\PluginSchemaInstaller;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* Keeps the plugin's tables in place via the supported event API.
*
* Mautic still calls PluginBundleBase::onPluginInstall(), but it is marked
* "@deprecated To be removed in 5.0. Listen to PluginEvents::ON_PLUGIN_INSTALL instead",
* so schema creation cannot keep relying on it. AbstractPluginBundle additionally
* overrides onPluginUpdate() to run the migration engine, which means the old
* create-missing-tables safety net no longer runs at all.
*/
class PluginSchemaSubscriber implements EventSubscriberInterface
{
private const PLUGIN_NAME = 'Custom Objects';

public function __construct(private PluginSchemaInstaller $schemaInstaller)
{
}

/**
* @return array<string, string>
*/
public static function getSubscribedEvents(): array
{
return [
PluginEvents::ON_PLUGIN_INSTALL => 'onPluginInstall',
PluginEvents::ON_PLUGIN_UPDATE => 'onPluginUpdate',
];
}

public function onPluginInstall(PluginInstallEvent $event): void
{
if (!$event->checkContext(self::PLUGIN_NAME)) {
return;
}

$this->schemaInstaller->ensureTablesExist();
}

/**
* In some rare cases the plugin tables are not created on install. Create the
* missing ones on update so the plugin can recover without a reinstall.
*/
public function onPluginUpdate(PluginUpdateEvent $event): void
{
if (!$event->checkContext(self::PLUGIN_NAME)) {
return;
}

$this->schemaInstaller->ensureTablesExist();
}
}
Loading