diff --git a/app/bundles/CoreBundle/Form/Type/DynamicContentTrait.php b/app/bundles/CoreBundle/Form/Type/DynamicContentTrait.php deleted file mode 100644 index 710ce96caa3..00000000000 --- a/app/bundles/CoreBundle/Form/Type/DynamicContentTrait.php +++ /dev/null @@ -1,51 +0,0 @@ -add( - 'dynamicContent', - CollectionType::class, - [ - 'entry_type' => DynamicContentFilterType::class, - 'allow_add' => true, - 'allow_delete' => true, - 'label' => false, - 'entry_options' => [ - 'label' => false, - ], - ] - ); - - $builder->addEventListener( - FormEvents::PRE_SUBMIT, - function (FormEvent $event): void { - $data = $event->getData(); - /** @var Email $entity */ - $entity = $event->getForm()->getData(); - - if (empty($data['dynamicContent'])) { - $data['dynamicContent'] = $entity->getDefaultDynamicContent(); - unset($data['dynamicContent'][0]['filters']['filter']); - } - - foreach ($data['dynamicContent'] as $key => $dc) { - if (empty($dc['filters'])) { - $data['dynamicContent'][$key]['filters'] = $entity->getDefaultDynamicContent()[0]['filters']; - } - } - - $event->setData($data); - } - ); - } -} diff --git a/app/bundles/CoreBundle/Tests/Unit/Form/Type/DynamicContentTraitTest.php b/app/bundles/CoreBundle/Tests/Unit/Form/Type/DynamicContentTraitTest.php deleted file mode 100644 index f73afd938c6..00000000000 --- a/app/bundles/CoreBundle/Tests/Unit/Form/Type/DynamicContentTraitTest.php +++ /dev/null @@ -1,135 +0,0 @@ - - */ - private MockObject $formBuilder; - - /** - * @var MockObject&FormEvent - */ - private MockObject $formEvent; - - /** - * @var MockObject&FormInterface - */ - private MockObject $form; - - /** - * @var MockObject (use DynamicContentEntityTrait) - */ - private MockObject $entity; - - /** - * @var MockObject (use DynamicContentTrait) - */ - private MockObject $trait; - - protected function setUp(): void - { - parent::setUp(); - - $this->formBuilder = $this->createMock(FormBuilderInterface::class); - $this->formEvent = $this->createMock(FormEvent::class); - $this->form = $this->createMock(FormInterface::class); - $this->entity = $this->getMockForTrait(DynamicContentEntityTrait::class); - $this->trait = $this->getMockForTrait(DynamicContentTrait::class); - } - - /** - * There is a problem when a user just grags&drop the Dynamic Content slot - * without configuring it. New email won't save with no error. We must ensure - * each dynamic content slot has its full structure. - */ - public function testAddDynamicContentFieldWithDecWithoutFiltersAndContent(): void - { - $this->formBuilder->expects($this->once()) - ->method('addEventListener') - ->with( - FormEvents::PRE_SUBMIT, - $this->callback(function ($formModifier) { - $inputData = [ - 'dynamicContent' => [ - [ - 'content' => '', - ], - ], - ]; - - $outputData = [ - 'dynamicContent' => [ - [ - 'content' => '', - 'filters' => [ - [ - 'content' => null, - 'filters' => [ - [ - 'glue' => null, - 'field' => null, - 'object' => null, - 'type' => null, - 'operator' => null, - 'display' => null, - 'filter' => null, - ], - ], - ], - ], - ], - ], - ]; - - $this->formEvent->expects($this->once()) - ->method('getForm') - ->willReturn($this->form); - - $this->form->expects($this->once()) - ->method('getData') - ->willReturn($this->entity); - - $this->formEvent->expects($this->once()) - ->method('getData') - ->willReturn($inputData); - - $this->formEvent->expects($this->once()) - ->method('setData') - ->with($outputData); - - $formModifier($this->formEvent); - - return true; - }) - ); - - $this->invokeMethod($this->trait, 'addDynamicContentField', [$this->formBuilder]); - } - - /** - * @param mixed[] $args - * - * @return mixed - */ - private function invokeMethod(object $object, string $methodName, array $args = []) - { - $reflection = new \ReflectionClass($object::class); - $method = $reflection->getMethod($methodName); - $method->setAccessible(true); - - return $method->invokeArgs($object, $args); - } -} diff --git a/app/bundles/DynamicContentBundle/Assets/js/dynamicContent.js b/app/bundles/DynamicContentBundle/Assets/js/dynamicContent.js index 957459a45d6..16d7d615c15 100644 --- a/app/bundles/DynamicContentBundle/Assets/js/dynamicContent.js +++ b/app/bundles/DynamicContentBundle/Assets/js/dynamicContent.js @@ -201,13 +201,13 @@ Mautic.addDwcFilter = function (elId, elObj) { //activate fields if (isSpecial) { - if (fieldType == 'select' || fieldType == 'multiselect' || fieldType == 'boolean') { + if (fieldType == 'select' || fieldType == 'multiselect' || fieldType == 'boolean' || fieldType == 'leadlist') { // Generate the options var fieldOptions = filterOption.data("field-list"); - mQuery.each(fieldOptions, function(index, val) { - if (mQuery.isPlainObject(val)) { + mQuery.each(fieldOptions, function(val, index) { + if (mQuery.isPlainObject(index)) { var optGroup = index; - mQuery.each(val, function(index, value) { + mQuery.each(optGroup, function(value, index) { mQuery('"); diff --git a/app/bundles/DynamicContentBundle/Config/config.php b/app/bundles/DynamicContentBundle/Config/config.php index 732aaf458c9..673f7ec3c81 100644 --- a/app/bundles/DynamicContentBundle/Config/config.php +++ b/app/bundles/DynamicContentBundle/Config/config.php @@ -49,6 +49,7 @@ 'class' => Mautic\DynamicContentBundle\Form\Type\DwcEntryFiltersType::class, 'arguments' => [ 'translator', + 'mautic.lead.model.list', ], 'methodCalls' => [ 'setConnection' => [ diff --git a/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php b/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php index a1e2748e69b..d0d62873683 100644 --- a/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php +++ b/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php @@ -173,9 +173,10 @@ public function decodeTokens(PageDisplayEvent $event): void $dom->loadHTML(mb_encode_numericentity($content, [0x80, 0x10FFFF, 0, 0xFFFFF], 'UTF-8'), LIBXML_NOERROR); $xpath = new \DOMXPath($dom); - $divContent = $xpath->query('//*[@data-slot="dwc"]'); - for ($i = 0; $i < $divContent->length; ++$i) { - $slot = $divContent->item($i); + $contentSlots = $xpath->query('//*[@data-slot="dwc"]'); + + for ($i = 0; $i < $contentSlots->length; ++$i) { + $slot = $contentSlots->item($i); if (!$slotName = $slot->getAttribute('data-param-slot-name')) { continue; } diff --git a/app/bundles/DynamicContentBundle/Form/Type/DwcEntryFiltersType.php b/app/bundles/DynamicContentBundle/Form/Type/DwcEntryFiltersType.php index e24056ced88..e3c77e4f91a 100644 --- a/app/bundles/DynamicContentBundle/Form/Type/DwcEntryFiltersType.php +++ b/app/bundles/DynamicContentBundle/Form/Type/DwcEntryFiltersType.php @@ -3,6 +3,7 @@ namespace Mautic\DynamicContentBundle\Form\Type; use Mautic\LeadBundle\Form\Type\FilterTrait; +use Mautic\LeadBundle\Model\ListModel; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -11,6 +12,7 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Exception\AccessException; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; @@ -22,7 +24,8 @@ class DwcEntryFiltersType extends AbstractType use FilterTrait; public function __construct( - private TranslatorInterface $translator + private TranslatorInterface $translator, + private ListModel $listModel ) { } @@ -68,7 +71,7 @@ function (FormEvent $event) use ($formModifier): void { } /** - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException + * @throws AccessException */ public function configureOptions(OptionsResolver $resolver): void { @@ -83,6 +86,7 @@ public function configureOptions(OptionsResolver $resolver): void 'deviceBrands', 'deviceOs', 'tags', + 'lists', ] ); @@ -90,6 +94,8 @@ public function configureOptions(OptionsResolver $resolver): void [ 'label' => false, 'error_bubbling' => false, + // @see \Mautic\LeadBundle\Controller\AjaxController::loadSegmentFilterFormAction() + 'lists' => $this->listModel->getChoiceFields()['lead']['leadlist']['properties']['list'], ] ); } diff --git a/app/bundles/DynamicContentBundle/Form/Type/DynamicContentType.php b/app/bundles/DynamicContentBundle/Form/Type/DynamicContentType.php index e075d67192b..b6a3a8704dd 100644 --- a/app/bundles/DynamicContentBundle/Form/Type/DynamicContentType.php +++ b/app/bundles/DynamicContentBundle/Form/Type/DynamicContentType.php @@ -324,8 +324,21 @@ public function buildView(FormView $view, FormInterface $form, array $options): private function filterFieldChoices(): void { unset($this->fieldChoices['company']); - $customFields = $this->leadModel->getRepository()->getCustomFieldList('lead'); - $this->fieldChoices['lead'] = array_filter($this->fieldChoices['lead'], fn ($key): bool => in_array($key, array_merge(array_keys($customFields[0]), ['date_added', 'date_modified', 'device_brand', 'device_model', 'device_os', 'device_type', 'tags']), true), ARRAY_FILTER_USE_KEY); + + $customFields = $this->leadModel->getRepository()->getCustomFieldList('lead'); + + $this->fieldChoices['lead'] = array_filter( + $this->fieldChoices['lead'], + fn ($key): bool => in_array( + $key, + array_merge( + array_keys($customFields[0]), + ['date_added', 'date_modified', 'device_brand', 'device_model', 'device_os', 'device_type', 'tags', 'leadlist'] + ), + true + ), + ARRAY_FILTER_USE_KEY + ); } /** diff --git a/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php b/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php index ab968f74370..809ff362225 100644 --- a/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php +++ b/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php @@ -18,6 +18,16 @@ class DynamicContentHelper { use MatchFilterForLeadTrait; + /** + * @const DYNAMIC_CONTENT_REGEX + */ + public const DYNAMIC_CONTENT_REGEX = '/{(dynamiccontent)=(\w+)(?:\/}|}(?:([^{]*(?:{(?!\/\1})[^{]*)*){\/\1})?)/is'; + + /** + * @const DYNAMIC_WEB_CONTENT_REGEX + */ + public const DYNAMIC_WEB_CONTENT_REGEX = '/{dwc=(.*?)}/'; + public function __construct( protected DynamicContentModel $dynamicContentModel, protected RealTimeExecutioner $realTimeExecutioner, diff --git a/app/bundles/DynamicContentBundle/Resources/views/DynamicContent/form.html.twig b/app/bundles/DynamicContentBundle/Resources/views/DynamicContent/form.html.twig index 271e7381f83..aa8b2fb182f 100644 --- a/app/bundles/DynamicContentBundle/Resources/views/DynamicContent/form.html.twig +++ b/app/bundles/DynamicContentBundle/Resources/views/DynamicContent/form.html.twig @@ -175,10 +175,15 @@ name="dwc[filters][__name__][filter]" id="dwc_filters___name___filter"> {% if form.vars[dataKey] is defined %} - {% for value, label in form.vars[dataKey] %} - {% if label is iterable %} - - {% for optionValue, optionLabel in label %} + {% set index = 0 %} + {% for label, value in form.vars[dataKey] %} + {% if value is iterable %} + + {% for optionLabel, optionValue in value %} + {% if (dataKey == 'regions') %} + {% set optionValue = index %} + {% set index = index + 1 %} + {% endif %} {% endfor %} diff --git a/app/bundles/DynamicContentBundle/Tests/EventListener/DynamicContentSubscriberTest.php b/app/bundles/DynamicContentBundle/Tests/Unit/EventListener/DynamicContentSubscriberTest.php similarity index 97% rename from app/bundles/DynamicContentBundle/Tests/EventListener/DynamicContentSubscriberTest.php rename to app/bundles/DynamicContentBundle/Tests/Unit/EventListener/DynamicContentSubscriberTest.php index 5fd905923a0..57443b46294 100644 --- a/app/bundles/DynamicContentBundle/Tests/EventListener/DynamicContentSubscriberTest.php +++ b/app/bundles/DynamicContentBundle/Tests/Unit/EventListener/DynamicContentSubscriberTest.php @@ -14,7 +14,6 @@ use Mautic\LeadBundle\Entity\CompanyLeadRepository; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\CompanyModel; -use Mautic\LeadBundle\Model\LeadModel; use Mautic\LeadBundle\Tracker\ContactTracker; use Mautic\PageBundle\Event\PageDisplayEvent; use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper; @@ -54,11 +53,6 @@ class DynamicContentSubscriberTest extends \PHPUnit\Framework\TestCase */ private MockObject $auditLogModel; - /** - * @var MockObject|LeadModel - */ - private MockObject $leadModel; - /** * @var MockObject|DynamicContentHelper */ @@ -96,7 +90,7 @@ protected function setUp(): void $this->formTokenHelper = $this->createMock(FormTokenHelper::class); $this->focusTokenHelper = $this->createMock(FocusTokenHelper::class); $this->auditLogModel = $this->createMock(AuditLogModel::class); - $this->leadModel = $this->createMock(LeadModel::class); + $this->contactTracker = $this->createMock(ContactTracker::class); $this->dynamicContentHelper = $this->createMock(DynamicContentHelper::class); $this->dynamicContentModel = $this->createMock(DynamicContentModel::class); $this->security = $this->createMock(CorePermissions::class); diff --git a/app/bundles/DynamicContentBundle/Tests/Helper/DynamicContentHelperTest.php b/app/bundles/DynamicContentBundle/Tests/Unit/Helper/DynamicContentHelperTest.php similarity index 97% rename from app/bundles/DynamicContentBundle/Tests/Helper/DynamicContentHelperTest.php rename to app/bundles/DynamicContentBundle/Tests/Unit/Helper/DynamicContentHelperTest.php index 368ed543c92..40249fd7420 100644 --- a/app/bundles/DynamicContentBundle/Tests/Helper/DynamicContentHelperTest.php +++ b/app/bundles/DynamicContentBundle/Tests/Unit/Helper/DynamicContentHelperTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Mautic\DynamicContentBundle\Tests\Helper; +namespace Mautic\DynamicContentBundle\Tests\Unit\Helper; use Mautic\CampaignBundle\Executioner\RealTimeExecutioner; use Mautic\CoreBundle\Event\TokenReplacementEvent; @@ -51,7 +51,7 @@ protected function setUp(): void $this->mockModel, $this->realTimeExecutioner, $this->mockDispatcher, - $this->leadModel + $this->leadModel, ); } @@ -94,13 +94,13 @@ public function testGetDwcBySlotNameWithPublished(): void ], ] ) - ->willReturnOnConsecutiveCalls(true, false); + ->willReturnOnConsecutiveCalls(['some entity'], []); // Only get published - $this->assertTrue($this->helper->getDwcsBySlotName('test', true)); + $this->assertCount(1, $this->helper->getDwcsBySlotName('test', true)); // Get all - $this->assertFalse($this->helper->getDwcsBySlotName('secondtest')); + $this->assertCount(0, $this->helper->getDwcsBySlotName('secondtest')); } public function testGetDynamicContentSlotForLeadWithListenerFindingMatch(): void diff --git a/app/bundles/EmailBundle/Form/Type/EmailType.php b/app/bundles/EmailBundle/Form/Type/EmailType.php index 21810c6ef87..4d38d0d46f3 100644 --- a/app/bundles/EmailBundle/Form/Type/EmailType.php +++ b/app/bundles/EmailBundle/Form/Type/EmailType.php @@ -8,7 +8,7 @@ use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer; use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber; use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber; -use Mautic\CoreBundle\Form\Type\DynamicContentTrait; +use Mautic\CoreBundle\Form\Type\DynamicContentFilterType; use Mautic\CoreBundle\Form\Type\FormButtonsType; use Mautic\CoreBundle\Form\Type\PublishDownDateType; use Mautic\CoreBundle\Form\Type\PublishUpDateType; @@ -24,6 +24,7 @@ use Mautic\PageBundle\Form\Type\PreferenceCenterListType; use Mautic\StageBundle\Model\StageModel; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -41,8 +42,6 @@ */ class EmailType extends AbstractType { - use DynamicContentTrait; - public function __construct( private TranslatorInterface $translator, private EntityManagerInterface $em, @@ -557,4 +556,43 @@ private function getGlobalMailerIsOwner(): bool { return (bool) $this->coreParametersHelper->get('mailer_is_owner'); } + + private function addDynamicContentField(FormBuilderInterface $builder): void + { + $builder->add( + 'dynamicContent', + CollectionType::class, + [ + 'entry_type' => DynamicContentFilterType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'label' => false, + 'entry_options' => [ + 'label' => false, + ], + ] + ); + + $builder->addEventListener( + FormEvents::PRE_SUBMIT, + function (FormEvent $event): void { + $data = $event->getData(); + /** @var Email $entity */ + $entity = $event->getForm()->getData(); + + if (empty($data['dynamicContent'])) { + $data['dynamicContent'] = $entity->getDefaultDynamicContent(); + unset($data['dynamicContent'][0]['filters']['filter']); + } + + foreach ($data['dynamicContent'] as $key => $dc) { + if (empty($dc['filters'])) { + $data['dynamicContent'][$key]['filters'] = $entity->getDefaultDynamicContent()[0]['filters']; + } + } + + $event->setData($data); + } + ); + } } diff --git a/app/bundles/EmailBundle/Helper/StatsCollectionHelper.php b/app/bundles/EmailBundle/Helper/StatsCollectionHelper.php index 01375ebc839..b1f95960fbf 100644 --- a/app/bundles/EmailBundle/Helper/StatsCollectionHelper.php +++ b/app/bundles/EmailBundle/Helper/StatsCollectionHelper.php @@ -6,7 +6,6 @@ use Mautic\EmailBundle\Stats\Helper\BouncedHelper; use Mautic\EmailBundle\Stats\Helper\ClickedHelper; use Mautic\EmailBundle\Stats\Helper\FailedHelper; -use Mautic\EmailBundle\Stats\Helper\FilterTrait; use Mautic\EmailBundle\Stats\Helper\OpenedHelper; use Mautic\EmailBundle\Stats\Helper\SentHelper; use Mautic\EmailBundle\Stats\Helper\UnsubscribedHelper; @@ -15,8 +14,6 @@ class StatsCollectionHelper { - use FilterTrait; - public const GENERAL_STAT_PREFIX = 'email'; public function __construct( diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 8125da3cec2..32cfc85f691 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -2,7 +2,6 @@ namespace Mautic\EmailBundle\Model; -use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\OptimisticLockException; @@ -106,7 +105,6 @@ public function __construct( private DNC $doNotContact, private StatsCollectionHelper $statsCollectionHelper, CorePermissions $security, - Connection $connection, EntityManagerInterface $em, EventDispatcherInterface $dispatcher, UrlGeneratorInterface $router, @@ -116,8 +114,6 @@ public function __construct( CoreParametersHelper $coreParametersHelper, private EmailStatModel $emailStatModel ) { - $this->connection = $connection; - parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); } diff --git a/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php b/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php index 472b289b186..16cb8e679f2 100644 --- a/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php +++ b/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php @@ -62,15 +62,10 @@ class EmailModelTest extends \PHPUnit\Framework\TestCase public const SEGMENT_B = 'segment B'; /** - * @var Connection|LeadDeviceRepository + * @var MockObject|LeadDeviceRepository */ private MockObject $leadDeviceRepository; - /** - * @var Connection|MockObject - */ - private MockObject $connection; - /** * @var MockObject&IpLookupHelper */ @@ -244,7 +239,6 @@ protected function setUp(): void $this->doNotContact = $this->createMock(DoNotContact::class); $this->statsCollectionHelper = $this->createMock(StatsCollectionHelper::class); $this->corePermissions = $this->createMock(CorePermissions::class); - $this->connection = $this->createMock(Connection::class); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->leadDeviceRepository = $this->createMock(LeadDeviceRepository::class); @@ -266,7 +260,6 @@ protected function setUp(): void $this->doNotContact, $this->statsCollectionHelper, $this->corePermissions, - $this->connection, $this->entityManager, $this->eventDispatcher, $this->createMock(UrlGeneratorInterface::class), @@ -680,7 +673,6 @@ public function testFrequencyRulesAreAppliedAndMessageGetsQueued(): void $this->doNotContact, $this->statsCollectionHelper, $this->corePermissions, - $this->connection, $this->entityManager, $this->eventDispatcher, $this->createMock(UrlGeneratorInterface::class), diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index f3673befd1b..ee3d080a3ed 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -14,6 +14,7 @@ class LeadListRepository extends CommonRepository { use OperatorListTrait; // @deprecated to be removed in Mautic 3. Not used inside this class. + use ExpressionHelperTrait; use RegexTrait; @@ -535,4 +536,91 @@ public function getSegmentCampaigns(int $segmentId): array return $lists; } + + public function isContactInAnySegment(int $contactId): bool + { + $tableName = MAUTIC_TABLE_PREFIX.'lead_lists_leads'; + + $sql = <<getEntityManager()->getConnection() + ->executeQuery( + $sql, + [$contactId], + [\PDO::PARAM_INT] + ) + ->fetchFirstColumn(); + + return !empty($segmentIds); + } + + public function isNotContactInAnySegment(int $contactId): bool + { + return !$this->isContactInAnySegment($contactId); + } + + /** + * @param int[] $expectedSegmentIds + */ + public function isContactInSegments(int $contactId, array $expectedSegmentIds): bool + { + $segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds); + + return !empty($segmentIds); + } + + /** + * @param int[] $expectedSegmentIds + */ + public function isNotContactInSegments(int $contactId, array $expectedSegmentIds): bool + { + $segmentIds = $this->fetchContactToSegmentIdsRelationships($contactId, $expectedSegmentIds); + + if (empty($segmentIds)) { + return true; // Contact is not associated wit any segment + } + + foreach ($expectedSegmentIds as $expectedSegmentId) { + if (in_array($expectedSegmentId, $segmentIds)) { // No exact type comparison used! + return false; + } + } + + return true; + } + + /** + * @param int[] $expectedSegmentIds + * + * @return int[] + */ + private function fetchContactToSegmentIdsRelationships(int $contactId, array $expectedSegmentIds): array + { + $tableName = MAUTIC_TABLE_PREFIX.'lead_lists_leads'; + + $sql = <<getEntityManager()->getConnection() + ->executeQuery( + $sql, + [$contactId, $expectedSegmentIds], + [ + \PDO::PARAM_INT, + ArrayParameterType::INTEGER, + ] + ) + ->fetchFirstColumn(); + } } diff --git a/app/bundles/LeadBundle/Entity/OperatorListTrait.php b/app/bundles/LeadBundle/Entity/OperatorListTrait.php index 4d36fb91e20..1aae8c532b0 100644 --- a/app/bundles/LeadBundle/Entity/OperatorListTrait.php +++ b/app/bundles/LeadBundle/Entity/OperatorListTrait.php @@ -44,10 +44,24 @@ trait OperatorListTrait ], ], 'default' => [ - 'exclude' => [ - OperatorOptions::IN, - OperatorOptions::NOT_IN, - OperatorOptions::DATE, + 'include' => [ + OperatorOptions::EQUAL_TO, + OperatorOptions::NOT_EQUAL_TO, + OperatorOptions::GREATER_THAN, + OperatorOptions::GREATER_THAN_OR_EQUAL, + OperatorOptions::LESS_THAN, + OperatorOptions::LESS_THAN_OR_EQUAL, + OperatorOptions::EMPTY, + OperatorOptions::NOT_EMPTY, + OperatorOptions::LIKE, + OperatorOptions::NOT_LIKE, + OperatorOptions::BETWEEN, + OperatorOptions::NOT_BETWEEN, + OperatorOptions::REGEXP, + OperatorOptions::NOT_REGEXP, + OperatorOptions::STARTS_WITH, + OperatorOptions::ENDS_WITH, + OperatorOptions::CONTAINS, ], ], 'multiselect' => [ @@ -59,9 +73,25 @@ trait OperatorListTrait ], ], 'date' => [ - 'exclude' => [ - OperatorOptions::IN, - OperatorOptions::NOT_IN, + 'include' => [ + OperatorOptions::EQUAL_TO, + OperatorOptions::NOT_EQUAL_TO, + OperatorOptions::GREATER_THAN, + OperatorOptions::GREATER_THAN_OR_EQUAL, + OperatorOptions::LESS_THAN, + OperatorOptions::LESS_THAN_OR_EQUAL, + OperatorOptions::EMPTY, + OperatorOptions::NOT_EMPTY, + OperatorOptions::LIKE, + OperatorOptions::NOT_LIKE, + OperatorOptions::BETWEEN, + OperatorOptions::NOT_BETWEEN, + OperatorOptions::REGEXP, + OperatorOptions::NOT_REGEXP, + OperatorOptions::DATE, + OperatorOptions::STARTS_WITH, + OperatorOptions::ENDS_WITH, + OperatorOptions::CONTAINS, ], ], 'lookup_id' => [ diff --git a/app/bundles/LeadBundle/EventListener/DynamicContentSubscriber.php b/app/bundles/LeadBundle/EventListener/DynamicContentSubscriber.php new file mode 100644 index 00000000000..21cfeed4aa5 --- /dev/null +++ b/app/bundles/LeadBundle/EventListener/DynamicContentSubscriber.php @@ -0,0 +1,58 @@ + ['onContactFilterEvaluate', 0], + ]; + } + + public function onContactFilterEvaluate(ContactFiltersEvaluateEvent $event): void + { + foreach ($event->getFilters() as $filter) { + if ('leadlist' === $filter['type']) { + // Segment membership evaluation. Check if contact/segment relationship is correct. + $event->setIsMatched( + $this->isContactSegmentRelationshipValid($event->getContact(), $filter['operator'], $filter['filter']) + ); + $event->setIsEvaluated(true); + + return; + } + } + } + + /** + * @param string $operator empty, !empty, in, !in + * @param ?int[] $segmentIds + */ + private function isContactSegmentRelationshipValid(Lead $contact, string $operator, array $segmentIds = null): bool + { + $contactId = (int) $contact->getId(); + + return match ($operator) { + OperatorOptions::EMPTY => $this->segmentRepository->isNotContactInAnySegment($contactId), + OperatorOptions::NOT_EMPTY => $this->segmentRepository->isContactInAnySegment($contactId), + OperatorOptions::IN => $this->segmentRepository->isContactInSegments($contactId, $segmentIds), + OperatorOptions::NOT_IN => $this->segmentRepository->isNotContactInSegments($contactId, $segmentIds), + default => throw new \InvalidArgumentException(sprintf("Unexpected operator '%s'", $operator)), + }; + } +} diff --git a/app/bundles/LeadBundle/EventListener/FilterOperatorSubscriber.php b/app/bundles/LeadBundle/EventListener/FilterOperatorSubscriber.php index 1d9ae9c9b26..1795257392b 100644 --- a/app/bundles/LeadBundle/EventListener/FilterOperatorSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/FilterOperatorSubscriber.php @@ -84,6 +84,20 @@ public function onGenerateSegmentFiltersAddCustomFields(LeadListFiltersChoicesEv public function onGenerateSegmentFiltersAddStaticFields(LeadListFiltersChoicesEvent $event): void { + $event->addChoice( + 'lead', + 'leadlist', + [ + 'label' => $this->translator->trans('mautic.lead.list.filter.lists'), + 'properties' => [ + 'type' => 'leadlist', + 'list' => $this->fieldChoicesProvider->getChoicesForField('multiselect', 'leadlist', $event->getSearch()), + ], + 'operators' => $this->typeOperatorProvider->getOperatorsForFieldType('multiselect'), + 'object' => 'lead', + ] + ); + // Only show for segments and not dynamic content addressed by https://github.com/mautic/mautic/pull/9260 if (!$event->isForSegmentation()) { return; @@ -130,15 +144,6 @@ public function onGenerateSegmentFiltersAddStaticFields(LeadListFiltersChoicesEv 'operators' => $this->typeOperatorProvider->getOperatorsForFieldType('default'), 'object' => 'lead', ], - 'leadlist' => [ - 'label' => $this->translator->trans('mautic.lead.list.filter.lists'), - 'properties' => [ - 'type' => 'leadlist', - 'list' => $this->fieldChoicesProvider->getChoicesForField('multiselect', 'leadlist', $event->getSearch()), - ], - 'operators' => $this->typeOperatorProvider->getOperatorsForFieldType('multiselect'), - 'object' => 'lead', - ], 'campaign' => [ 'label' => $this->translator->trans('mautic.lead.list.filter.campaign'), 'properties' => [ diff --git a/app/bundles/LeadBundle/Form/DataTransformer/FieldFilterTransformer.php b/app/bundles/LeadBundle/Form/DataTransformer/FieldFilterTransformer.php index f98ce82012d..ebb4de5e43e 100644 --- a/app/bundles/LeadBundle/Form/DataTransformer/FieldFilterTransformer.php +++ b/app/bundles/LeadBundle/Form/DataTransformer/FieldFilterTransformer.php @@ -42,20 +42,20 @@ public function transform($rawFilters) return []; } - foreach ($rawFilters as $k => $f) { + foreach ($rawFilters as $key => $filter) { if (!empty($this->default)) { - $rawFilters[$k] = array_merge($this->default, $rawFilters[$k]); + $rawFilters[$key] = array_merge($this->default, $rawFilters[$key]); } - if ('datetime' === $f['type']) { - $bcFilter = $f['filter'] ?? ''; - $filter = $f['properties']['filter'] ?? $bcFilter; + if ('datetime' === $filter['type']) { + $bcFilter = $filter['filter'] ?? ''; + $filter = $filter['properties']['filter'] ?? $bcFilter; if (empty($filter) || in_array($filter, $this->relativeDateStrings) || stristr($filter[0], '-') || stristr($filter[0], '+')) { continue; } $dt = new DateTimeHelper($filter, 'Y-m-d H:i'); - $rawFilters[$k]['properties']['filter'] = $dt->toLocalString(); + $rawFilters[$key]['properties']['filter'] = $dt->toLocalString(); } } diff --git a/app/bundles/LeadBundle/Form/Type/FilterTrait.php b/app/bundles/LeadBundle/Form/Type/FilterTrait.php index e0eb3b0679d..a6c790d05bd 100644 --- a/app/bundles/LeadBundle/Form/Type/FilterTrait.php +++ b/app/bundles/LeadBundle/Form/Type/FilterTrait.php @@ -3,7 +3,6 @@ namespace Mautic\LeadBundle\Form\Type; use Doctrine\DBAL\Connection; -use Mautic\CoreBundle\Helper\ArrayHelper; use Mautic\LeadBundle\Entity\RegexTrait; use Mautic\LeadBundle\Helper\FormFieldHelper; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -31,6 +30,9 @@ public function setConnection(Connection $connection): void $this->connection = $connection; } + /** + * @param string $eventName + */ public function buildFiltersForm($eventName, FormEvent $event, TranslatorInterface $translator, $currentListId = null): void { $data = $event->getData(); @@ -85,7 +87,7 @@ public function buildFiltersForm($eventName, FormEvent $event, TranslatorInterfa } $customOptions['choices'] = $options['lists']; - $customOptions['multiple'] = true; + $customOptions['multiple'] = in_array($data['operator'], ['in', '!in']); $customOptions['choice_translation_domain'] = false; $type = ChoiceType::class; break; @@ -264,14 +266,9 @@ public function buildFiltersForm($eventName, FormEvent $event, TranslatorInterfa $choices = []; if (!empty($field['properties']['list'])) { $list = $field['properties']['list']; - $choices = - ArrayHelper::flipArray( - ('boolean' === $fieldType) - ? - FormFieldHelper::parseBooleanList($list) - : - FormFieldHelper::parseList($list) - ); + $choices = ('boolean' === $fieldType) + ? FormFieldHelper::parseBooleanList($list) + : FormFieldHelper::parseList($list); } if ('select' === $fieldType) { @@ -355,6 +352,12 @@ function ($regex, ExecutionContextInterface $context): void { array_splice($customOptions['constraints'], $i, 1); } } + + if (in_array($data['operator'], ['empty', '!empty'])) { + // @see Symfony\Component\Form\Extension\Core\Type\ChoiceType::configureOptions + $data['filter'] = null; + } + $form->add( 'filter', $type, @@ -394,7 +397,7 @@ function ($regex, ExecutionContextInterface $context): void { ] ); - if (FormEvents::PRE_SUBMIT == $eventName) { + if (FormEvents::PRE_SUBMIT === $eventName) { $event->setData($data); } } diff --git a/app/bundles/LeadBundle/Form/Type/FilterType.php b/app/bundles/LeadBundle/Form/Type/FilterType.php index fd7b7303794..0576918650a 100644 --- a/app/bundles/LeadBundle/Form/Type/FilterType.php +++ b/app/bundles/LeadBundle/Form/Type/FilterType.php @@ -19,8 +19,6 @@ */ class FilterType extends AbstractType { - use FilterTrait; - public function __construct( private FormAdjustmentsProviderInterface $formAdjustmentsProvider, private ListModel $listModel diff --git a/app/bundles/LeadBundle/Tests/Entity/LeadListRepositoryTest.php b/app/bundles/LeadBundle/Tests/Entity/LeadListRepositoryTest.php index 0d437e416f7..39d802a304d 100644 --- a/app/bundles/LeadBundle/Tests/Entity/LeadListRepositoryTest.php +++ b/app/bundles/LeadBundle/Tests/Entity/LeadListRepositoryTest.php @@ -4,6 +4,8 @@ namespace Mautic\LeadBundle\Tests\Entity; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\ORM\Query\Expr; use Mautic\CoreBundle\Test\Doctrine\RepositoryConfiguratorTrait; @@ -11,6 +13,7 @@ use Mautic\LeadBundle\Entity\LeadListRepository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Cache\InvalidArgumentException; class LeadListRepositoryTest extends TestCase { @@ -32,12 +35,147 @@ protected function setUp(): void { parent::setUp(); + $this->connection = $this->createMock(Connection::class); $this->queryBuilderMock = $this->createMock(QueryBuilder::class); $this->expressionMock = $this->createMock(Expr::class); + $this->repository = $this->configureRepository(LeadList::class); + } + + public function testIsContactInAnySegmentFalse(): void + { + $contactId = 1; + $this->mockIsContactInAnySegment($contactId, []); + self::assertFalse($this->repository->isContactInAnySegment($contactId)); + } + + public function testIsContactInAnySegmentTrue(): void + { + $contactId = 1; + $this->mockIsContactInAnySegment($contactId, [1]); + self::assertTrue($this->repository->isContactInAnySegment($contactId)); + } + + public function testIsNotContactInAnySegmentTrue(): void + { + $contactId = 1; + $this->mockIsContactInAnySegment($contactId, []); + self::assertTrue($this->repository->isNotContactInAnySegment($contactId)); + } + + public function testIsNotContactInAnySegmentFalse(): void + { + $contactId = 1; + $this->mockIsContactInAnySegment($contactId, [1]); + self::assertFalse($this->repository->isNotContactInAnySegment($contactId)); + } + + public function testIsContactInSegmentsNone(): void + { + $contactId = 1; + $expectedSegmentIds = [1]; + $queryResult = []; + $this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult); + self::assertFalse($this->repository->isContactInSegments($contactId, $expectedSegmentIds)); + } + + public function testIsContactInSegmentsOne(): void + { + $contactId = 1; + $expectedSegmentIds = [1, 2]; + $queryResult = [1]; + $this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult); + self::assertTrue($this->repository->isContactInSegments($contactId, $expectedSegmentIds)); + } + + public function testIsContactInSegmentsAll(): void + { + $contactId = 1; + $expectedSegmentIds = [1, 2]; + $queryResult = [1, 2]; + $this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult); + self::assertTrue($this->repository->isContactInSegments($contactId, $expectedSegmentIds)); + } + + public function testIsNotContactInSegmentsNone(): void + { + $contactId = 1; + $expectedSegmentIds = [1]; + $queryResult = [0]; + $this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult); + self::assertTrue($this->repository->isNotContactInSegments($contactId, $expectedSegmentIds)); + } + + public function testIsNotContactInSegmentsOne(): void + { + $contactId = 1; + $expectedSegmentIds = [1, 2]; + $queryResult = [1]; + $this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult); + self::assertFalse($this->repository->isNotContactInSegments($contactId, $expectedSegmentIds)); + } + + public function testIsNotContactInSegmentsAll(): void + { + $contactId = 1; + $expectedSegmentIds = [1, 2]; + $queryResult = [1, 2]; + $this->mockIsContactInSegments($contactId, $expectedSegmentIds, $queryResult); + self::assertFalse($this->repository->isNotContactInSegments($contactId, $expectedSegmentIds)); + } - $this->repository = $this->configureRepository(LeadList::class); + /** + * @param array $queryResult + */ + private function mockIsContactInAnySegment(int $contactId, array $queryResult): void + { + $prefix = MAUTIC_TABLE_PREFIX; + $sql = <<connection->expects(self::once()) + ->method('executeQuery') + ->with($sql, [$contactId], [\PDO::PARAM_INT]) + ->willReturn($this->result); + $this->result->expects(self::once()) + ->method('fetchFirstColumn') + ->willReturn($queryResult); } + /** + * @param array $expectedSegmentIds + * @param array $queryResult + */ + private function mockIsContactInSegments(int $contactId, array $expectedSegmentIds, array $queryResult): void + { + $prefix = MAUTIC_TABLE_PREFIX; + $sql = <<connection->expects(self::once()) + ->method('executeQuery') + ->with( + $sql, + [$contactId, $expectedSegmentIds], + [\PDO::PARAM_INT, ArrayParameterType::INTEGER] + ) + ->willReturn($this->result); + + $this->result->expects(self::once()) + ->method('fetchFirstColumn') + ->willReturn($queryResult); + } + + /** + * @throws InvalidArgumentException + */ public function testGetMultipleLeadCounts(): void { $listIds = [765, 766]; @@ -54,7 +192,7 @@ public function testGetMultipleLeadCounts(): void ], ]; - $this->mockGetLeadCount($queryResult); + $this->mockGetLeadCount($queryResult, false); $this->queryBuilderMock->expects(self::once()) ->method('from') @@ -66,11 +204,18 @@ public function testGetMultipleLeadCounts(): void ->with('l.leadlist_id', $listIds) ->willReturnSelf(); - $this->expressionMock + $this->expressionMock->expects(self::once()) ->method('eq') ->with('l.manually_removed', ':false') ->willReturnSelf(); + $this->queryBuilderMock->expects(self::once()) + ->method('setParameter') + ->withConsecutive( + ['false', false, 'boolean'] + ) + ->willReturnSelf(); + self::assertSame(array_combine($listIds, $counts), $this->repository->getLeadCount($listIds)); } @@ -123,7 +268,7 @@ public function testGetSingleLeadCount(): void /** * @param array $queryResult */ - private function mockGetLeadCount(array $queryResult): void + private function mockGetLeadCount(array $queryResult, bool $addParam = true): void { $this->connection->method('createQueryBuilder') ->willReturn($this->queryBuilderMock); @@ -137,10 +282,12 @@ private function mockGetLeadCount(array $queryResult): void ->method('expr') ->willReturn($this->expressionMock); - $this->queryBuilderMock->expects(self::once()) - ->method('setParameter') - ->with('false', false, 'boolean') - ->willReturnSelf(); + if ($addParam) { + $this->queryBuilderMock->expects(self::once()) + ->method('setParameter') + ->with('false', false, 'boolean') + ->willReturnSelf(); + } $this->queryBuilderMock->expects(self::once()) ->method('where') diff --git a/app/bundles/LeadBundle/Tests/EventListener/DynamicContentSubscriberTest.php b/app/bundles/LeadBundle/Tests/EventListener/DynamicContentSubscriberTest.php new file mode 100644 index 00000000000..8922250cd13 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/EventListener/DynamicContentSubscriberTest.php @@ -0,0 +1,160 @@ +segmentRepository = $this->createMock(LeadListRepository::class); + $this->subscriber = new DynamicContentSubscriber($this->segmentRepository); + + parent::setUp(); + } + + public function testGetSubscribedEvents(): void + { + self::assertSame( + [ + DynamicContentEvents::ON_CONTACTS_FILTER_EVALUATE => ['onContactFilterEvaluate', 0], + ], + DynamicContentSubscriber::getSubscribedEvents() + ); + } + + public function testOnContactFilterEvaluateUnknownOperator(): void + { + $contactId = 1; + $filters = [ + [ + 'type' => 'leadlist', + 'operator' => 'unknownFilter', + 'filter' => null, + ], + ]; + $contact = (new Lead())->setId($contactId); + + $event = new ContactFiltersEvaluateEvent($filters, $contact); + + $this->expectException(\InvalidArgumentException::class); + + $this->subscriber->onContactFilterEvaluate($event); + } + + public function testOnContactFilterEvaluateEmpty(): void + { + $contactId = 1; + $filters = [ + [ + 'type' => 'leadlist', + 'operator' => OperatorOptions::EMPTY, + 'filter' => null, + ], + ]; + $contact = (new Lead())->setId($contactId); + + $event = new ContactFiltersEvaluateEvent($filters, $contact); + + $this->segmentRepository->expects(self::once()) + ->method('isNotContactInAnySegment') + ->with($contactId) + ->willReturn(true); + + $this->subscriber->onContactFilterEvaluate($event); + self::assertTrue($event->isEvaluated()); + self::assertTrue($event->isMatched()); + } + + public function testOnContactFilterEvaluateNotEmpty(): void + { + $contactId = 1; + $filters = [ + [ + 'type' => 'leadlist', + 'operator' => OperatorOptions::NOT_EMPTY, + 'filter' => null, + ], + ]; + $contact = (new Lead())->setId($contactId); + + $event = new ContactFiltersEvaluateEvent($filters, $contact); + + $this->segmentRepository->expects(self::once()) + ->method('isContactInAnySegment') + ->with($contactId) + ->willReturn(true); + + $this->subscriber->onContactFilterEvaluate($event); + self::assertTrue($event->isEvaluated()); + self::assertTrue($event->isMatched()); + } + + public function testOnContactFilterEvaluateNotIn(): void + { + $contactId = 1; + $filters = [ + [ + 'type' => 'leadlist', + 'operator' => OperatorOptions::IN, + 'filter' => ['something'], + ], + ]; + $contact = (new Lead())->setId($contactId); + + $event = new ContactFiltersEvaluateEvent($filters, $contact); + + $this->segmentRepository->expects(self::once()) + ->method('isContactInSegments') + ->with($contactId, $filters[0]['filter']) + ->willReturn(true); + + $this->subscriber->onContactFilterEvaluate($event); + self::assertTrue($event->isEvaluated()); + self::assertTrue($event->isMatched()); + } + + public function testOnContactFilterEvaluateNotNotIn(): void + { + $contactId = 1; + $filters = [ + [ + 'type' => 'leadlist', + 'operator' => OperatorOptions::NOT_IN, + 'filter' => ['something'], + ], + ]; + $contact = (new Lead())->setId($contactId); + + $event = new ContactFiltersEvaluateEvent($filters, $contact); + + $this->segmentRepository->expects(self::once()) + ->method('isNotContactInSegments') + ->with($contactId, $filters[0]['filter']) + ->willReturn(true); + + $this->subscriber->onContactFilterEvaluate($event); + self::assertTrue($event->isEvaluated()); + self::assertTrue($event->isMatched()); + } +} diff --git a/app/bundles/LeadBundle/Tests/EventListener/TypeOperatorSubscriberTest.php b/app/bundles/LeadBundle/Tests/EventListener/TypeOperatorSubscriberTest.php index a856c1e0271..269192f0118 100644 --- a/app/bundles/LeadBundle/Tests/EventListener/TypeOperatorSubscriberTest.php +++ b/app/bundles/LeadBundle/Tests/EventListener/TypeOperatorSubscriberTest.php @@ -121,8 +121,9 @@ public function testOnTypeOperatorsCollect(): void $this->assertNotContains(OperatorOptions::IN, $operators['text']['include']); $this->assertContains(OperatorOptions::EQUAL_TO, $operators['boolean']['include']); $this->assertNotContains(OperatorOptions::IN, $operators['boolean']['include']); - $this->assertContains(OperatorOptions::IN, $operators['date']['exclude']); - $this->assertNotContains(OperatorOptions::EQUAL_TO, $operators['date']['exclude']); + $this->assertNotContains(OperatorOptions::IN, $operators['date']['include']); + $this->assertContains(OperatorOptions::EQUAL_TO, $operators['date']['include']); + $this->assertContains(OperatorOptions::DATE, $operators['date']['include']); $this->assertContains(OperatorOptions::EQUAL_TO, $operators['number']['include']); $this->assertNotContains(OperatorOptions::IN, $operators['number']['include']); $this->assertContains(OperatorOptions::EMPTY, $operators['country']['include']); diff --git a/app/bundles/PageBundle/Tests/Controller/PageControllerFunctionalTest.php b/app/bundles/PageBundle/Tests/Controller/PageControllerFunctionalTest.php new file mode 100644 index 00000000000..92fb590bf8b --- /dev/null +++ b/app/bundles/PageBundle/Tests/Controller/PageControllerFunctionalTest.php @@ -0,0 +1,82 @@ +createSegment(); + $filter = [ + [ + 'glue' => 'and', + 'field' => 'leadlist', + 'object' => 'lead', + 'type' => 'leadlist', + 'filter' => [$segment->getId()], + 'display' => null, + 'operator' => 'in', + ], + ]; + $dynamicContent = $this->createDynamicContentWithSegmentFilter($filter); + + $dynamicContentToken = sprintf('{dwc=%s}', $dynamicContent->getSlotName()); + $page = $this->createPage($dynamicContentToken); + + $this->client->request(Request::METHOD_GET, sprintf('/%s', $page->getAlias())); + $response = $this->client->getResponse(); + $this->assertSame(200, $response->getStatusCode()); + $this->assertStringContainsString('Test Html', $response->getContent()); + } + + private function createSegment(): LeadList + { + $segment = new LeadList(); + $segment->setName('Segment 1'); + $segment->setPublicName('Segment 1'); + $segment->setAlias('segment_1'); + $this->em->persist($segment); + $this->em->flush(); + + return $segment; + } + + /** + * @param mixed[] $filters + */ + private function createDynamicContentWithSegmentFilter(array $filters = []): DynamicContent + { + $dynamicContent = new DynamicContent(); + $dynamicContent->setName('DC 1'); + $dynamicContent->setDescription('Customised value'); + $dynamicContent->setFilters($filters); + $dynamicContent->setIsCampaignBased(false); + $dynamicContent->setSlotName('Segment1_Slot'); + $this->em->persist($dynamicContent); + $this->em->flush(); + + return $dynamicContent; + } + + private function createPage(string $token = ''): Page + { + $page = new Page(); + $page->setIsPublished(true); + $page->setTitle('Page Title'); + $page->setAlias('page-alias'); + $page->setTemplate('Blank'); + $page->setCustomHtml('Test Html'.$token); + $this->em->persist($page); + $this->em->flush(); + + return $page; + } +}