From e63d604b954108b7502811c508bc953426d4aef5 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 3 Jan 2018 12:25:43 +0100 Subject: [PATCH 001/778] WIP - Segment refactoring - Count function moved to separate repository --- app/bundles/LeadBundle/Config/config.php | 25 +- .../LeadBundle/Controller/ListController.php | 9 + .../LeadBundle/Entity/LeadListRepository.php | 9 +- .../Entity/LeadListSegmentRepository.php | 1469 +++++++++++++++++ .../LeadBundle/Entity/OperatorListTrait.php | 114 +- app/bundles/LeadBundle/Model/ListModel.php | 17 +- .../LeadBundle/Segment/LeadSegmentFilter.php | 152 ++ .../Segment/LeadSegmentFilterFactory.php | 27 + .../LeadBundle/Segment/LeadSegmentFilters.php | 121 ++ .../LeadBundle/Segment/LeadSegmentService.php | 41 + .../LeadBundle/Segment/OperatorOptions.php | 133 ++ 11 files changed, 2001 insertions(+), 116 deletions(-) create mode 100644 app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilter.php create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilters.php create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentService.php create mode 100644 app/bundles/LeadBundle/Segment/OperatorOptions.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index cbc29e7a304..9815cf90ef8 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -752,11 +752,34 @@ ], ], 'mautic.lead.model.list' => [ - 'class' => 'Mautic\LeadBundle\Model\ListModel', + 'class' => \Mautic\LeadBundle\Model\ListModel::class, 'arguments' => [ 'mautic.helper.core_parameters', + 'mautic.lead.model.lead_segment_service', ], ], + 'mautic.lead.model.lead_segment_service' => [ + 'class' => \Mautic\LeadBundle\Segment\LeadSegmentService::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment_filter_factory', + 'mautic.lead.repository.lead_list_segment_repository', + ], + ], + 'mautic.lead.model.lead_segment_filter_factory' => [ + 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, + ], + 'mautic.lead.repository.lead_list_segment_repository' => [ + 'class' => \Mautic\LeadBundle\Entity\LeadListSegmentRepository::class, + 'arguments' => [ + 'doctrine.orm.entity_manager', + 'mautic.lead.segment.operator_options', + 'event_dispatcher', + 'translator', + ], + ], + 'mautic.lead.segment.operator_options' => [ + 'class' => \Mautic\LeadBundle\Segment\OperatorOptions::class, + ], 'mautic.lead.model.note' => [ 'class' => 'Mautic\LeadBundle\Model\NoteModel', ], diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index bb11f161172..2cd1e0cccb9 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -558,6 +558,15 @@ protected function changeList($listId, $action) */ public function viewAction($objectId) { + /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ + $listModel = $this->get('mautic.lead.model.list'); + + $list = $listModel->getEntity($objectId); + //dump($list); + $processed = $listModel->rebuildListLeads($list, 2, 2); + dump($processed); + exit; + /** @var \Mautic\LeadBundle\Model\ListModel $model */ $model = $this->getModel('lead.list'); $security = $this->get('mautic.security'); diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 8cdfbae108c..e28dbf9cf37 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -320,7 +320,7 @@ public function getLeadCount($listIds) public function getLeadsByList($lists, $args = []) { // Return only IDs - $idOnly = (!array_key_exists('idOnly', $args)) ? false : $args['idOnly']; + $idOnly = (!array_key_exists('idOnly', $args)) ? false : $args['idOnly']; //Always TRUE // Return counts $countOnly = (!array_key_exists('countOnly', $args)) ? false : $args['countOnly']; // Return only leads that have not been added or manually manipulated to the lists yet @@ -328,7 +328,7 @@ public function getLeadsByList($lists, $args = []) // Return leads that do not belong to a list based on filters $nonMembersOnly = (!array_key_exists('nonMembersOnly', $args)) ? false : $args['nonMembersOnly']; // Use filters to dynamically generate the list - $dynamic = ($newOnly || $nonMembersOnly || (!$newOnly && !$nonMembersOnly && $countOnly)); + $dynamic = ($newOnly || $nonMembersOnly || (!$newOnly && !$nonMembersOnly && $countOnly)); ///This is always true - we can ommit conditions // Limiters $batchLimiters = (!array_key_exists('batchLimiters', $args)) ? false : $args['batchLimiters']; $start = (!array_key_exists('start', $args)) ? false : $args['start']; @@ -389,7 +389,8 @@ public function getLeadsByList($lists, $args = []) if ($newOnly || !$nonMembersOnly) { // !$nonMembersOnly is mainly used for tests as we just want a live count $expr = $this->generateSegmentExpression($filters, $parameters, $q, null, $id); - + dump($expr); + dump($expr->count()); if (!$this->hasCompanyFilter && !$expr->count()) { // Treat this as if it has no filters since all the filters are now invalid (fields were deleted) $return[$id] = []; @@ -507,6 +508,8 @@ public function getLeadsByList($lists, $args = []) // remove any possible group by $q->resetQueryPart('groupBy'); } + dump($q->getSQL()); + dump($q->getParameters()); $results = $q->execute()->fetchAll(); diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php new file mode 100644 index 00000000000..87bb2512770 --- /dev/null +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -0,0 +1,1469 @@ +entityManager = $entityManager; + $this->operatorOptions = $operatorOptions; + $this->dispatcher = $dispatcher; + $this->translator = $translator; + } + + public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilters, array $batchLimiters) + { + //TODO + $withMinId = false; + $limit = null; + $start = null; + + if (!count($leadSegmentFilters)) { + return 0; + } + + $q = $this->entityManager->getConnection()->createQueryBuilder(); + $select = 'count(l.id) as lead_count, max(l.id) as max_id'; + if ($withMinId) { + $select .= ', min(l.id) as min_id'; + } + + $q->select($select) + ->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); + + $batchExpr = $q->expr()->andX(); + // Only leads that existed at the time of count + if ($batchLimiters) { + if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { + $batchExpr->add( + $q->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") + ); + } elseif (!empty($batchLimiters['maxId'])) { + $batchExpr->add( + $q->expr()->lte('l.id', $batchLimiters['maxId']) + ); + } + } + + $expr = $this->generateSegmentExpression($leadSegmentFilters, $q, $id); + + // Leads that do not have any record in the lead_lists_leads table for this lead list + // For non null fields - it's apparently better to use left join over not exists due to not using nullable + // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ + $listOnExpr = $q->expr()->andX( + $q->expr()->eq('ll.leadlist_id', $id), + $q->expr()->eq('ll.lead_id', 'l.id') + ); + + if (!empty($batchLimiters['dateTime'])) { + // Only leads in the list at the time of count + $listOnExpr->add( + $q->expr()->lte('ll.date_added', $q->expr()->literal($batchLimiters['dateTime'])) + ); + } + + $q->leftJoin( + 'l', + MAUTIC_TABLE_PREFIX.'lead_lists_leads', + 'll', + $listOnExpr + ); + + $expr->add($q->expr()->isNull('ll.lead_id')); + + if ($batchExpr->count()) { + $expr->add($batchExpr); + } + + if ($expr->count()) { + $q->andWhere($expr); + } + + if (!empty($limit)) { + $q->setFirstResult($start) + ->setMaxResults($limit); + } + + // remove any possible group by + $q->resetQueryPart('groupBy'); + + dump($q->getSQL()); + echo 'SQL parameters:'; + dump($q->getParameters()); + + $results = $q->execute()->fetchAll(); + + $leads = []; + foreach ($results as $r) { + $leads = [ + 'count' => $r['lead_count'], + 'maxId' => $r['max_id'], + ]; + if ($withMinId) { + $leads['minId'] = $r['min_id']; + } + } + + return $leads; + } + + private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) + { + $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); + + if ($leadSegmentFilters->isHasCompanyFilter()) { + $this->applyCompanyFieldFilters($q); + } + + return $expr; + } + + /** + * @param LeadSegmentFilters $leadSegmentFilters + * @param QueryBuilder $q + * @param int $listId + * + * @return \Doctrine\DBAL\Query\Expression\CompositeExpression|mixed + */ + private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId) + { + $parameters = []; + + $schema = $this->entityManager->getConnection()->getSchemaManager(); + // Get table columns + $leadTableSchema = $schema->listTableColumns(MAUTIC_TABLE_PREFIX.'leads'); + $companyTableSchema = $schema->listTableColumns(MAUTIC_TABLE_PREFIX.'companies'); + + $options = $this->operatorOptions->getFilterExpressionFunctionsNonStatic(); + + // Add custom filters operators + $event = new LeadListFiltersOperatorsEvent($options, $this->translator); + $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_OPERATORS_ON_GENERATE, $event); + $options = $event->getOperators(); + + $groups = []; + $groupExpr = $q->expr()->andX(); + + foreach ($leadSegmentFilters as $k => $leadSegmentFilter) { + $object = $leadSegmentFilter->getObject(); + + $column = false; + $field = false; + $columnType = false; + + $filterField = $leadSegmentFilter->getField(); + + if ($leadSegmentFilter->isLeadType()) { + $column = isset($leadTableSchema[$filterField]) ? $leadTableSchema[$filterField] : false; + $field = "l.{$filterField}"; + } elseif ($leadSegmentFilter->isCompanyType()) { + $column = isset($companyTableSchema[$filterField]) ? $companyTableSchema[$filterField] : false; + $field = "comp.{$filterField}"; + } + + $operatorDetails = $options[$leadSegmentFilter->getOperator()]; + $func = $operatorDetails['expr']; + + if ($column) { + // Format the field based on platform specific functions that DBAL doesn't support natively + $formatter = AbstractFormatter::createFormatter($this->entityManager->getConnection()); + $columnType = $column->getType(); + + switch ($leadSegmentFilter->getType()) { + case 'datetime': + if (!$columnType instanceof UTCDateTimeType) { + $field = $formatter->toDateTime($field); + } + break; + case 'date': + if (!$columnType instanceof DateType && !$columnType instanceof UTCDateTimeType) { + $field = $formatter->toDate($field); + } + break; + case 'time': + if (!$columnType instanceof TimeType && !$columnType instanceof UTCDateTimeType) { + $field = $formatter->toTime($field); + } + break; + case 'number': + if (!$columnType instanceof IntegerType && !$columnType instanceof FloatType) { + $field = $formatter->toNumeric($field); + } + break; + } + } + + //the next one will determine the group + if ($leadSegmentFilter->getGlue() === 'or') { + // Create a new group of andX expressions + if ($groupExpr->count()) { + $groups[] = $groupExpr; + $groupExpr = $q->expr()->andX(); + } + } + + $parameter = $this->generateRandomParameterName(); + $exprParameter = ":$parameter"; + $ignoreAutoFilter = false; + + // Special handling of relative date strings + if ($leadSegmentFilter->getType() === 'datetime' || $leadSegmentFilter->getType() === 'date') { + $relativeDateStrings = $this->getRelativeDateStrings(); + // Check if the column type is a date/time stamp + $isTimestamp = ($leadSegmentFilter->getType() === 'datetime' || $columnType instanceof UTCDateTimeType); + $getDate = function ($string) use ($isTimestamp, $relativeDateStrings, $leadSegmentFilter, &$func) { + $key = array_search($string, $relativeDateStrings, true); + $dtHelper = new DateTimeHelper('midnight today', null, 'local'); + $requiresBetween = in_array($func, ['eq', 'neq'], true) && $isTimestamp; + $timeframe = str_replace('mautic.lead.list.', '', $key); + $modifier = false; + $isRelative = true; + + switch ($timeframe) { + case 'birthday': + case 'anniversary': + $func = 'like'; + $isRelative = false; + $leadSegmentFilter->setOperator('like'); + $leadSegmentFilter->setFilter('%'.date('-m-d')); + break; + case 'today': + case 'tomorrow': + case 'yesterday': + if ($timeframe === 'yesterday') { + $dtHelper->modify('-1 day'); + } elseif ($timeframe === 'tomorrow') { + $dtHelper->modify('+1 day'); + } + + // Today = 2015-08-28 00:00:00 + if ($requiresBetween) { + // eq: + // field >= 2015-08-28 00:00:00 + // field < 2015-08-29 00:00:00 + + // neq: + // field < 2015-08-28 00:00:00 + // field >= 2015-08-29 00:00:00 + $modifier = '+1 day'; + } else { + // lt: + // field < 2015-08-28 00:00:00 + // gt: + // field > 2015-08-28 23:59:59 + + // lte: + // field <= 2015-08-28 23:59:59 + // gte: + // field >= 2015-08-28 00:00:00 + if (in_array($func, ['gt', 'lte'])) { + $modifier = '+1 day -1 second'; + } + } + break; + case 'week_last': + case 'week_next': + case 'week_this': + $interval = str_replace('week_', '', $timeframe); + $dtHelper->setDateTime('midnight monday '.$interval.' week', null); + + // This week: Monday 2015-08-24 00:00:00 + if ($requiresBetween) { + // eq: + // field >= Mon 2015-08-24 00:00:00 + // field < Mon 2015-08-31 00:00:00 + + // neq: + // field < Mon 2015-08-24 00:00:00 + // field >= Mon 2015-08-31 00:00:00 + $modifier = '+1 week'; + } else { + // lt: + // field < Mon 2015-08-24 00:00:00 + // gt: + // field > Sun 2015-08-30 23:59:59 + + // lte: + // field <= Sun 2015-08-30 23:59:59 + // gte: + // field >= Mon 2015-08-24 00:00:00 + if (in_array($func, ['gt', 'lte'])) { + $modifier = '+1 week -1 second'; + } + } + break; + + case 'month_last': + case 'month_next': + case 'month_this': + $interval = substr($key, -4); + $dtHelper->setDateTime('midnight first day of '.$interval.' month', null); + + // This month: 2015-08-01 00:00:00 + if ($requiresBetween) { + // eq: + // field >= 2015-08-01 00:00:00 + // field < 2015-09:01 00:00:00 + + // neq: + // field < 2015-08-01 00:00:00 + // field >= 2016-09-01 00:00:00 + $modifier = '+1 month'; + } else { + // lt: + // field < 2015-08-01 00:00:00 + // gt: + // field > 2015-08-31 23:59:59 + + // lte: + // field <= 2015-08-31 23:59:59 + // gte: + // field >= 2015-08-01 00:00:00 + if (in_array($func, ['gt', 'lte'])) { + $modifier = '+1 month -1 second'; + } + } + break; + case 'year_last': + case 'year_next': + case 'year_this': + $interval = substr($key, -4); + $dtHelper->setDateTime('midnight first day of '.$interval.' year', null); + + // This year: 2015-01-01 00:00:00 + if ($requiresBetween) { + // eq: + // field >= 2015-01-01 00:00:00 + // field < 2016-01-01 00:00:00 + + // neq: + // field < 2015-01-01 00:00:00 + // field >= 2016-01-01 00:00:00 + $modifier = '+1 year'; + } else { + // lt: + // field < 2015-01-01 00:00:00 + // gt: + // field > 2015-12-31 23:59:59 + + // lte: + // field <= 2015-12-31 23:59:59 + // gte: + // field >= 2015-01-01 00:00:00 + if (in_array($func, ['gt', 'lte'])) { + $modifier = '+1 year -1 second'; + } + } + break; + default: + $isRelative = false; + break; + } + + // check does this match php date params pattern? + if ($timeframe !== 'anniversary' && + (stristr($string[0], '-') || stristr($string[0], '+'))) { + $date = new \DateTime('now'); + $date->modify($string); + + $dateTime = $date->format('Y-m-d H:i:s'); + $dtHelper->setDateTime($dateTime, null); + + $isRelative = true; + } + + if ($isRelative) { + if ($requiresBetween) { + $startWith = ($isTimestamp) ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); + + $dtHelper->modify($modifier); + $endWith = ($isTimestamp) ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); + + // Use a between statement + $func = ($func == 'neq') ? 'notBetween' : 'between'; + $leadSegmentFilter->setFilter([$startWith, $endWith]); + } else { + if ($modifier) { + $dtHelper->modify($modifier); + } + + $details['filter'] = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); + } + } + }; + + if (is_array($leadSegmentFilter->getFilter())) { + foreach ($leadSegmentFilter->getFilter() as $filterValue) { + $getDate($filterValue); + } + } else { + $getDate($leadSegmentFilter->getFilter()); + } + } + + // Generate a unique alias + $alias = $this->generateRandomParameterName(); + + switch ($leadSegmentFilter->getField()) { + case 'hit_url': + case 'referer': + case 'source': + case 'source_id': + case 'url_title': + $operand = in_array( + $func, + [ + 'eq', + 'like', + 'regexp', + 'notRegexp', + 'startsWith', + 'endsWith', + 'contains', + ] + ) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + $column = $leadSegmentFilter->getField(); + + if ($column === 'hit_url') { + $column = 'url'; + } + + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX.'page_hits', $alias); + + switch ($func) { + case 'eq': + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.'.$column, $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) + ); + break; + case 'regexp': + case 'notRegexp': + $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); + $not = ($func === 'notRegexp') ? ' NOT' : ''; + $subqb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.lead_id', 'l.id'), + $alias.'.'.$column.$not.' REGEXP '.$exprParameter + ) + ); + break; + case 'like': + case 'notLike': + case 'startsWith': + case 'endsWith': + case 'contains': + switch ($func) { + case 'like': + case 'notLike': + case 'contains': + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; + break; + case 'startsWith': + $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; + break; + case 'endsWith': + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); + break; + } + + $subqb->where( + $q->expr()->andX( + $q->expr()->like($alias.'.'.$column, $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) + ); + break; + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'device_model': + $ignoreAutoFilter = true; + $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp']) ? 'EXISTS' : 'NOT EXISTS'; + + $column = $leadSegmentFilter->getField(); + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX.'lead_devices', $alias); + switch ($func) { + case 'eq': + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.'.$column, $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) + ); + break; + case 'like': + case '!like': + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; + $subqb->where( + $q->expr()->andX( + $q->expr()->like($alias.'.'.$column, $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) + ); + break; + case 'regexp': + case 'notRegexp': + $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); + $not = ($func === 'notRegexp') ? ' NOT' : ''; + $subqb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.lead_id', 'l.id'), + $alias.'.'.$column.$not.' REGEXP '.$exprParameter + ) + ); + break; + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + + break; + case 'hit_url_date': + case 'lead_email_read_date': + $operand = (in_array($func, ['eq', 'gt', 'lt', 'gte', 'lte', 'between'])) ? 'EXISTS' : 'NOT EXISTS'; + $table = 'page_hits'; + $column = 'date_hit'; + + if ($leadSegmentFilter->getField() === 'lead_email_read_date') { + $column = 'date_read'; + $table = 'email_stats'; + } + + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + + switch ($func) { + case 'eq': + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias.'.'.$column, $exprParameter), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) + ); + break; + case 'between': + case 'notBetween': + // Filter should be saved with double || to separate options + $parameter2 = $this->generateRandomParameterName(); + $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; + $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; + $exprParameter2 = ":$parameter2"; + $ignoreAutoFilter = true; + $field = $column; + + if ($func === 'between') { + $subqb->where( + $q->expr() + ->andX( + $q->expr()->gte($alias.'.'.$field, $exprParameter), + $q->expr()->lt($alias.'.'.$field, $exprParameter2), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) + ); + } else { + $subqb->where( + $q->expr() + ->andX( + $q->expr()->lt($alias.'.'.$field, $exprParameter), + $q->expr()->gte($alias.'.'.$field, $exprParameter2), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) + ); + } + break; + default: + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->$func( + $alias.'.'.$column, + $exprParameter + ), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) + ); + break; + } + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'page_id': + case 'email_id': + case 'redirect_id': + case 'notification': + $operand = ($func === 'eq') ? 'EXISTS' : 'NOT EXISTS'; + $column = $leadSegmentFilter->getField(); + $table = 'page_hits'; + $select = 'id'; + + if ($leadSegmentFilter->getField() === 'notification') { + $table = 'push_ids'; + $column = 'id'; + } + + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + + if ($leadSegmentFilter->getFilter() == 1) { + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->isNotNull($alias.'.'.$column), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) + ); + } else { + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->isNull($alias.'.'.$column), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) + ); + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'sessions': + $operand = 'EXISTS'; + $table = 'page_hits'; + $select = 'COUNT(id)'; + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + + $alias2 = $this->generateRandomParameterName(); + $subqb2 = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($alias2.'.id') + ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); + + $subqb2->where( + $q->expr() + ->andX( + $q->expr()->eq($alias2.'.lead_id', 'l.id'), + $q->expr()->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), + $q->expr()->lt($alias2.'.date_hit', $alias.'.date_hit') + ) + ); + + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias.'.lead_id', 'l.id'), + $q->expr() + ->isNull($alias.'.email_id'), + $q->expr() + ->isNull($alias.'.redirect_id'), + sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()) + ) + ); + + $opr = ''; + switch ($func) { + case 'eq': + $opr = '='; + break; + case 'gt': + $opr = '>'; + break; + case 'gte': + $opr = '>='; + break; + case 'lt': + $opr = '<'; + break; + case 'lte': + $opr = '<='; + break; + } + if ($opr) { + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); + } + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'hit_url_count': + case 'lead_email_read_count': + $operand = 'EXISTS'; + $table = 'page_hits'; + $select = 'COUNT(id)'; + if ($leadSegmentFilter->getField() === 'lead_email_read_count') { + $table = 'email_stats'; + $select = 'COALESCE(SUM(open_count),0)'; + } + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) + ); + + $opr = ''; + switch ($func) { + case 'eq': + $opr = '='; + break; + case 'gt': + $opr = '>'; + break; + case 'gte': + $opr = '>='; + break; + case 'lt': + $opr = '<'; + break; + case 'lte': + $opr = '<='; + break; + } + + if ($opr) { + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + + case 'dnc_bounced': + case 'dnc_unsubscribed': + case 'dnc_bounced_sms': + case 'dnc_unsubscribed_sms': + // Special handling of do not contact + $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; + + $parts = explode('_', $leadSegmentFilter->getField()); + $channel = 'email'; + + if (count($parts) === 3) { + $channel = $parts[2]; + } + + $channelParameter = $this->generateRandomParameterName(); + $subqb = $this->entityManager->getConnection()->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) + ->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.reason', $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id'), + $q->expr()->eq($alias.'.channel', ":$channelParameter") + ) + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subqb->getSQL()) + ); + + // Filter will always be true and differentiated via EXISTS/NOT EXISTS + $leadSegmentFilter->setFilter(true); + + $ignoreAutoFilter = true; + + $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; + $parameters[$channelParameter] = $channel; + + break; + + case 'leadlist': + $table = 'lead_lists_leads'; + $column = 'leadlist_id'; + $falseParameter = $this->generateRandomParameterName(); + $parameters[$falseParameter] = false; + $trueParameter = $this->generateRandomParameterName(); + $parameters[$trueParameter] = true; + $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; + $ignoreAutoFilter = true; + + if ($filterListIds = (array) $leadSegmentFilter->getFilter()) { + $listQb = $this->entityManager->getConnection()->createQueryBuilder() + ->select('l.id, l.filters') + ->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'l'); + $listQb->where( + $listQb->expr()->in('l.id', $filterListIds) + ); + $filterLists = $listQb->execute()->fetchAll(); + $not = 'NOT EXISTS' === $func; + + // Each segment's filters must be appended as ORs so that each list is evaluated individually + $existsExpr = $not ? $listQb->expr()->andX() : $listQb->expr()->orX(); + + foreach ($filterLists as $list) { + $alias = $this->generateRandomParameterName(); + $id = (int) $list['id']; + if ($id === (int) $listId) { + // Ignore as somehow self is included in the list + continue; + } + + $listFilters = unserialize($list['filters']); + if (empty($listFilters)) { + // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $id, + $parameters, + [ + $alias.'.manually_removed' => $falseParameter, + ] + ); + } else { + // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet + // but also leverage the current membership to take into account those manually added or removed from the segment + + // Build a "live" query based on current filters to catch those that have not been processed yet + $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); + $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); + + // Left join membership to account for manually added and removed + $membershipAlias = $this->generateRandomParameterName(); + $subQb->leftJoin( + $alias, + MAUTIC_TABLE_PREFIX.$table, + $membershipAlias, + "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" + ) + ->where( + $subQb->expr()->orX( + $filterExpr, + $subQb->expr()->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added + ) + ) + ->andWhere( + $subQb->expr()->eq("$alias.id", 'l.id'), + $subQb->expr()->orX( + $subQb->expr()->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr()->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + ) + ); + } + + $existsExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); + } + + if ($existsExpr->count()) { + $groupExpr->add($existsExpr); + } + } + + break; + case 'tags': + case 'globalcategory': + case 'lead_email_received': + case 'lead_email_sent': + case 'device_type': + case 'device_brand': + case 'device_os': + // Special handling of lead lists and tags + $func = in_array($func, ['eq', 'in'], true) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + + // Collect these and apply after building the query because we'll want to apply the lead first for each of the subqueries + $subQueryFilters = []; + switch ($leadSegmentFilter->getField()) { + case 'tags': + $table = 'lead_tags_xref'; + $column = 'tag_id'; + break; + case 'globalcategory': + $table = 'lead_categories'; + $column = 'category_id'; + break; + case 'lead_email_received': + $table = 'email_stats'; + $column = 'email_id'; + + $trueParameter = $this->generateRandomParameterName(); + $subQueryFilters[$alias.'.is_read'] = $trueParameter; + $parameters[$trueParameter] = true; + break; + case 'lead_email_sent': + $table = 'email_stats'; + $column = 'email_id'; + break; + case 'device_type': + $table = 'lead_devices'; + $column = 'device'; + break; + case 'device_brand': + $table = 'lead_devices'; + $column = 'device_brand'; + break; + case 'device_os': + $table = 'lead_devices'; + $column = 'device_os_name'; + break; + } + + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $leadSegmentFilter->getFilter(), + $parameters, + $subQueryFilters + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); + break; + case 'stage': + // A note here that SQL EXISTS is being used for the eq and neq cases. + // I think this code might be inefficient since the sub-query is rerun + // for every row in the outer query's table. This might have to be refactored later on + // if performance is desired. + + $subQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'stages', $alias); + + switch ($func) { + case 'empty': + $groupExpr->add( + $q->expr()->isNull('l.stage_id') + ); + break; + case 'notEmpty': + $groupExpr->add( + $q->expr()->isNotNull('l.stage_id') + ); + break; + case 'eq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subQb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.id', 'l.stage_id'), + $q->expr()->eq($alias.'.id', ":$parameter") + ) + ); + $groupExpr->add(sprintf('EXISTS (%s)', $subQb->getSQL())); + break; + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subQb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.id', 'l.stage_id'), + $q->expr()->eq($alias.'.id', ":$parameter") + ) + ); + $groupExpr->add(sprintf('NOT EXISTS (%s)', $subQb->getSQL())); + break; + } + + break; + case 'integration_campaigns': + $parameter2 = $this->generateRandomParameterName(); + $operand = in_array($func, ['eq', 'neq']) ? 'EXISTS' : 'NOT EXISTS'; + $ignoreAutoFilter = true; + + $subQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'integration_entity', $alias); + switch ($func) { + case 'eq': + case 'neq': + if (strpos($leadSegmentFilter->getFilter(), '::') !== false) { + list($integrationName, $campaignId) = explode('::', $leadSegmentFilter->getFilter()); + } else { + // Assuming this is a Salesforce integration for BC with pre 2.11.0 + $integrationName = 'Salesforce'; + $campaignId = $leadSegmentFilter->getFilter(); + } + + $parameters[$parameter] = $campaignId; + $parameters[$parameter2] = $integrationName; + $subQb->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.integration', ":$parameter2"), + $q->expr()->eq($alias.'.integration_entity', "'CampaignMember'"), + $q->expr()->eq($alias.'.integration_entity_id', ":$parameter"), + $q->expr()->eq($alias.'.internal_entity', "'lead'"), + $q->expr()->eq($alias.'.internal_entity_id', 'l.id') + ) + ); + break; + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subQb->getSQL())); + + break; + default: + if (!$column) { + // Column no longer exists so continue + continue; + } + + if ('company' === $object) { + // Must tell getLeadsByList how to best handle the relationship with the companies table + if (!in_array($func, ['empty', 'neq', 'notIn', 'notLike'], true)) { + $this->listFiltersInnerJoinCompany = true; + } + } + + switch ($func) { + case 'between': + case 'notBetween': + // Filter should be saved with double || to separate options + $parameter2 = $this->generateRandomParameterName(); + $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; + $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; + $exprParameter2 = ":$parameter2"; + $ignoreAutoFilter = true; + + if ($func === 'between') { + $groupExpr->add( + $q->expr()->andX( + $q->expr()->gte($field, $exprParameter), + $q->expr()->lt($field, $exprParameter2) + ) + ); + } else { + $groupExpr->add( + $q->expr()->andX( + $q->expr()->lt($field, $exprParameter), + $q->expr()->gte($field, $exprParameter2) + ) + ); + } + break; + + case 'notEmpty': + $groupExpr->add( + $q->expr()->andX( + $q->expr()->isNotNull($field), + $q->expr()->neq($field, $q->expr()->literal('')) + ) + ); + $ignoreAutoFilter = true; + break; + + case 'empty': + $leadSegmentFilter->setFilter(''); + $groupExpr->add( + $this->generateFilterExpression($q, $field, 'eq', $exprParameter, true) + ); + break; + + case 'in': + case 'notIn': + $cleanFilter = []; + foreach ($leadSegmentFilter->getFilter() as $key => $value) { + $cleanFilter[] = $q->expr()->literal( + InputHelper::clean($value) + ); + } + $leadSegmentFilter->setFilter($cleanFilter); + + if ($leadSegmentFilter->getType() === 'multiselect') { + foreach ($leadSegmentFilter->getFilter() as $filter) { + $filter = trim($filter, "'"); + + if (substr($func, 0, 3) === 'not') { + $operator = 'NOT REGEXP'; + } else { + $operator = 'REGEXP'; + } + + $groupExpr->add( + $field." $operator '\\\\|?$filter\\\\|?'" + ); + } + } else { + $groupExpr->add( + $this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null) + ); + } + $ignoreAutoFilter = true; + break; + + case 'neq': + $groupExpr->add( + $this->generateFilterExpression($q, $field, $func, $exprParameter, null) + ); + break; + + case 'like': + case 'notLike': + case 'startsWith': + case 'endsWith': + case 'contains': + $ignoreAutoFilter = true; + + switch ($func) { + case 'like': + case 'notLike': + $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%'.$leadSegmentFilter->getFilter().'%' + : $leadSegmentFilter->getFilter(); + break; + case 'startsWith': + $func = 'like'; + $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; + break; + case 'endsWith': + $func = 'like'; + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); + break; + case 'contains': + $func = 'like'; + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; + break; + } + + $groupExpr->add( + $this->generateFilterExpression($q, $field, $func, $exprParameter, null) + ); + break; + case 'regexp': + case 'notRegexp': + $ignoreAutoFilter = true; + $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); + $not = ($func === 'notRegexp') ? ' NOT' : ''; + $groupExpr->add( + // Escape single quotes while accounting for those that may already be escaped + $field.$not.' REGEXP '.$exprParameter + ); + break; + default: + $groupExpr->add($q->expr()->$func($field, $exprParameter)); + } + } + + if (!$ignoreAutoFilter) { + if (!is_array($leadSegmentFilter->getFilter())) { + switch ($leadSegmentFilter->getType()) { + case 'number': + $leadSegmentFilter->setFilter((float) $leadSegmentFilter->getFilter()); + break; + + case 'boolean': + $leadSegmentFilter->setFilter((bool) $leadSegmentFilter->getFilter()); + break; + } + } + + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + } + + if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { + $event = new LeadListFilteringEvent($leadSegmentFilter, null, $alias, $func, $q, $this->entityManager); + $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); + if ($event->isFilteringDone()) { + $groupExpr->add($event->getSubQuery()); + } + } + } + + // Get the last of the filters + if ($groupExpr->count()) { + $groups[] = $groupExpr; + } + if (count($groups) === 1) { + // Only one andX expression + $expr = $groups[0]; + } elseif (count($groups) > 1) { + // Sets of expressions grouped by OR + $orX = $q->expr()->orX(); + $orX->addMultiple($groups); + + // Wrap in a andX for other functions to append + $expr = $q->expr()->andX($orX); + } else { + $expr = $groupExpr; + } + + foreach ($parameters as $k => $v) { + $paramType = null; + + if (is_array($v) && isset($v['type'], $v['value'])) { + $paramType = $v['type']; + $v = $v['value']; + } + $q->setParameter($k, $v, $paramType); + } + + return $expr; + } + + /** + * Generate a unique parameter name. + * + * @return string + */ + private function generateRandomParameterName() + { + $alpha_numeric = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + $paramName = substr(str_shuffle($alpha_numeric), 0, 8); + + if (!in_array($paramName, $this->usedParameterNames, true)) { + $this->usedParameterNames[] = $paramName; + + return $paramName; + } + + return $this->generateRandomParameterName(); + } + + /** + * @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q + * @param $column + * @param $operator + * @param $parameter + * @param $includeIsNull true/false or null to auto determine based on operator + * + * @return mixed + */ + public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) + { + // in/notIn for dbal will use a raw array + if (!is_array($parameter) && strpos($parameter, ':') !== 0) { + $parameter = ":$parameter"; + } + + if (null === $includeIsNull) { + // Auto determine based on negate operators + $includeIsNull = (in_array($operator, ['neq', 'notLike', 'notIn'])); + } + + if ($includeIsNull) { + $expr = $q->expr()->orX( + $q->expr()->$operator($column, $parameter), + $q->expr()->isNull($column) + ); + } else { + $expr = $q->expr()->$operator($column, $parameter); + } + + return $expr; + } + + /** + * @return array + */ + private function getRelativeDateStrings() + { + $keys = self::getRelativeDateTranslationKeys(); + + $strings = []; + foreach ($keys as $key) { + $strings[$key] = $this->translator->trans($key); + } + + return $strings; + } + + /** + * @return array + */ + private static function getRelativeDateTranslationKeys() + { + return [ + 'mautic.lead.list.month_last', + 'mautic.lead.list.month_next', + 'mautic.lead.list.month_this', + 'mautic.lead.list.today', + 'mautic.lead.list.tomorrow', + 'mautic.lead.list.yesterday', + 'mautic.lead.list.week_last', + 'mautic.lead.list.week_next', + 'mautic.lead.list.week_this', + 'mautic.lead.list.year_last', + 'mautic.lead.list.year_next', + 'mautic.lead.list.year_this', + 'mautic.lead.list.anniversary', + ]; + } + + /** + * @param $table + * @param $alias + * @param $column + * @param $value + * @param array $parameters + * @param null $leadId + * @param array $subQueryFilters + * + * @return QueryBuilder + */ + protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) + { + $subQb = $this->entityManager->getConnection()->createQueryBuilder(); + $subExpr = $subQb->expr()->andX(); + + if ('leads' !== $table) { + $subExpr->add( + $subQb->expr()->eq($alias.'.lead_id', 'l.id') + ); + } + + foreach ($subQueryFilters as $subColumn => $subParameter) { + $subExpr->add( + $subQb->expr()->eq($subColumn, ":$subParameter") + ); + } + + if (null !== $value && !empty($column)) { + $subFilterParamter = $this->generateRandomParameterName(); + $subFunc = 'eq'; + if (is_array($value)) { + $subFunc = 'in'; + $subExpr->add( + $subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") + ); + $parameters[$subFilterParamter] = ['value' => $value, 'type' => \Doctrine\DBAL\Connection::PARAM_STR_ARRAY]; + } else { + $parameters[$subFilterParamter] = $value; + } + + $subExpr->add( + $subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") + ); + } + + $subQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.$table, $alias) + ->where($subExpr); + + return $subQb; + } + + /** + * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should + * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. + * + * @param QueryBuilder $q + */ + private function applyCompanyFieldFilters(QueryBuilder $q) + { + $joinType = $this->listFiltersInnerJoinCompany ? 'join' : 'leftJoin'; + // Join company tables for query optimization + $q->$joinType('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'l.id = cl.lead_id') + ->$joinType( + 'cl', + MAUTIC_TABLE_PREFIX.'companies', + 'comp', + 'cl.company_id = comp.id' + ); + + // Return only unique contacts + $q->groupBy('l.id'); + } +} diff --git a/app/bundles/LeadBundle/Entity/OperatorListTrait.php b/app/bundles/LeadBundle/Entity/OperatorListTrait.php index 156a6d8d5e1..1f11a135d63 100644 --- a/app/bundles/LeadBundle/Entity/OperatorListTrait.php +++ b/app/bundles/LeadBundle/Entity/OperatorListTrait.php @@ -11,6 +11,8 @@ namespace Mautic\LeadBundle\Entity; +use Mautic\LeadBundle\Segment\OperatorOptions; + trait OperatorListTrait { protected $typeOperators = [ @@ -76,114 +78,6 @@ trait OperatorListTrait ], ]; - protected $operatorOptions = [ - '=' => [ - 'label' => 'mautic.lead.list.form.operator.equals', - 'expr' => 'eq', - 'negate_expr' => 'neq', - ], - '!=' => [ - 'label' => 'mautic.lead.list.form.operator.notequals', - 'expr' => 'neq', - 'negate_expr' => 'eq', - ], - 'gt' => [ - 'label' => 'mautic.lead.list.form.operator.greaterthan', - 'expr' => 'gt', - 'negate_expr' => 'lt', - ], - 'gte' => [ - 'label' => 'mautic.lead.list.form.operator.greaterthanequals', - 'expr' => 'gte', - 'negate_expr' => 'lt', - ], - 'lt' => [ - 'label' => 'mautic.lead.list.form.operator.lessthan', - 'expr' => 'lt', - 'negate_expr' => 'gt', - ], - 'lte' => [ - 'label' => 'mautic.lead.list.form.operator.lessthanequals', - 'expr' => 'lte', - 'negate_expr' => 'gt', - ], - 'empty' => [ - 'label' => 'mautic.lead.list.form.operator.isempty', - 'expr' => 'empty', //special case - 'negate_expr' => 'notEmpty', - ], - '!empty' => [ - 'label' => 'mautic.lead.list.form.operator.isnotempty', - 'expr' => 'notEmpty', //special case - 'negate_expr' => 'empty', - ], - 'like' => [ - 'label' => 'mautic.lead.list.form.operator.islike', - 'expr' => 'like', - 'negate_expr' => 'notLike', - ], - '!like' => [ - 'label' => 'mautic.lead.list.form.operator.isnotlike', - 'expr' => 'notLike', - 'negate_expr' => 'like', - ], - 'between' => [ - 'label' => 'mautic.lead.list.form.operator.between', - 'expr' => 'between', //special case - 'negate_expr' => 'notBetween', - // @todo implement in list UI - 'hide' => true, - ], - '!between' => [ - 'label' => 'mautic.lead.list.form.operator.notbetween', - 'expr' => 'notBetween', //special case - 'negate_expr' => 'between', - // @todo implement in list UI - 'hide' => true, - ], - 'in' => [ - 'label' => 'mautic.lead.list.form.operator.in', - 'expr' => 'in', - 'negate_expr' => 'notIn', - ], - '!in' => [ - 'label' => 'mautic.lead.list.form.operator.notin', - 'expr' => 'notIn', - 'negate_expr' => 'in', - ], - 'regexp' => [ - 'label' => 'mautic.lead.list.form.operator.regexp', - 'expr' => 'regexp', //special case - 'negate_expr' => 'notRegexp', - ], - '!regexp' => [ - 'label' => 'mautic.lead.list.form.operator.notregexp', - 'expr' => 'notRegexp', //special case - 'negate_expr' => 'regexp', - ], - 'date' => [ - 'label' => 'mautic.lead.list.form.operator.date', - 'expr' => 'date', //special case - 'negate_expr' => 'date', - 'hide' => true, - ], - 'startsWith' => [ - 'label' => 'mautic.core.operator.starts.with', - 'expr' => 'startsWith', - 'negate_expr' => 'startsWith', - ], - 'endsWith' => [ - 'label' => 'mautic.core.operator.ends.with', - 'expr' => 'endsWith', - 'negate_expr' => 'endsWith', - ], - 'contains' => [ - 'label' => 'mautic.core.operator.contains', - 'expr' => 'contains', - 'negate_expr' => 'contains', - ], - ]; - /** * @param null $operator * @@ -191,7 +85,9 @@ trait OperatorListTrait */ public function getFilterExpressionFunctions($operator = null) { - return (null === $operator) ? $this->operatorOptions : $this->operatorOptions[$operator]; + $operatorOption = OperatorOptions::getFilterExpressionFunctions(); + + return (null === $operator) ? $operatorOption : $operatorOption[$operator]; } /** diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 3386629e7f1..550631d0c11 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -29,6 +29,7 @@ use Mautic\LeadBundle\Event\ListPreProcessListEvent; use Mautic\LeadBundle\Helper\FormFieldHelper; use Mautic\LeadBundle\LeadEvents; +use Mautic\LeadBundle\Segment\LeadSegmentService; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -46,14 +47,20 @@ class ListModel extends FormModel */ protected $coreParametersHelper; + /** + * @var LeadSegmentService + */ + private $leadSegment; + /** * ListModel constructor. * * @param CoreParametersHelper $coreParametersHelper */ - public function __construct(CoreParametersHelper $coreParametersHelper) + public function __construct(CoreParametersHelper $coreParametersHelper, LeadSegmentService $leadSegment) { $this->coreParametersHelper = $coreParametersHelper; + $this->leadSegment = $leadSegment; } /** @@ -781,6 +788,9 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa new ListPreProcessListEvent($list, false) ); + // Get a count of leads to add + $newLeadsCount = $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); + dump($newLeadsCount); // Get a count of leads to add $newLeadsCount = $this->getLeadsByList( $list, @@ -791,7 +801,8 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa 'batchLimiters' => $batchLimiters, ] ); - + dump($newLeadsCount); + exit; // Ensure the same list is used each batch $batchLimiters['maxId'] = (int) $newLeadsCount[$id]['maxId']; @@ -1255,7 +1266,7 @@ public function removeLead($lead, $lists, $manuallyRemoved = false, $batchProces * * @return mixed */ - public function getLeadsByList($lists, $idOnly = false, $args = []) + public function getLeadsByList($lists, $idOnly = false, array $args = []) { $args['idOnly'] = $idOnly; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php new file mode 100644 index 00000000000..c0f69866e66 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -0,0 +1,152 @@ +glue = isset($filter['glue']) ? $filter['glue'] : null; + $this->field = isset($filter['field']) ? $filter['field'] : null; + $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; + $this->type = isset($filter['type']) ? $filter['type'] : null; + $this->filter = isset($filter['filter']) ? $filter['filter'] : null; + $this->display = isset($filter['display']) ? $filter['display'] : null; + $this->operator = isset($filter['operator']) ? $filter['operator'] : null; + } + + /** + * @return string|null + */ + public function getGlue() + { + return $this->glue; + } + + /** + * @return string|null + */ + public function getField() + { + return $this->field; + } + + /** + * @return string|null + */ + public function getObject() + { + return $this->object; + } + + /** + * @return bool + */ + public function isLeadType() + { + return $this->object === self::LEAD_OBJECT; + } + + /** + * @return bool + */ + public function isCompanyType() + { + return $this->object === self::COMPANY_OBJECT; + } + + /** + * @return string|null + */ + public function getType() + { + return $this->type; + } + + /** + * @return string|array|null + */ + public function getFilter() + { + return $this->filter; + } + + /** + * @return string|null + */ + public function getDisplay() + { + return $this->display; + } + + /** + * @return string|null + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @param string|null $operator + */ + public function setOperator($operator) + { + $this->operator = $operator; + } + + /** + * @param string|array|null $filter + */ + public function setFilter($filter) + { + $this->filter = $filter; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php new file mode 100644 index 00000000000..0ecb4e437fc --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -0,0 +1,27 @@ +getFilters(); + foreach ($filters as $filter) { + $leadSegmentFilter = new LeadSegmentFilter($filter); + $this->leadSegmentFilters[] = $leadSegmentFilter; + + if ($leadSegmentFilter->isCompanyType()) { + $this->hasCompanyFilter = true; + } + } + } + + /** + * Return the current element. + * + * @see http://php.net/manual/en/iterator.current.php + * + * @return LeadSegmentFilter + */ + public function current() + { + return $this->leadSegmentFilters[$this->position]; + } + + /** + * Move forward to next element. + * + * @see http://php.net/manual/en/iterator.next.php + */ + public function next() + { + ++$this->position; + } + + /** + * Return the key of the current element. + * + * @see http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Checks if current position is valid. + * + * @see http://php.net/manual/en/iterator.valid.php + * + * @return bool + */ + public function valid() + { + return isset($this->leadSegmentFilters[$this->position]); + } + + /** + * Rewind the Iterator to the first element. + * + * @see http://php.net/manual/en/iterator.rewind.php + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Count elements of an object. + * + * @see http://php.net/manual/en/countable.count.php + * + * @return int + */ + public function count() + { + return count($this->leadSegmentFilters); + } + + /** + * @return bool + */ + public function isHasCompanyFilter() + { + return $this->hasCompanyFilter; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php new file mode 100644 index 00000000000..d9fca67dfb0 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -0,0 +1,41 @@ +leadListSegmentRepository = $leadListSegmentRepository; + $this->leadSegmentFilterFactory = $leadSegmentFilterFactory; + } + + public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) + { + $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($entity); + + return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); + } +} diff --git a/app/bundles/LeadBundle/Segment/OperatorOptions.php b/app/bundles/LeadBundle/Segment/OperatorOptions.php new file mode 100644 index 00000000000..adb96e8da4b --- /dev/null +++ b/app/bundles/LeadBundle/Segment/OperatorOptions.php @@ -0,0 +1,133 @@ + [ + 'label' => 'mautic.lead.list.form.operator.equals', + 'expr' => 'eq', + 'negate_expr' => 'neq', + ], + '!=' => [ + 'label' => 'mautic.lead.list.form.operator.notequals', + 'expr' => 'neq', + 'negate_expr' => 'eq', + ], + 'gt' => [ + 'label' => 'mautic.lead.list.form.operator.greaterthan', + 'expr' => 'gt', + 'negate_expr' => 'lt', + ], + 'gte' => [ + 'label' => 'mautic.lead.list.form.operator.greaterthanequals', + 'expr' => 'gte', + 'negate_expr' => 'lt', + ], + 'lt' => [ + 'label' => 'mautic.lead.list.form.operator.lessthan', + 'expr' => 'lt', + 'negate_expr' => 'gt', + ], + 'lte' => [ + 'label' => 'mautic.lead.list.form.operator.lessthanequals', + 'expr' => 'lte', + 'negate_expr' => 'gt', + ], + 'empty' => [ + 'label' => 'mautic.lead.list.form.operator.isempty', + 'expr' => 'empty', //special case + 'negate_expr' => 'notEmpty', + ], + '!empty' => [ + 'label' => 'mautic.lead.list.form.operator.isnotempty', + 'expr' => 'notEmpty', //special case + 'negate_expr' => 'empty', + ], + 'like' => [ + 'label' => 'mautic.lead.list.form.operator.islike', + 'expr' => 'like', + 'negate_expr' => 'notLike', + ], + '!like' => [ + 'label' => 'mautic.lead.list.form.operator.isnotlike', + 'expr' => 'notLike', + 'negate_expr' => 'like', + ], + 'between' => [ + 'label' => 'mautic.lead.list.form.operator.between', + 'expr' => 'between', //special case + 'negate_expr' => 'notBetween', + // @todo implement in list UI + 'hide' => true, + ], + '!between' => [ + 'label' => 'mautic.lead.list.form.operator.notbetween', + 'expr' => 'notBetween', //special case + 'negate_expr' => 'between', + // @todo implement in list UI + 'hide' => true, + ], + 'in' => [ + 'label' => 'mautic.lead.list.form.operator.in', + 'expr' => 'in', + 'negate_expr' => 'notIn', + ], + '!in' => [ + 'label' => 'mautic.lead.list.form.operator.notin', + 'expr' => 'notIn', + 'negate_expr' => 'in', + ], + 'regexp' => [ + 'label' => 'mautic.lead.list.form.operator.regexp', + 'expr' => 'regexp', //special case + 'negate_expr' => 'notRegexp', + ], + '!regexp' => [ + 'label' => 'mautic.lead.list.form.operator.notregexp', + 'expr' => 'notRegexp', //special case + 'negate_expr' => 'regexp', + ], + 'date' => [ + 'label' => 'mautic.lead.list.form.operator.date', + 'expr' => 'date', //special case + 'negate_expr' => 'date', + 'hide' => true, + ], + 'startsWith' => [ + 'label' => 'mautic.core.operator.starts.with', + 'expr' => 'startsWith', + 'negate_expr' => 'startsWith', + ], + 'endsWith' => [ + 'label' => 'mautic.core.operator.ends.with', + 'expr' => 'endsWith', + 'negate_expr' => 'endsWith', + ], + 'contains' => [ + 'label' => 'mautic.core.operator.contains', + 'expr' => 'contains', + 'negate_expr' => 'contains', + ], + ]; + + public static function getFilterExpressionFunctions() + { + return self::$operatorOptions; + } + + public function getFilterExpressionFunctionsNonStatic() + { + return self::$operatorOptions; + } +} From 7e9988d98d9349691193b55f9b144fcf397f560f Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 3 Jan 2018 16:32:52 +0100 Subject: [PATCH 002/778] WIP - Segment refactoring - Move building operators and date to separate classes --- app/bundles/LeadBundle/Config/config.php | 20 +- .../Entity/LeadListSegmentRepository.php | 300 +----------------- .../LeadBundle/Segment/LeadSegmentFilter.php | 21 ++ .../Segment/LeadSegmentFilterDate.php | 268 ++++++++++++++++ .../Segment/LeadSegmentFilterFactory.php | 28 +- .../Segment/LeadSegmentFilterOperator.php | 60 ++++ .../LeadBundle/Segment/LeadSegmentFilters.php | 15 +- 7 files changed, 402 insertions(+), 310 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 9815cf90ef8..3dc1f5ae294 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -767,14 +767,30 @@ ], 'mautic.lead.model.lead_segment_filter_factory' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment_filter_date', + 'mautic.lead.model.lead_segment_filter_operator', + ], + ], + 'mautic.lead.model.lead_segment_filter_date' => [ + 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterDate::class, + 'arguments' => [ + 'translator', + ], + ], + 'mautic.lead.model.lead_segment_filter_operator' => [ + 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterOperator::class, + 'arguments' => [ + 'translator', + 'event_dispatcher', + 'mautic.lead.segment.operator_options', + ], ], 'mautic.lead.repository.lead_list_segment_repository' => [ 'class' => \Mautic\LeadBundle\Entity\LeadListSegmentRepository::class, 'arguments' => [ 'doctrine.orm.entity_manager', - 'mautic.lead.segment.operator_options', 'event_dispatcher', - 'translator', ], ], 'mautic.lead.segment.operator_options' => [ diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 87bb2512770..0fc1b5f5b01 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -12,22 +12,12 @@ namespace Mautic\LeadBundle\Entity; use Doctrine\DBAL\Query\QueryBuilder; -use Doctrine\DBAL\Types\DateType; -use Doctrine\DBAL\Types\FloatType; -use Doctrine\DBAL\Types\IntegerType; -use Doctrine\DBAL\Types\TimeType; use Doctrine\ORM\EntityManager; -use Mautic\CoreBundle\Doctrine\QueryFormatter\AbstractFormatter; -use Mautic\CoreBundle\Doctrine\Type\UTCDateTimeType; -use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\InputHelper; use Mautic\LeadBundle\Event\LeadListFilteringEvent; -use Mautic\LeadBundle\Event\LeadListFiltersOperatorsEvent; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\LeadSegmentFilters; -use Mautic\LeadBundle\Segment\OperatorOptions; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Translation\TranslatorInterface; class LeadListSegmentRepository { @@ -36,19 +26,11 @@ class LeadListSegmentRepository /** @var EntityManager */ private $entityManager; - /** - * @var OperatorOptions - */ - private $operatorOptions; - /** * @var EventDispatcherInterface */ private $dispatcher; - /** @var TranslatorInterface */ - private $translator; - /** * @var bool */ @@ -65,14 +47,10 @@ class LeadListSegmentRepository public function __construct( EntityManager $entityManager, - OperatorOptions $operatorOptions, - EventDispatcherInterface $dispatcher, - TranslatorInterface $translator + EventDispatcherInterface $dispatcher ) { - $this->entityManager = $entityManager; - $this->operatorOptions = $operatorOptions; - $this->dispatcher = $dispatcher; - $this->translator = $translator; + $this->entityManager = $entityManager; + $this->dispatcher = $dispatcher; } public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilters, array $batchLimiters) @@ -198,13 +176,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $leadTableSchema = $schema->listTableColumns(MAUTIC_TABLE_PREFIX.'leads'); $companyTableSchema = $schema->listTableColumns(MAUTIC_TABLE_PREFIX.'companies'); - $options = $this->operatorOptions->getFilterExpressionFunctionsNonStatic(); - - // Add custom filters operators - $event = new LeadListFiltersOperatorsEvent($options, $this->translator); - $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_OPERATORS_ON_GENERATE, $event); - $options = $event->getOperators(); - $groups = []; $groupExpr = $q->expr()->andX(); @@ -213,7 +184,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $column = false; $field = false; - $columnType = false; $filterField = $leadSegmentFilter->getField(); @@ -225,38 +195,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $field = "comp.{$filterField}"; } - $operatorDetails = $options[$leadSegmentFilter->getOperator()]; - $func = $operatorDetails['expr']; - - if ($column) { - // Format the field based on platform specific functions that DBAL doesn't support natively - $formatter = AbstractFormatter::createFormatter($this->entityManager->getConnection()); - $columnType = $column->getType(); - - switch ($leadSegmentFilter->getType()) { - case 'datetime': - if (!$columnType instanceof UTCDateTimeType) { - $field = $formatter->toDateTime($field); - } - break; - case 'date': - if (!$columnType instanceof DateType && !$columnType instanceof UTCDateTimeType) { - $field = $formatter->toDate($field); - } - break; - case 'time': - if (!$columnType instanceof TimeType && !$columnType instanceof UTCDateTimeType) { - $field = $formatter->toTime($field); - } - break; - case 'number': - if (!$columnType instanceof IntegerType && !$columnType instanceof FloatType) { - $field = $formatter->toNumeric($field); - } - break; - } - } - //the next one will determine the group if ($leadSegmentFilter->getGlue() === 'or') { // Create a new group of andX expressions @@ -270,200 +208,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $exprParameter = ":$parameter"; $ignoreAutoFilter = false; - // Special handling of relative date strings - if ($leadSegmentFilter->getType() === 'datetime' || $leadSegmentFilter->getType() === 'date') { - $relativeDateStrings = $this->getRelativeDateStrings(); - // Check if the column type is a date/time stamp - $isTimestamp = ($leadSegmentFilter->getType() === 'datetime' || $columnType instanceof UTCDateTimeType); - $getDate = function ($string) use ($isTimestamp, $relativeDateStrings, $leadSegmentFilter, &$func) { - $key = array_search($string, $relativeDateStrings, true); - $dtHelper = new DateTimeHelper('midnight today', null, 'local'); - $requiresBetween = in_array($func, ['eq', 'neq'], true) && $isTimestamp; - $timeframe = str_replace('mautic.lead.list.', '', $key); - $modifier = false; - $isRelative = true; - - switch ($timeframe) { - case 'birthday': - case 'anniversary': - $func = 'like'; - $isRelative = false; - $leadSegmentFilter->setOperator('like'); - $leadSegmentFilter->setFilter('%'.date('-m-d')); - break; - case 'today': - case 'tomorrow': - case 'yesterday': - if ($timeframe === 'yesterday') { - $dtHelper->modify('-1 day'); - } elseif ($timeframe === 'tomorrow') { - $dtHelper->modify('+1 day'); - } - - // Today = 2015-08-28 00:00:00 - if ($requiresBetween) { - // eq: - // field >= 2015-08-28 00:00:00 - // field < 2015-08-29 00:00:00 - - // neq: - // field < 2015-08-28 00:00:00 - // field >= 2015-08-29 00:00:00 - $modifier = '+1 day'; - } else { - // lt: - // field < 2015-08-28 00:00:00 - // gt: - // field > 2015-08-28 23:59:59 - - // lte: - // field <= 2015-08-28 23:59:59 - // gte: - // field >= 2015-08-28 00:00:00 - if (in_array($func, ['gt', 'lte'])) { - $modifier = '+1 day -1 second'; - } - } - break; - case 'week_last': - case 'week_next': - case 'week_this': - $interval = str_replace('week_', '', $timeframe); - $dtHelper->setDateTime('midnight monday '.$interval.' week', null); - - // This week: Monday 2015-08-24 00:00:00 - if ($requiresBetween) { - // eq: - // field >= Mon 2015-08-24 00:00:00 - // field < Mon 2015-08-31 00:00:00 - - // neq: - // field < Mon 2015-08-24 00:00:00 - // field >= Mon 2015-08-31 00:00:00 - $modifier = '+1 week'; - } else { - // lt: - // field < Mon 2015-08-24 00:00:00 - // gt: - // field > Sun 2015-08-30 23:59:59 - - // lte: - // field <= Sun 2015-08-30 23:59:59 - // gte: - // field >= Mon 2015-08-24 00:00:00 - if (in_array($func, ['gt', 'lte'])) { - $modifier = '+1 week -1 second'; - } - } - break; - - case 'month_last': - case 'month_next': - case 'month_this': - $interval = substr($key, -4); - $dtHelper->setDateTime('midnight first day of '.$interval.' month', null); - - // This month: 2015-08-01 00:00:00 - if ($requiresBetween) { - // eq: - // field >= 2015-08-01 00:00:00 - // field < 2015-09:01 00:00:00 - - // neq: - // field < 2015-08-01 00:00:00 - // field >= 2016-09-01 00:00:00 - $modifier = '+1 month'; - } else { - // lt: - // field < 2015-08-01 00:00:00 - // gt: - // field > 2015-08-31 23:59:59 - - // lte: - // field <= 2015-08-31 23:59:59 - // gte: - // field >= 2015-08-01 00:00:00 - if (in_array($func, ['gt', 'lte'])) { - $modifier = '+1 month -1 second'; - } - } - break; - case 'year_last': - case 'year_next': - case 'year_this': - $interval = substr($key, -4); - $dtHelper->setDateTime('midnight first day of '.$interval.' year', null); - - // This year: 2015-01-01 00:00:00 - if ($requiresBetween) { - // eq: - // field >= 2015-01-01 00:00:00 - // field < 2016-01-01 00:00:00 - - // neq: - // field < 2015-01-01 00:00:00 - // field >= 2016-01-01 00:00:00 - $modifier = '+1 year'; - } else { - // lt: - // field < 2015-01-01 00:00:00 - // gt: - // field > 2015-12-31 23:59:59 - - // lte: - // field <= 2015-12-31 23:59:59 - // gte: - // field >= 2015-01-01 00:00:00 - if (in_array($func, ['gt', 'lte'])) { - $modifier = '+1 year -1 second'; - } - } - break; - default: - $isRelative = false; - break; - } - - // check does this match php date params pattern? - if ($timeframe !== 'anniversary' && - (stristr($string[0], '-') || stristr($string[0], '+'))) { - $date = new \DateTime('now'); - $date->modify($string); - - $dateTime = $date->format('Y-m-d H:i:s'); - $dtHelper->setDateTime($dateTime, null); - - $isRelative = true; - } - - if ($isRelative) { - if ($requiresBetween) { - $startWith = ($isTimestamp) ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); - - $dtHelper->modify($modifier); - $endWith = ($isTimestamp) ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); - - // Use a between statement - $func = ($func == 'neq') ? 'notBetween' : 'between'; - $leadSegmentFilter->setFilter([$startWith, $endWith]); - } else { - if ($modifier) { - $dtHelper->modify($modifier); - } - - $details['filter'] = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); - } - } - }; - - if (is_array($leadSegmentFilter->getFilter())) { - foreach ($leadSegmentFilter->getFilter() as $filterValue) { - $getDate($filterValue); - } - } else { - $getDate($leadSegmentFilter->getFilter()); - } - } + $func = $leadSegmentFilter->getFunc(); // Generate a unique alias $alias = $this->generateRandomParameterName(); @@ -1355,43 +1100,6 @@ public function generateFilterExpression($q, $column, $operator, $parameter, $in return $expr; } - /** - * @return array - */ - private function getRelativeDateStrings() - { - $keys = self::getRelativeDateTranslationKeys(); - - $strings = []; - foreach ($keys as $key) { - $strings[$key] = $this->translator->trans($key); - } - - return $strings; - } - - /** - * @return array - */ - private static function getRelativeDateTranslationKeys() - { - return [ - 'mautic.lead.list.month_last', - 'mautic.lead.list.month_next', - 'mautic.lead.list.month_this', - 'mautic.lead.list.today', - 'mautic.lead.list.tomorrow', - 'mautic.lead.list.yesterday', - 'mautic.lead.list.week_last', - 'mautic.lead.list.week_next', - 'mautic.lead.list.week_this', - 'mautic.lead.list.year_last', - 'mautic.lead.list.year_next', - 'mautic.lead.list.year_this', - 'mautic.lead.list.anniversary', - ]; - } - /** * @param $table * @param $alias diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index c0f69866e66..73241cab137 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -51,6 +51,11 @@ class LeadSegmentFilter */ private $operator; + /** + * @var string + */ + private $func; + public function __construct(array $filter) { $this->glue = isset($filter['glue']) ? $filter['glue'] : null; @@ -149,4 +154,20 @@ public function setFilter($filter) { $this->filter = $filter; } + + /** + * @return string + */ + public function getFunc() + { + return $this->func; + } + + /** + * @param string $func + */ + public function setFunc($func) + { + $this->func = $func; + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php new file mode 100644 index 00000000000..2dfbd11af1e --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php @@ -0,0 +1,268 @@ +translator = $translator; + } + + public function fixDateOptions(LeadSegmentFilter $leadSegmentFilter) + { + $type = $leadSegmentFilter->getType(); + if ($type !== 'datetime' && $type !== 'date') { + return; + } + + if (is_array($leadSegmentFilter->getFilter())) { + foreach ($leadSegmentFilter->getFilter() as $filterValue) { + $this->getDate($filterValue, $leadSegmentFilter); + } + } else { + $this->getDate($leadSegmentFilter->getFilter(), $leadSegmentFilter); + } + } + + private function getDate($string, LeadSegmentFilter $leadSegmentFilter) + { + $relativeDateStrings = $this->getRelativeDateStrings(); + + // Check if the column type is a date/time stamp + $isTimestamp = $leadSegmentFilter->getType() === 'datetime'; + + $key = array_search($string, $relativeDateStrings, true); + $dtHelper = new DateTimeHelper('midnight today', null, 'local'); + $requiresBetween = in_array($leadSegmentFilter->getFunc(), ['eq', 'neq'], true) && $isTimestamp; + $timeframe = str_replace('mautic.lead.list.', '', $key); + $modifier = false; + $isRelative = true; + + switch ($timeframe) { + case 'birthday': + case 'anniversary': + $isRelative = false; + $leadSegmentFilter->setOperator('like'); + $leadSegmentFilter->setFilter('%'.date('-m-d')); + break; + case 'today': + case 'tomorrow': + case 'yesterday': + if ($timeframe === 'yesterday') { + $dtHelper->modify('-1 day'); + } elseif ($timeframe === 'tomorrow') { + $dtHelper->modify('+1 day'); + } + + // Today = 2015-08-28 00:00:00 + if ($requiresBetween) { + // eq: + // field >= 2015-08-28 00:00:00 + // field < 2015-08-29 00:00:00 + + // neq: + // field < 2015-08-28 00:00:00 + // field >= 2015-08-29 00:00:00 + $modifier = '+1 day'; + } else { + // lt: + // field < 2015-08-28 00:00:00 + // gt: + // field > 2015-08-28 23:59:59 + + // lte: + // field <= 2015-08-28 23:59:59 + // gte: + // field >= 2015-08-28 00:00:00 + if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { + $modifier = '+1 day -1 second'; + } + } + break; + case 'week_last': + case 'week_next': + case 'week_this': + $interval = str_replace('week_', '', $timeframe); + $dtHelper->setDateTime('midnight monday '.$interval.' week', null); + + // This week: Monday 2015-08-24 00:00:00 + if ($requiresBetween) { + // eq: + // field >= Mon 2015-08-24 00:00:00 + // field < Mon 2015-08-31 00:00:00 + + // neq: + // field < Mon 2015-08-24 00:00:00 + // field >= Mon 2015-08-31 00:00:00 + $modifier = '+1 week'; + } else { + // lt: + // field < Mon 2015-08-24 00:00:00 + // gt: + // field > Sun 2015-08-30 23:59:59 + + // lte: + // field <= Sun 2015-08-30 23:59:59 + // gte: + // field >= Mon 2015-08-24 00:00:00 + if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { + $modifier = '+1 week -1 second'; + } + } + break; + + case 'month_last': + case 'month_next': + case 'month_this': + $interval = substr($key, -4); + $dtHelper->setDateTime('midnight first day of '.$interval.' month', null); + + // This month: 2015-08-01 00:00:00 + if ($requiresBetween) { + // eq: + // field >= 2015-08-01 00:00:00 + // field < 2015-09:01 00:00:00 + + // neq: + // field < 2015-08-01 00:00:00 + // field >= 2016-09-01 00:00:00 + $modifier = '+1 month'; + } else { + // lt: + // field < 2015-08-01 00:00:00 + // gt: + // field > 2015-08-31 23:59:59 + + // lte: + // field <= 2015-08-31 23:59:59 + // gte: + // field >= 2015-08-01 00:00:00 + if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { + $modifier = '+1 month -1 second'; + } + } + break; + case 'year_last': + case 'year_next': + case 'year_this': + $interval = substr($key, -4); + $dtHelper->setDateTime('midnight first day of '.$interval.' year', null); + + // This year: 2015-01-01 00:00:00 + if ($requiresBetween) { + // eq: + // field >= 2015-01-01 00:00:00 + // field < 2016-01-01 00:00:00 + + // neq: + // field < 2015-01-01 00:00:00 + // field >= 2016-01-01 00:00:00 + $modifier = '+1 year'; + } else { + // lt: + // field < 2015-01-01 00:00:00 + // gt: + // field > 2015-12-31 23:59:59 + + // lte: + // field <= 2015-12-31 23:59:59 + // gte: + // field >= 2015-01-01 00:00:00 + if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { + $modifier = '+1 year -1 second'; + } + } + break; + default: + $isRelative = false; + break; + } + + // check does this match php date params pattern? + if ($timeframe !== 'anniversary' && + (stristr($string[0], '-') || stristr($string[0], '+'))) { + $date = new \DateTime('now'); + $date->modify($string); + + $dateTime = $date->format('Y-m-d H:i:s'); + $dtHelper->setDateTime($dateTime, null); + + $isRelative = true; + } + + if ($isRelative) { + if ($requiresBetween) { + $startWith = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); + + $dtHelper->modify($modifier); + $endWith = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); + + // Use a between statement + $func = ($leadSegmentFilter->getFunc() === 'neq') ? 'notBetween' : 'between'; + $leadSegmentFilter->setFunc($func); + + $leadSegmentFilter->setFilter([$startWith, $endWith]); + } else { + if ($modifier) { + $dtHelper->modify($modifier); + } + + $filter = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); + $leadSegmentFilter->setFilter($filter); + } + } + } + + /** + * @return array + */ + private function getRelativeDateStrings() + { + $keys = self::getRelativeDateTranslationKeys(); + + $strings = []; + foreach ($keys as $key) { + $strings[$key] = $this->translator->trans($key); + } + + return $strings; + } + + /** + * @return array + */ + private static function getRelativeDateTranslationKeys() + { + return [ + 'mautic.lead.list.month_last', + 'mautic.lead.list.month_next', + 'mautic.lead.list.month_this', + 'mautic.lead.list.today', + 'mautic.lead.list.tomorrow', + 'mautic.lead.list.yesterday', + 'mautic.lead.list.week_last', + 'mautic.lead.list.week_next', + 'mautic.lead.list.week_this', + 'mautic.lead.list.year_last', + 'mautic.lead.list.year_next', + 'mautic.lead.list.year_this', + 'mautic.lead.list.anniversary', + ]; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 0ecb4e437fc..7f87d41867c 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -15,6 +15,22 @@ class LeadSegmentFilterFactory { + /** + * @var LeadSegmentFilterDate + */ + private $leadSegmentFilterDate; + + /** + * @var LeadSegmentFilterOperator + */ + private $leadSegmentFilterOperator; + + public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSegmentFilterOperator $leadSegmentFilterOperator) + { + $this->leadSegmentFilterDate = $leadSegmentFilterDate; + $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; + } + /** * @param LeadList $leadList * @@ -22,6 +38,16 @@ class LeadSegmentFilterFactory */ public function getLeadListFilters(LeadList $leadList) { - return new LeadSegmentFilters($leadList); + $leadSegmentFilters = new LeadSegmentFilters(); + + $filters = $leadList->getFilters(); + foreach ($filters as $filter) { + $leadSegmentFilter = new LeadSegmentFilter($filter); + $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilter); + $this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); + $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); + } + + return $leadSegmentFilters; } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php new file mode 100644 index 00000000000..751c9db2578 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php @@ -0,0 +1,60 @@ +translator = $translator; + $this->dispatcher = $dispatcher; + $this->operatorOptions = $operatorOptions; + } + + public function fixOperator(LeadSegmentFilter $leadSegmentFilter) + { + $options = $this->operatorOptions->getFilterExpressionFunctionsNonStatic(); + + // Add custom filters operators + $event = new LeadListFiltersOperatorsEvent($options, $this->translator); + $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_OPERATORS_ON_GENERATE, $event); + $options = $event->getOperators(); + + $operatorDetails = $options[$leadSegmentFilter->getOperator()]; + $func = $operatorDetails['expr']; + + $leadSegmentFilter->setFunc($func); + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php index 23a0325117a..d001f42cd5e 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php @@ -11,8 +11,6 @@ namespace Mautic\LeadBundle\Segment; -use Mautic\LeadBundle\Entity\LeadList; - class LeadSegmentFilters implements \Iterator, \Countable { /** @@ -30,16 +28,11 @@ class LeadSegmentFilters implements \Iterator, \Countable */ private $leadSegmentFilters = []; - public function __construct(LeadList $leadList) + public function addLeadSegmentFilter(LeadSegmentFilter $leadSegmentFilter) { - $filters = $leadList->getFilters(); - foreach ($filters as $filter) { - $leadSegmentFilter = new LeadSegmentFilter($filter); - $this->leadSegmentFilters[] = $leadSegmentFilter; - - if ($leadSegmentFilter->isCompanyType()) { - $this->hasCompanyFilter = true; - } + $this->leadSegmentFilters[] = $leadSegmentFilter; + if ($leadSegmentFilter->isCompanyType()) { + $this->hasCompanyFilter = true; } } From 602da617a97a8e0170022e5fb5476a2190a6357b Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 3 Jan 2018 16:38:21 +0100 Subject: [PATCH 003/778] WIP - Segment refactoring - Move relative dates to separate class --- app/bundles/LeadBundle/Config/config.php | 8 ++- .../Segment/LeadSegmentFilterDate.php | 50 +++------------ .../LeadBundle/Segment/RelativeDate.php | 62 +++++++++++++++++++ 3 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/RelativeDate.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 3dc1f5ae294..fe6e0b8e929 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -772,10 +772,16 @@ 'mautic.lead.model.lead_segment_filter_operator', ], ], + 'mautic.lead.model.relative_date' => [ + 'class' => \Mautic\LeadBundle\Segment\RelativeDate::class, + 'arguments' => [ + 'translator', + ], + ], 'mautic.lead.model.lead_segment_filter_date' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterDate::class, 'arguments' => [ - 'translator', + 'mautic.lead.model.relative_date', ], ], 'mautic.lead.model.lead_segment_filter_operator' => [ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php index 2dfbd11af1e..61eb335c121 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php @@ -12,16 +12,17 @@ namespace Mautic\LeadBundle\Segment; use Mautic\CoreBundle\Helper\DateTimeHelper; -use Symfony\Component\Translation\TranslatorInterface; class LeadSegmentFilterDate { - /** @var TranslatorInterface */ - private $translator; + /** + * @var RelativeDate + */ + private $relativeDate; - public function __construct(TranslatorInterface $translator) + public function __construct(RelativeDate $relativeDate) { - $this->translator = $translator; + $this->relativeDate = $relativeDate; } public function fixDateOptions(LeadSegmentFilter $leadSegmentFilter) @@ -42,7 +43,7 @@ public function fixDateOptions(LeadSegmentFilter $leadSegmentFilter) private function getDate($string, LeadSegmentFilter $leadSegmentFilter) { - $relativeDateStrings = $this->getRelativeDateStrings(); + $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); // Check if the column type is a date/time stamp $isTimestamp = $leadSegmentFilter->getType() === 'datetime'; @@ -228,41 +229,4 @@ private function getDate($string, LeadSegmentFilter $leadSegmentFilter) } } } - - /** - * @return array - */ - private function getRelativeDateStrings() - { - $keys = self::getRelativeDateTranslationKeys(); - - $strings = []; - foreach ($keys as $key) { - $strings[$key] = $this->translator->trans($key); - } - - return $strings; - } - - /** - * @return array - */ - private static function getRelativeDateTranslationKeys() - { - return [ - 'mautic.lead.list.month_last', - 'mautic.lead.list.month_next', - 'mautic.lead.list.month_this', - 'mautic.lead.list.today', - 'mautic.lead.list.tomorrow', - 'mautic.lead.list.yesterday', - 'mautic.lead.list.week_last', - 'mautic.lead.list.week_next', - 'mautic.lead.list.week_this', - 'mautic.lead.list.year_last', - 'mautic.lead.list.year_next', - 'mautic.lead.list.year_this', - 'mautic.lead.list.anniversary', - ]; - } } diff --git a/app/bundles/LeadBundle/Segment/RelativeDate.php b/app/bundles/LeadBundle/Segment/RelativeDate.php new file mode 100644 index 00000000000..c25c6cea9b3 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/RelativeDate.php @@ -0,0 +1,62 @@ +translator = $translator; + } + + /** + * @return array + */ + public function getRelativeDateStrings() + { + $keys = $this->getRelativeDateTranslationKeys(); + + $strings = []; + foreach ($keys as $key) { + $strings[$key] = $this->translator->trans($key); + } + + return $strings; + } + + /** + * @return array + */ + private function getRelativeDateTranslationKeys() + { + return [ + 'mautic.lead.list.month_last', + 'mautic.lead.list.month_next', + 'mautic.lead.list.month_this', + 'mautic.lead.list.today', + 'mautic.lead.list.tomorrow', + 'mautic.lead.list.yesterday', + 'mautic.lead.list.week_last', + 'mautic.lead.list.week_next', + 'mautic.lead.list.week_this', + 'mautic.lead.list.year_last', + 'mautic.lead.list.year_next', + 'mautic.lead.list.year_this', + 'mautic.lead.list.anniversary', + ]; + } +} From 45ad6828592a56edce30477736e9a31f8e1e86e7 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 3 Jan 2018 16:57:12 +0100 Subject: [PATCH 004/778] Move generating parameters name to separate class --- app/bundles/LeadBundle/Config/config.php | 4 ++ .../Entity/LeadListSegmentRepository.php | 33 +++++--------- .../Segment/RandomParameterName.php | 44 +++++++++++++++++++ 3 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/RandomParameterName.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index fe6e0b8e929..5522baddcfc 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -792,11 +792,15 @@ 'mautic.lead.segment.operator_options', ], ], + 'mautic.lead.model.random_parameter_name' => [ + 'class' => \Mautic\LeadBundle\Segment\RandomParameterName::class, + ], 'mautic.lead.repository.lead_list_segment_repository' => [ 'class' => \Mautic\LeadBundle\Entity\LeadListSegmentRepository::class, 'arguments' => [ 'doctrine.orm.entity_manager', 'event_dispatcher', + 'mautic.lead.model.random_parameter_name', ], ], 'mautic.lead.segment.operator_options' => [ diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 0fc1b5f5b01..8bfafaf5357 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -17,6 +17,7 @@ use Mautic\LeadBundle\Event\LeadListFilteringEvent; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\LeadSegmentFilters; +use Mautic\LeadBundle\Segment\RandomParameterName; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class LeadListSegmentRepository @@ -32,25 +33,23 @@ class LeadListSegmentRepository private $dispatcher; /** - * @var bool + * @var RandomParameterName */ - private $listFiltersInnerJoinCompany = false; + private $randomParameterName; /** - * Contains randomly generated parameter names generated by - * `generateRandomParameterName()`. This eliminates chances - * for parameter name collision. - * - * @var array + * @var bool */ - private $usedParameterNames = []; + private $listFiltersInnerJoinCompany = false; public function __construct( EntityManager $entityManager, - EventDispatcherInterface $dispatcher + EventDispatcherInterface $dispatcher, + RandomParameterName $randomParameterName ) { - $this->entityManager = $entityManager; - $this->dispatcher = $dispatcher; + $this->entityManager = $entityManager; + $this->dispatcher = $dispatcher; + $this->randomParameterName = $randomParameterName; } public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilters, array $batchLimiters) @@ -1054,17 +1053,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query */ private function generateRandomParameterName() { - $alpha_numeric = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - $paramName = substr(str_shuffle($alpha_numeric), 0, 8); - - if (!in_array($paramName, $this->usedParameterNames, true)) { - $this->usedParameterNames[] = $paramName; - - return $paramName; - } - - return $this->generateRandomParameterName(); + return $this->randomParameterName->generateRandomParameterName(); } /** diff --git a/app/bundles/LeadBundle/Segment/RandomParameterName.php b/app/bundles/LeadBundle/Segment/RandomParameterName.php new file mode 100644 index 00000000000..2ca68a02d94 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/RandomParameterName.php @@ -0,0 +1,44 @@ +usedParameterNames, true)) { + $this->usedParameterNames[] = $paramName; + + return $paramName; + } + + return $this->generateRandomParameterName(); + } +} From 0cc6b5110da6b94c5e5778e2d2955dd74c7fa3ac Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 4 Jan 2018 15:36:06 +0100 Subject: [PATCH 005/778] WIP - Segment refactoring - Filter cleanup moved to proper class --- .../Entity/LeadListSegmentRepository.php | 12 ------ .../LeadBundle/Segment/LeadSegmentFilter.php | 38 +++++++++++++++++-- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 8bfafaf5357..4fb7894dfda 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -991,18 +991,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query } if (!$ignoreAutoFilter) { - if (!is_array($leadSegmentFilter->getFilter())) { - switch ($leadSegmentFilter->getType()) { - case 'number': - $leadSegmentFilter->setFilter((float) $leadSegmentFilter->getFilter()); - break; - - case 'boolean': - $leadSegmentFilter->setFilter((bool) $leadSegmentFilter->getFilter()); - break; - } - } - $parameters[$parameter] = $leadSegmentFilter->getFilter(); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 73241cab137..57bf88e1b79 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -37,7 +37,7 @@ class LeadSegmentFilter private $type; /** - * @var string|array|null + * @var string|array|bool|float|null */ private $filter; @@ -62,9 +62,13 @@ public function __construct(array $filter) $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->filter = isset($filter['filter']) ? $filter['filter'] : null; $this->display = isset($filter['display']) ? $filter['display'] : null; - $this->operator = isset($filter['operator']) ? $filter['operator'] : null; + + $operatorValue = isset($filter['operator']) ? $filter['operator'] : null; + $this->setOperator($operatorValue); + + $filterValue = isset($filter['filter']) ? $filter['filter'] : null; + $this->setFilter($filterValue); } /** @@ -148,10 +152,12 @@ public function setOperator($operator) } /** - * @param string|array|null $filter + * @param string|array|bool|float|null $filter */ public function setFilter($filter) { + $filter = $this->sanitizeFilter($filter); + $this->filter = $filter; } @@ -170,4 +176,28 @@ public function setFunc($func) { $this->func = $func; } + + /** + * @param string|array|bool|float|null $filter + * + * @return string|array|bool|float|null + */ + private function sanitizeFilter($filter) + { + if ($filter === null || is_array($filter) || !$this->getType()) { + return $filter; + } + + switch ($this->getType()) { + case 'number': + $filter = (float) $filter; + break; + + case 'boolean': + $filter = (bool) $filter; + break; + } + + return $filter; + } } From 5afb7f2d68389cc8790a155a519240eee2754b57 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 4 Jan 2018 15:45:58 +0100 Subject: [PATCH 006/778] WIP - Segment refactoring - Removed BC for LeadListFilteringEvent (pass an array) --- .../Entity/LeadListSegmentRepository.php | 2 +- .../LeadBundle/Segment/LeadSegmentFilter.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 4fb7894dfda..8dceb4faa0a 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -995,7 +995,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query } if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { - $event = new LeadListFilteringEvent($leadSegmentFilter, null, $alias, $func, $q, $this->entityManager); + $event = new LeadListFilteringEvent($leadSegmentFilter->toArray(), null, $alias, $func, $q, $this->entityManager); $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); if ($event->isFilteringDone()) { $groupExpr->add($event->getSubQuery()); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 57bf88e1b79..76de02ea6a9 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -177,6 +177,23 @@ public function setFunc($func) $this->func = $func; } + /** + * @return array + */ + public function toArray() + { + return [ + 'glue' => $this->getGlue(), + 'field' => $this->getField(), + 'object' => $this->getObject(), + 'type' => $this->getType(), + 'filter' => $this->getFilter(), + 'display' => $this->getDisplay(), + 'operator' => $this->getOperator(), + 'func' => $this->getFunc(), + ]; + } + /** * @param string|array|bool|float|null $filter * From bac5cada3225018817419a4e77d5569e9c078ff5 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 4 Jan 2018 15:57:35 +0100 Subject: [PATCH 007/778] WIP - Segment refactoring - Move company join type to proper class --- .../Entity/LeadListSegmentRepository.php | 21 +++++-------------- .../LeadBundle/Segment/LeadSegmentFilters.php | 21 +++++++++++++++++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 8dceb4faa0a..63f07326a40 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -37,11 +37,6 @@ class LeadListSegmentRepository */ private $randomParameterName; - /** - * @var bool - */ - private $listFiltersInnerJoinCompany = false; - public function __construct( EntityManager $entityManager, EventDispatcherInterface $dispatcher, @@ -153,7 +148,7 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); if ($leadSegmentFilters->isHasCompanyFilter()) { - $this->applyCompanyFieldFilters($q); + $this->applyCompanyFieldFilters($q, $leadSegmentFilters); } return $expr; @@ -855,13 +850,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query continue; } - if ('company' === $object) { - // Must tell getLeadsByList how to best handle the relationship with the companies table - if (!in_array($func, ['empty', 'neq', 'notIn', 'notLike'], true)) { - $this->listFiltersInnerJoinCompany = true; - } - } - switch ($func) { case 'between': case 'notBetween': @@ -1134,11 +1122,12 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. * - * @param QueryBuilder $q + * @param QueryBuilder $q + * @param LeadSegmentFilters $leadSegmentFilters */ - private function applyCompanyFieldFilters(QueryBuilder $q) + private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) { - $joinType = $this->listFiltersInnerJoinCompany ? 'join' : 'leftJoin'; + $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; // Join company tables for query optimization $q->$joinType('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'l.id = cl.lead_id') ->$joinType( diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php index d001f42cd5e..9d7aa217be3 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php @@ -18,21 +18,30 @@ class LeadSegmentFilters implements \Iterator, \Countable */ private $position = 0; + /** + * @var array|LeadSegmentFilter[] + */ + private $leadSegmentFilters = []; + /** * @var bool */ private $hasCompanyFilter = false; /** - * @var array|LeadSegmentFilter[] + * @var bool */ - private $leadSegmentFilters = []; + private $listFiltersInnerJoinCompany = false; public function addLeadSegmentFilter(LeadSegmentFilter $leadSegmentFilter) { $this->leadSegmentFilters[] = $leadSegmentFilter; if ($leadSegmentFilter->isCompanyType()) { $this->hasCompanyFilter = true; + // Must tell getLeadsByList how to best handle the relationship with the companies table + if (!in_array($leadSegmentFilter->getFunc(), ['empty', 'neq', 'notIn', 'notLike'], true)) { + $this->listFiltersInnerJoinCompany = true; + } } } @@ -111,4 +120,12 @@ public function isHasCompanyFilter() { return $this->hasCompanyFilter; } + + /** + * @return bool + */ + public function isListFiltersInnerJoinCompany() + { + return $this->listFiltersInnerJoinCompany; + } } From 81998caaa8f628a1dccd9c7c517cb40b802bc179 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 5 Jan 2018 14:15:31 +0100 Subject: [PATCH 008/778] started refactoring the query building process --- app/bundles/LeadBundle/Config/config.php | 8 + .../Entity/LeadListSegmentRepository.php | 34 +- app/bundles/LeadBundle/Model/ListModel.php | 1 + .../LeadBundle/Segment/LeadSegmentService.php | 23 +- .../Services/LeadSegmentQueryBuilder.php | 1397 +++++++++++++++++ app/bundles/LeadBundle/Services/test.sql | 1 + 6 files changed, 1461 insertions(+), 3 deletions(-) create mode 100644 app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php create mode 100644 app/bundles/LeadBundle/Services/test.sql diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 5522baddcfc..b312df6b054 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -758,11 +758,19 @@ 'mautic.lead.model.lead_segment_service', ], ], + 'mautic.lead.repository.lead_segment_query_builder' => [ + 'class' => \Mautic\LeadBundle\Services\LeadSegmentQueryBuilder::class, + 'arguments' => [ + 'doctrine.orm.entity_manager', + 'mautic.lead.model.random_parameter_name', + ], + ], 'mautic.lead.model.lead_segment_service' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentService::class, 'arguments' => [ 'mautic.lead.model.lead_segment_filter_factory', 'mautic.lead.repository.lead_list_segment_repository', + 'mautic.lead.repository.lead_segment_query_builder' ], ], 'mautic.lead.model.lead_segment_filter_factory' => [ diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 63f07326a40..2a2a7272af7 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -83,6 +83,10 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte $expr = $this->generateSegmentExpression($leadSegmentFilters, $q, $id); + echo 'SQL parameters:'; + dump($q->getParameters()); + + // Leads that do not have any record in the lead_lists_leads table for this lead list // For non null fields - it's apparently better to use left join over not exists due to not using nullable // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ @@ -145,6 +149,7 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) { + var_dump(debug_backtrace()[1]['function']); $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); if ($leadSegmentFilters->isHasCompanyFilter()) { @@ -163,6 +168,7 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter */ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId) { + var_dump(debug_backtrace()[1]['function']); $parameters = []; $schema = $this->entityManager->getConnection()->getSchemaManager(); @@ -181,6 +187,10 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $filterField = $leadSegmentFilter->getField(); + if($filterField=='lead_email_read_date') { + + } + if ($leadSegmentFilter->isLeadType()) { $column = isset($leadTableSchema[$filterField]) ? $leadTableSchema[$filterField] : false; $field = "l.{$filterField}"; @@ -207,6 +217,10 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query // Generate a unique alias $alias = $this->generateRandomParameterName(); +// var_dump($func.":".$leadSegmentFilter->getField()); +// var_dump($exprParameter); + + switch ($leadSegmentFilter->getField()) { case 'hit_url': case 'referer': @@ -347,11 +361,18 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $table = 'email_stats'; } + + if($filterField=='lead_email_read_date') { + var_dump($func); + } + $subqb = $this->entityManager->getConnection() ->createQueryBuilder() ->select('id') ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + + switch ($func) { case 'eq': case 'neq': @@ -398,7 +419,12 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query } break; default: - $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $parameter2 = $this->generateRandomParameterName(); + + if($filterField=='lead_email_read_date') { + var_dump($exprParameter); + } + $parameters[$parameter2] = $leadSegmentFilter->getFilter(); $subqb->where( $q->expr() @@ -406,7 +432,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $q->expr() ->$func( $alias.'.'.$column, - $exprParameter + $parameter2 ), $q->expr() ->eq($alias.'.lead_id', 'l.id') @@ -974,7 +1000,9 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query ); break; default: + $ignoreAutoFilter = true; $groupExpr->add($q->expr()->$func($field, $exprParameter)); + $parameters[$exprParameter] = $leadSegmentFilter->getFilter(); } } @@ -1019,6 +1047,8 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $q->setParameter($k, $v, $paramType); } + var_dump($parameters); + return $expr; } diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 550631d0c11..342b3934b6c 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -30,6 +30,7 @@ use Mautic\LeadBundle\Helper\FormFieldHelper; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\LeadSegmentService; +use Mautic\LeadBundle\Services\LeadSegmentQueryBuilder; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index d9fca67dfb0..0214ca15af9 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -11,8 +11,10 @@ namespace Mautic\LeadBundle\Segment; +use Doctrine\DBAL\Query\QueryBuilder; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListSegmentRepository; +use Mautic\LeadBundle\Services\LeadSegmentQueryBuilder; class LeadSegmentService { @@ -26,16 +28,35 @@ class LeadSegmentService */ private $leadSegmentFilterFactory; - public function __construct(LeadSegmentFilterFactory $leadSegmentFilterFactory, LeadListSegmentRepository $leadListSegmentRepository) + /** + * @var LeadSegmentQueryBuilder + */ + private $queryBuilder; + + public function __construct( + LeadSegmentFilterFactory $leadSegmentFilterFactory, + LeadListSegmentRepository $leadListSegmentRepository, + LeadSegmentQueryBuilder $queryBuilder) { $this->leadListSegmentRepository = $leadListSegmentRepository; $this->leadSegmentFilterFactory = $leadSegmentFilterFactory; + $this->queryBuilder = $queryBuilder; } public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) { $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($entity); + /** @var QueryBuilder $qb */ + $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); + var_dump($sql = $qb->getSQL()); + $parameters = $qb->getParameters(); + foreach($parameters as $parameter=>$value) { + $sql = str_replace(':' . $parameter, $value, $sql); + } + var_dump($sql); +// die(); + return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php new file mode 100644 index 00000000000..7c79ce1b3a8 --- /dev/null +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -0,0 +1,1397 @@ +entityManager = $entityManager; + $this->randomParameterName = $randomParameterName; + + //@todo Will be generate automatically, just as POC + $this->tableAliases['leads'] = 'l'; + $this->tableAliases['email_stats'] = 'es'; + $this->tableAliases['page_hits'] = 'ph'; + + $this->translator['lead_email_read_count'] = [ + 'foreign_table' => 'email_stats', + 'foreign_table_field' => 'lead_id', + 'table' => 'leads', + 'table_field' => 'id', + 'func' => 'sum', + 'field' => 'open_count' + ]; + + $this->translator['lead_email_read_date'] = [ + 'foreign_table' => 'page_hits', + 'foreign_table_field' => 'lead_id', + 'table' => 'leads', + 'table_field' => 'id', + 'field' => 'date_hit' + ]; + } + +//object(Mautic\LeadBundle\Segment\LeadSegmentFilter)[841] +//private 'glue' => string 'and' (length=3) +//private 'field' => string 'lead_email_read_date' (length=20) +//private 'object' => string 'lead' (length=4) +//private 'type' => string 'datetime' (length=8) +//private 'filter' => string '2017-09-10 23:14' (length=16) +//private 'display' => null +//private 'operator' => string 'gt' (length=2) +//private 'func' => string 'gt' (length=2) + + public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) + { + /** @var QueryBuilder $qb */ + $qb = $this->entityManager->getConnection() + ->createQueryBuilder() + ; + + $qb->select('*') + ->from('MauticLeadBundle:Lead', 'l') + ; + + foreach ($leadSegmentFilters as $filter) { + $qb = $this->getQueryPart($filter, $qb); + } + + + return $qb; + echo 'SQL parameters:'; + dump($q->getParameters()); + + + // Leads that do not have any record in the lead_lists_leads table for this lead list + // For non null fields - it's apparently better to use left join over not exists due to not using nullable + // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ + $listOnExpr = $q->expr() + ->andX( + $q->expr() + ->eq('ll.leadlist_id', $id), + $q->expr() + ->eq('ll.lead_id', 'l.id') + ) + ; + + if (!empty($batchLimiters['dateTime'])) { + // Only leads in the list at the time of count + $listOnExpr->add( + $q->expr() + ->lte('ll.date_added', $q->expr() + ->literal($batchLimiters['dateTime'])) + ); + } + + $q->leftJoin( + 'l', + MAUTIC_TABLE_PREFIX . 'lead_lists_leads', + 'll', + $listOnExpr + ); + + $expr->add($q->expr() + ->isNull('ll.lead_id')); + + if ($batchExpr->count()) { + $expr->add($batchExpr); + } + + if ($expr->count()) { + $q->andWhere($expr); + } + + if (!empty($limit)) { + $q->setFirstResult($start) + ->setMaxResults($limit) + ; + } + + // remove any possible group by + $q->resetQueryPart('groupBy'); + + dump($q->getSQL()); + echo 'SQL parameters:'; + dump($q->getParameters()); + + $results = $q->execute() + ->fetchAll() + ; + + $leads = []; + foreach ($results as $r) { + $leads = [ + 'count' => $r['lead_count'], + 'maxId' => $r['max_id'], + ]; + if ($withMinId) { + $leads['minId'] = $r['min_id']; + } + } + + return $leads; + } + + private function getFilterOperator(LeadSegmentFilter $filter, $translated = false) + { + switch ($filter->getOperator()) { + case 'gt': + return '>'; + case 'eq': + return '='; + case 'gt': + return '>'; + case 'gte': + return '>='; + case 'lt': + return '<'; + case 'lte': + return '<='; + } + throw new \Exception(sprintf('Unknown operator \'%s\'.', $filter->getOperator())); + } + + private function getFilterValue(LeadSegmentFilter $filter, $parameterHolder, Column $dbColumn = null) + { + switch ($filter->getType()) { + case 'number': + return ":" . $parameterHolder; + case 'datetime': + return sprintf('":%s"', $parameterHolder); + default: + var_dump($dbColumn->getType()); + die(); + } + throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getType())); + } + + private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) + { + $parameters = []; + + //@todo cache metadata + $schema = $this->entityManager->getConnection() + ->getSchemaManager() + ; + $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) + ->getTableName() + ; + + + /** @var Column $dbColumn */ + $dbColumn = isset($schema->listTableColumns($tableName)[$filter->getField()]) + ? $schema->listTableColumns($tableName)[$filter->getField()] + : false; + + + if ($dbColumn) { + $dbField = $dbColumn->getFullQualifiedName(ucfirst($filter->getObject())); + } + else { + $translated = isset($this->translator[$filter->getField()]) + ? $this->translator[$filter->getField()] + : false; + + if (!$translated) { + var_dump('Unknown field: ' . $filter->getField()); + var_dump($filter); + return $qb; + throw new \Exception('Unknown field: ' . $filter->getField()); + } + + var_dump($translated); + $dbColumn = $schema->listTableColumns($translated['foreign_table'])[$translated['field']]; + } + + + $parameterHolder = $this->generateRandomParameterName(); + + if (isset($translated) && $translated) { + if (isset($translated['func'])) { + //@todo rewrite with getFullQualifiedName + $qb->leftJoin( + $this->tableAliases[$translated['table']], + $translated['foreign_table'], + $this->tableAliases[$translated['foreign_table']], + sprintf('%s.%s = %s.%s', + $this->tableAliases[$translated['table']], + $translated['table_field'], + $this->tableAliases[$translated['foreign_table']], + $translated['foreign_table_field'] + ) + ); + + //@todo rewrite with getFullQualifiedName + $qb->andHaving( + isset($translated['func']) + ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], + $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder)) + : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], + $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn)) + ); + + } else { + //@todo rewrite with getFullQualifiedName + $qb->innerJoin( + $this->tableAliases[$translated['table']], + $translated['foreign_table'], + $this->tableAliases[$translated['foreign_table']], + sprintf('%s.%s = %s.%s and %s', + $this->tableAliases[$translated['table']], + $translated['table_field'], + $this->tableAliases[$translated['foreign_table']], + $translated['foreign_table_field'], + sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], + $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn)) + ) + ); + } + + + + $qb->setParameter($parameterHolder, $filter->getFilter()); + + $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); + + var_dump($translated); + } + + return $qb; + +// //the next one will determine the group +// if ($leadSegmentFilter->getGlue() === 'or') { +// // Create a new group of andX expressions +// if ($groupExpr->count()) { +// $groups[] = $groupExpr; +// $groupExpr = $q->expr() +// ->andX() +// ; +// } +// } + + $parameterName = $this->generateRandomParameterName(); + + //@todo what is this? +// $ignoreAutoFilter = false; +// +// $func = $filter->getFunc(); +// +// // Generate a unique alias +// $alias = $this->generateRandomParameterName(); +// +// var_dump($func . ":" . $leadSegmentFilter->getField()); +// var_dump($exprParameter); + + + switch ($leadSegmentFilter->getField()) { + case 'hit_url': + case 'referer': + case 'source': + case 'source_id': + case 'url_title': + $operand = in_array( + $func, + [ + 'eq', + 'like', + 'regexp', + 'notRegexp', + 'startsWith', + 'endsWith', + 'contains', + ] + ) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + $column = $leadSegmentFilter->getField(); + + if ($column === 'hit_url') { + $column = 'url'; + } + + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX . 'page_hits', $alias) + ; + + switch ($func) { + case 'eq': + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.' . $column, $exprParameter), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + break; + case 'regexp': + case 'notRegexp': + $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); + $not = ($func === 'notRegexp') ? ' NOT' : ''; + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.lead_id', 'l.id'), + $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter + ) + ); + break; + case 'like': + case 'notLike': + case 'startsWith': + case 'endsWith': + case 'contains': + switch ($func) { + case 'like': + case 'notLike': + case 'contains': + $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; + break; + case 'startsWith': + $parameters[$parameter] = $leadSegmentFilter->getFilter() . '%'; + break; + case 'endsWith': + $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter(); + break; + } + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->like($alias . '.' . $column, $exprParameter), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + break; + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'device_model': + $ignoreAutoFilter = true; + $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp']) ? 'EXISTS' : 'NOT EXISTS'; + + $column = $leadSegmentFilter->getField(); + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX . 'lead_devices', $alias) + ; + switch ($func) { + case 'eq': + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.' . $column, $exprParameter), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + break; + case 'like': + case '!like': + $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->like($alias . '.' . $column, $exprParameter), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + break; + case 'regexp': + case 'notRegexp': + $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); + $not = ($func === 'notRegexp') ? ' NOT' : ''; + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.lead_id', 'l.id'), + $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter + ) + ); + break; + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + + break; + case 'hit_url_date': + case 'lead_email_read_date': + $operand = (in_array($func, ['eq', 'gt', 'lt', 'gte', 'lte', 'between'])) ? 'EXISTS' : 'NOT EXISTS'; + $table = 'page_hits'; + $column = 'date_hit'; + + if ($leadSegmentFilter->getField() === 'lead_email_read_date') { + $column = 'date_read'; + $table = 'email_stats'; + } + + + if ($filterField == 'lead_email_read_date') { + var_dump($func); + } + + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX . $table, $alias) + ; + + + switch ($func) { + case 'eq': + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.' . $column, $exprParameter), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + break; + case 'between': + case 'notBetween': + // Filter should be saved with double || to separate options + $parameter2 = $this->generateRandomParameterName(); + $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; + $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; + $exprParameter2 = ":$parameter2"; + $ignoreAutoFilter = true; + $field = $column; + + if ($func === 'between') { + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->gte($alias . '.' . $field, $exprParameter), + $q->expr() + ->lt($alias . '.' . $field, $exprParameter2), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + } + else { + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->lt($alias . '.' . $field, $exprParameter), + $q->expr() + ->gte($alias . '.' . $field, $exprParameter2), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + } + break; + default: + $parameter2 = $this->generateRandomParameterName(); + + if ($filterField == 'lead_email_read_date') { + var_dump($exprParameter); + } + $parameters[$parameter2] = $leadSegmentFilter->getFilter(); + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->$func( + $alias . '.' . $column, + $parameter2 + ), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + break; + } + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'page_id': + case 'email_id': + case 'redirect_id': + case 'notification': + $operand = ($func === 'eq') ? 'EXISTS' : 'NOT EXISTS'; + $column = $leadSegmentFilter->getField(); + $table = 'page_hits'; + $select = 'id'; + + if ($leadSegmentFilter->getField() === 'notification') { + $table = 'push_ids'; + $column = 'id'; + } + + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX . $table, $alias) + ; + + if ($leadSegmentFilter->getFilter() == 1) { + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->isNotNull($alias . '.' . $column), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + } + else { + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->isNull($alias . '.' . $column), + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'sessions': + $operand = 'EXISTS'; + $table = 'page_hits'; + $select = 'COUNT(id)'; + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX . $table, $alias) + ; + + $alias2 = $this->generateRandomParameterName(); + $subqb2 = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($alias2 . '.id') + ->from(MAUTIC_TABLE_PREFIX . $table, $alias2) + ; + + $subqb2->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias2 . '.lead_id', 'l.id'), + $q->expr() + ->gt($alias2 . '.date_hit', '(' . $alias . '.date_hit - INTERVAL 30 MINUTE)'), + $q->expr() + ->lt($alias2 . '.date_hit', $alias . '.date_hit') + ) + ); + + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.lead_id', 'l.id'), + $q->expr() + ->isNull($alias . '.email_id'), + $q->expr() + ->isNull($alias . '.redirect_id'), + sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()) + ) + ); + + $opr = ''; + switch ($func) { + case 'eq': + $opr = '='; + break; + case 'gt': + $opr = '>'; + break; + case 'gte': + $opr = '>='; + break; + case 'lt': + $opr = '<'; + break; + case 'lte': + $opr = '<='; + break; + } + if ($opr) { + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->having($select . $opr . $leadSegmentFilter->getFilter()); + } + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + case 'hit_url_count': + case 'lead_email_read_count': + $operand = 'EXISTS'; + $table = 'page_hits'; + $select = 'COUNT(id)'; + if ($leadSegmentFilter->getField() === 'lead_email_read_count') { + $table = 'email_stats'; + $select = 'COALESCE(SUM(open_count),0)'; + } + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX . $table, $alias) + ; + + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.lead_id', 'l.id') + ) + ); + + $opr = ''; + switch ($func) { + case 'eq': + $opr = '='; + break; + case 'gt': + $opr = '>'; + break; + case 'gte': + $opr = '>='; + break; + case 'lt': + $opr = '<'; + break; + case 'lte': + $opr = '<='; + break; + } + + if ($opr) { + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + $subqb->having($select . $opr . $leadSegmentFilter->getFilter()); + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); + break; + + case 'dnc_bounced': + case 'dnc_unsubscribed': + case 'dnc_bounced_sms': + case 'dnc_unsubscribed_sms': + // Special handling of do not contact + $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; + + $parts = explode('_', $leadSegmentFilter->getField()); + $channel = 'email'; + + if (count($parts) === 3) { + $channel = $parts[2]; + } + + $channelParameter = $this->generateRandomParameterName(); + $subqb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX . 'lead_donotcontact', $alias) + ->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.reason', $exprParameter), + $q->expr() + ->eq($alias . '.lead_id', 'l.id'), + $q->expr() + ->eq($alias . '.channel', ":$channelParameter") + ) + ) + ; + + $groupExpr->add( + sprintf('%s (%s)', $func, $subqb->getSQL()) + ); + + // Filter will always be true and differentiated via EXISTS/NOT EXISTS + $leadSegmentFilter->setFilter(true); + + $ignoreAutoFilter = true; + + $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; + $parameters[$channelParameter] = $channel; + + break; + + case 'leadlist': + $table = 'lead_lists_leads'; + $column = 'leadlist_id'; + $falseParameter = $this->generateRandomParameterName(); + $parameters[$falseParameter] = false; + $trueParameter = $this->generateRandomParameterName(); + $parameters[$trueParameter] = true; + $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; + $ignoreAutoFilter = true; + + if ($filterListIds = (array)$leadSegmentFilter->getFilter()) { + $listQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('l.id, l.filters') + ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l') + ; + $listQb->where( + $listQb->expr() + ->in('l.id', $filterListIds) + ); + $filterLists = $listQb->execute() + ->fetchAll() + ; + $not = 'NOT EXISTS' === $func; + + // Each segment's filters must be appended as ORs so that each list is evaluated individually + $existsExpr = $not ? $listQb->expr() + ->andX() : $listQb->expr() + ->orX() + ; + + foreach ($filterLists as $list) { + $alias = $this->generateRandomParameterName(); + $id = (int)$list['id']; + if ($id === (int)$listId) { + // Ignore as somehow self is included in the list + continue; + } + + $listFilters = unserialize($list['filters']); + if (empty($listFilters)) { + // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $id, + $parameters, + [ + $alias . '.manually_removed' => $falseParameter, + ] + ); + } + else { + // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet + // but also leverage the current membership to take into account those manually added or removed from the segment + + // Build a "live" query based on current filters to catch those that have not been processed yet + $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); + $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); + + // Left join membership to account for manually added and removed + $membershipAlias = $this->generateRandomParameterName(); + $subQb->leftJoin( + $alias, + MAUTIC_TABLE_PREFIX . $table, + $membershipAlias, + "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" + ) + ->where( + $subQb->expr() + ->orX( + $filterExpr, + $subQb->expr() + ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added + ) + ) + ->andWhere( + $subQb->expr() + ->eq("$alias.id", 'l.id'), + $subQb->expr() + ->orX( + $subQb->expr() + ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr() + ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + ) + ) + ; + } + + $existsExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); + } + + if ($existsExpr->count()) { + $groupExpr->add($existsExpr); + } + } + + break; + case 'tags': + case 'globalcategory': + case 'lead_email_received': + case 'lead_email_sent': + case 'device_type': + case 'device_brand': + case 'device_os': + // Special handling of lead lists and tags + $func = in_array($func, ['eq', 'in'], true) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + + // Collect these and apply after building the query because we'll want to apply the lead first for each of the subqueries + $subQueryFilters = []; + switch ($leadSegmentFilter->getField()) { + case 'tags': + $table = 'lead_tags_xref'; + $column = 'tag_id'; + break; + case 'globalcategory': + $table = 'lead_categories'; + $column = 'category_id'; + break; + case 'lead_email_received': + $table = 'email_stats'; + $column = 'email_id'; + + $trueParameter = $this->generateRandomParameterName(); + $subQueryFilters[$alias . '.is_read'] = $trueParameter; + $parameters[$trueParameter] = true; + break; + case 'lead_email_sent': + $table = 'email_stats'; + $column = 'email_id'; + break; + case 'device_type': + $table = 'lead_devices'; + $column = 'device'; + break; + case 'device_brand': + $table = 'lead_devices'; + $column = 'device_brand'; + break; + case 'device_os': + $table = 'lead_devices'; + $column = 'device_os_name'; + break; + } + + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $leadSegmentFilter->getFilter(), + $parameters, + $subQueryFilters + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); + break; + case 'stage': + // A note here that SQL EXISTS is being used for the eq and neq cases. + // I think this code might be inefficient since the sub-query is rerun + // for every row in the outer query's table. This might have to be refactored later on + // if performance is desired. + + $subQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX . 'stages', $alias) + ; + + switch ($func) { + case 'empty': + $groupExpr->add( + $q->expr() + ->isNull('l.stage_id') + ); + break; + case 'notEmpty': + $groupExpr->add( + $q->expr() + ->isNotNull('l.stage_id') + ); + break; + case 'eq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subQb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.id', 'l.stage_id'), + $q->expr() + ->eq($alias . '.id', ":$parameter") + ) + ); + $groupExpr->add(sprintf('EXISTS (%s)', $subQb->getSQL())); + break; + case 'neq': + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + + $subQb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.id', 'l.stage_id'), + $q->expr() + ->eq($alias . '.id', ":$parameter") + ) + ); + $groupExpr->add(sprintf('NOT EXISTS (%s)', $subQb->getSQL())); + break; + } + + break; + case 'integration_campaigns': + $parameter2 = $this->generateRandomParameterName(); + $operand = in_array($func, ['eq', 'neq']) ? 'EXISTS' : 'NOT EXISTS'; + $ignoreAutoFilter = true; + + $subQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX . 'integration_entity', $alias) + ; + switch ($func) { + case 'eq': + case 'neq': + if (strpos($leadSegmentFilter->getFilter(), '::') !== false) { + list($integrationName, $campaignId) = explode('::', $leadSegmentFilter->getFilter()); + } + else { + // Assuming this is a Salesforce integration for BC with pre 2.11.0 + $integrationName = 'Salesforce'; + $campaignId = $leadSegmentFilter->getFilter(); + } + + $parameters[$parameter] = $campaignId; + $parameters[$parameter2] = $integrationName; + $subQb->where( + $q->expr() + ->andX( + $q->expr() + ->eq($alias . '.integration', ":$parameter2"), + $q->expr() + ->eq($alias . '.integration_entity', "'CampaignMember'"), + $q->expr() + ->eq($alias . '.integration_entity_id', ":$parameter"), + $q->expr() + ->eq($alias . '.internal_entity', "'lead'"), + $q->expr() + ->eq($alias . '.internal_entity_id', 'l.id') + ) + ); + break; + } + + $groupExpr->add(sprintf('%s (%s)', $operand, $subQb->getSQL())); + + break; + default: + if (!$column) { + // Column no longer exists so continue + continue; + } + + switch ($func) { + case 'between': + case 'notBetween': + // Filter should be saved with double || to separate options + $parameter2 = $this->generateRandomParameterName(); + $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; + $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; + $exprParameter2 = ":$parameter2"; + $ignoreAutoFilter = true; + + if ($func === 'between') { + $groupExpr->add( + $q->expr() + ->andX( + $q->expr() + ->gte($field, $exprParameter), + $q->expr() + ->lt($field, $exprParameter2) + ) + ); + } + else { + $groupExpr->add( + $q->expr() + ->andX( + $q->expr() + ->lt($field, $exprParameter), + $q->expr() + ->gte($field, $exprParameter2) + ) + ); + } + break; + + case 'notEmpty': + $groupExpr->add( + $q->expr() + ->andX( + $q->expr() + ->isNotNull($field), + $q->expr() + ->neq($field, $q->expr() + ->literal('')) + ) + ); + $ignoreAutoFilter = true; + break; + + case 'empty': + $leadSegmentFilter->setFilter(''); + $groupExpr->add( + $this->generateFilterExpression($q, $field, 'eq', $exprParameter, true) + ); + break; + + case 'in': + case 'notIn': + $cleanFilter = []; + foreach ($leadSegmentFilter->getFilter() as $key => $value) { + $cleanFilter[] = $q->expr() + ->literal( + InputHelper::clean($value) + ) + ; + } + $leadSegmentFilter->setFilter($cleanFilter); + + if ($leadSegmentFilter->getType() === 'multiselect') { + foreach ($leadSegmentFilter->getFilter() as $filter) { + $filter = trim($filter, "'"); + + if (substr($func, 0, 3) === 'not') { + $operator = 'NOT REGEXP'; + } + else { + $operator = 'REGEXP'; + } + + $groupExpr->add( + $field . " $operator '\\\\|?$filter\\\\|?'" + ); + } + } + else { + $groupExpr->add( + $this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null) + ); + } + $ignoreAutoFilter = true; + break; + + case 'neq': + $groupExpr->add( + $this->generateFilterExpression($q, $field, $func, $exprParameter, null) + ); + break; + + case 'like': + case 'notLike': + case 'startsWith': + case 'endsWith': + case 'contains': + $ignoreAutoFilter = true; + + switch ($func) { + case 'like': + case 'notLike': + $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%' . $leadSegmentFilter->getFilter() . '%' + : $leadSegmentFilter->getFilter(); + break; + case 'startsWith': + $func = 'like'; + $parameters[$parameter] = $leadSegmentFilter->getFilter() . '%'; + break; + case 'endsWith': + $func = 'like'; + $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter(); + break; + case 'contains': + $func = 'like'; + $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; + break; + } + + $groupExpr->add( + $this->generateFilterExpression($q, $field, $func, $exprParameter, null) + ); + break; + case 'regexp': + case 'notRegexp': + $ignoreAutoFilter = true; + $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); + $not = ($func === 'notRegexp') ? ' NOT' : ''; + $groupExpr->add( + // Escape single quotes while accounting for those that may already be escaped + $field . $not . ' REGEXP ' . $exprParameter + ); + break; + default: + $ignoreAutoFilter = true; + $groupExpr->add($q->expr() + ->$func($field, $exprParameter)); + $parameters[$exprParameter] = $leadSegmentFilter->getFilter(); + } + } + + if (!$ignoreAutoFilter) { + $parameters[$parameter] = $leadSegmentFilter->getFilter(); + } + + if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { + $event = new LeadListFilteringEvent($leadSegmentFilter->toArray(), null, $alias, $func, $q, $this->entityManager); + $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); + if ($event->isFilteringDone()) { + $groupExpr->add($event->getSubQuery()); + } + } + + + // Get the last of the filters + if ($groupExpr->count()) { + $groups[] = $groupExpr; + } + if (count($groups) === 1) { + // Only one andX expression + $expr = $groups[0]; + } + elseif (count($groups) > 1) { + // Sets of expressions grouped by OR + $orX = $q->expr() + ->orX() + ; + $orX->addMultiple($groups); + + // Wrap in a andX for other functions to append + $expr = $q->expr() + ->andX($orX) + ; + } + else { + $expr = $groupExpr; + } + + foreach ($parameters as $k => $v) { + $paramType = null; + + if (is_array($v) && isset($v['type'], $v['value'])) { + $paramType = $v['type']; + $v = $v['value']; + } + $q->setParameter($k, $v, $paramType); + } + + var_dump($parameters); + + return $expr; + } + + /** + * Generate a unique parameter name. + * + * @return string + */ + private function generateRandomParameterName() + { + return $this->randomParameterName->generateRandomParameterName(); + } + + /** + * @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q + * @param $column + * @param $operator + * @param $parameter + * @param $includeIsNull true/false or null to auto determine based on operator + * + * @return mixed + */ + public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) + { + // in/notIn for dbal will use a raw array + if (!is_array($parameter) && strpos($parameter, ':') !== 0) { + $parameter = ":$parameter"; + } + + if (null === $includeIsNull) { + // Auto determine based on negate operators + $includeIsNull = (in_array($operator, ['neq', 'notLike', 'notIn'])); + } + + if ($includeIsNull) { + $expr = $q->expr() + ->orX( + $q->expr() + ->$operator($column, $parameter), + $q->expr() + ->isNull($column) + ) + ; + } + else { + $expr = $q->expr() + ->$operator($column, $parameter) + ; + } + + return $expr; + } + + /** + * @param $table + * @param $alias + * @param $column + * @param $value + * @param array $parameters + * @param null $leadId + * @param array $subQueryFilters + * + * @return QueryBuilder + */ + protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) + { + $subQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ; + $subExpr = $subQb->expr() + ->andX() + ; + + if ('leads' !== $table) { + $subExpr->add( + $subQb->expr() + ->eq($alias . '.lead_id', 'l.id') + ); + } + + foreach ($subQueryFilters as $subColumn => $subParameter) { + $subExpr->add( + $subQb->expr() + ->eq($subColumn, ":$subParameter") + ); + } + + if (null !== $value && !empty($column)) { + $subFilterParamter = $this->generateRandomParameterName(); + $subFunc = 'eq'; + if (is_array($value)) { + $subFunc = 'in'; + $subExpr->add( + $subQb->expr() + ->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") + ); + $parameters[$subFilterParamter] = ['value' => $value, 'type' => \Doctrine\DBAL\Connection::PARAM_STR_ARRAY]; + } + else { + $parameters[$subFilterParamter] = $value; + } + + $subExpr->add( + $subQb->expr() + ->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") + ); + } + + $subQb->select('null') + ->from(MAUTIC_TABLE_PREFIX . $table, $alias) + ->where($subExpr) + ; + + return $subQb; + } + + /** + * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should + * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. + * + * @param QueryBuilder $q + * @param LeadSegmentFilters $leadSegmentFilters + */ + private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) + { + $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; + // Join company tables for query optimization + $q->$joinType('l', MAUTIC_TABLE_PREFIX . 'companies_leads', 'cl', 'l.id = cl.lead_id') + ->$joinType( + 'cl', + MAUTIC_TABLE_PREFIX . 'companies', + 'comp', + 'cl.company_id = comp.id' + ) + ; + + // Return only unique contacts + $q->groupBy('l.id'); + } + + + private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) + { + var_dump(debug_backtrace()[1]['function']); + $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); + + if ($leadSegmentFilters->isHasCompanyFilter()) { + $this->applyCompanyFieldFilters($q, $leadSegmentFilters); + } + + return $expr; + } + + +} diff --git a/app/bundles/LeadBundle/Services/test.sql b/app/bundles/LeadBundle/Services/test.sql new file mode 100644 index 00000000000..23a7eb5d5ae --- /dev/null +++ b/app/bundles/LeadBundle/Services/test.sql @@ -0,0 +1 @@ +SELECT * FROM MauticLeadBundle:Lead l LEFT JOIN email_stats es ON l.id = es.lead_id INNER JOIN page_hits ph ON l.id = ph.lead_id and ph.date_hit > "2017-09-10 23:14" GROUP BY l.id HAVING sum(es.open_count) > 0 \ No newline at end of file From e13976fc4944b12da94d3c87d1b680b9aa9603df Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Sat, 6 Jan 2018 18:07:55 +0100 Subject: [PATCH 009/778] reverted unwanted changes done to petr's work --- .../LeadBundle/Entity/LeadListRepository.php | 1 + .../Entity/LeadListSegmentRepository.php | 232 +++++++++--------- .../LeadBundle/Segment/LeadSegmentService.php | 4 +- .../Services/LeadSegmentQueryBuilder.php | 80 +++--- 4 files changed, 160 insertions(+), 157 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index e28dbf9cf37..e5abbcc4ebe 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -511,6 +511,7 @@ public function getLeadsByList($lists, $args = []) dump($q->getSQL()); dump($q->getParameters()); + die(); $results = $q->execute()->fetchAll(); foreach ($results as $r) { diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 2a2a7272af7..1fb64f02f64 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -65,7 +65,7 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte } $q->select($select) - ->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); + ->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); $batchExpr = $q->expr()->andX(); // Only leads that existed at the time of count @@ -121,7 +121,7 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte if (!empty($limit)) { $q->setFirstResult($start) - ->setMaxResults($limit); + ->setMaxResults($limit); } // remove any possible group by @@ -248,9 +248,9 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query } $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX.'page_hits', $alias); + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX.'page_hits', $alias); switch ($func) { case 'eq': @@ -310,9 +310,9 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $column = $leadSegmentFilter->getField(); $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX.'lead_devices', $alias); + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX.'lead_devices', $alias); switch ($func) { case 'eq': case 'neq': @@ -367,9 +367,9 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query } $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + ->createQueryBuilder() + ->select('id') + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); @@ -380,12 +380,12 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $subqb->where( $q->expr() - ->andX( - $q->expr() - ->eq($alias.'.'.$column, $exprParameter), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr() + ->eq($alias.'.'.$column, $exprParameter), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) ); break; case 'between': @@ -401,20 +401,20 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query if ($func === 'between') { $subqb->where( $q->expr() - ->andX( - $q->expr()->gte($alias.'.'.$field, $exprParameter), - $q->expr()->lt($alias.'.'.$field, $exprParameter2), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr()->gte($alias.'.'.$field, $exprParameter), + $q->expr()->lt($alias.'.'.$field, $exprParameter2), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) ); } else { $subqb->where( $q->expr() - ->andX( - $q->expr()->lt($alias.'.'.$field, $exprParameter), - $q->expr()->gte($alias.'.'.$field, $exprParameter2), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr()->lt($alias.'.'.$field, $exprParameter), + $q->expr()->gte($alias.'.'.$field, $exprParameter2), + $q->expr()->eq($alias.'.lead_id', 'l.id') + ) ); } break; @@ -428,15 +428,15 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $subqb->where( $q->expr() - ->andX( - $q->expr() - ->$func( - $alias.'.'.$column, - $parameter2 - ), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr() + ->$func( + $alias.'.'.$column, + $parameter2 + ), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) ); break; } @@ -457,29 +457,29 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query } $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); if ($leadSegmentFilter->getFilter() == 1) { $subqb->where( $q->expr() - ->andX( - $q->expr() - ->isNotNull($alias.'.'.$column), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr() + ->isNotNull($alias.'.'.$column), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) ); } else { $subqb->where( $q->expr() - ->andX( - $q->expr() - ->isNull($alias.'.'.$column), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr() + ->isNull($alias.'.'.$column), + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) ); } @@ -490,38 +490,38 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $table = 'page_hits'; $select = 'COUNT(id)'; $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); $alias2 = $this->generateRandomParameterName(); $subqb2 = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($alias2.'.id') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); + ->createQueryBuilder() + ->select($alias2.'.id') + ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); $subqb2->where( $q->expr() - ->andX( - $q->expr()->eq($alias2.'.lead_id', 'l.id'), - $q->expr()->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), - $q->expr()->lt($alias2.'.date_hit', $alias.'.date_hit') - ) + ->andX( + $q->expr()->eq($alias2.'.lead_id', 'l.id'), + $q->expr()->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), + $q->expr()->lt($alias2.'.date_hit', $alias.'.date_hit') + ) ); $parameters[$parameter] = $leadSegmentFilter->getFilter(); $subqb->where( $q->expr() - ->andX( - $q->expr() - ->eq($alias.'.lead_id', 'l.id'), - $q->expr() - ->isNull($alias.'.email_id'), - $q->expr() - ->isNull($alias.'.redirect_id'), - sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()) - ) + ->andX( + $q->expr() + ->eq($alias.'.lead_id', 'l.id'), + $q->expr() + ->isNull($alias.'.email_id'), + $q->expr() + ->isNull($alias.'.redirect_id'), + sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()) + ) ); $opr = ''; @@ -558,17 +558,17 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $select = 'COALESCE(SUM(open_count),0)'; } $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); + ->createQueryBuilder() + ->select($select) + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); $parameters[$parameter] = $leadSegmentFilter->getFilter(); $subqb->where( $q->expr() - ->andX( - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) + ->andX( + $q->expr() + ->eq($alias.'.lead_id', 'l.id') + ) ); $opr = ''; @@ -614,15 +614,15 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $channelParameter = $this->generateRandomParameterName(); $subqb = $this->entityManager->getConnection()->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) - ->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.reason', $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id'), - $q->expr()->eq($alias.'.channel', ":$channelParameter") - ) - ); + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) + ->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.reason', $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id'), + $q->expr()->eq($alias.'.channel', ":$channelParameter") + ) + ); $groupExpr->add( sprintf('%s (%s)', $func, $subqb->getSQL()) @@ -650,8 +650,8 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query if ($filterListIds = (array) $leadSegmentFilter->getFilter()) { $listQb = $this->entityManager->getConnection()->createQueryBuilder() - ->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'l'); + ->select('l.id, l.filters') + ->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'l'); $listQb->where( $listQb->expr()->in('l.id', $filterListIds) ); @@ -698,19 +698,19 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" ) - ->where( - $subQb->expr()->orX( - $filterExpr, - $subQb->expr()->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ) - ) - ->andWhere( - $subQb->expr()->eq("$alias.id", 'l.id'), - $subQb->expr()->orX( - $subQb->expr()->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr()->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - ) - ); + ->where( + $subQb->expr()->orX( + $filterExpr, + $subQb->expr()->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added + ) + ) + ->andWhere( + $subQb->expr()->eq("$alias.id", 'l.id'), + $subQb->expr()->orX( + $subQb->expr()->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr()->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + ) + ); } $existsExpr->add( @@ -793,9 +793,9 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query // if performance is desired. $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'stages', $alias); + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'stages', $alias); switch ($func) { case 'empty': @@ -839,9 +839,9 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $ignoreAutoFilter = true; $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'integration_entity', $alias); + ->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'integration_entity', $alias); switch ($func) { case 'eq': case 'neq': @@ -1142,8 +1142,8 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu } $subQb->select('null') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias) - ->where($subExpr); + ->from(MAUTIC_TABLE_PREFIX.$table, $alias) + ->where($subExpr); return $subQb; } @@ -1160,12 +1160,12 @@ private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $l $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; // Join company tables for query optimization $q->$joinType('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'l.id = cl.lead_id') - ->$joinType( - 'cl', - MAUTIC_TABLE_PREFIX.'companies', - 'comp', - 'cl.company_id = comp.id' - ); + ->$joinType( + 'cl', + MAUTIC_TABLE_PREFIX.'companies', + 'comp', + 'cl.company_id = comp.id' + ); // Return only unique contacts $q->groupBy('l.id'); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 0214ca15af9..9443e6f8eb5 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -49,14 +49,14 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); - var_dump($sql = $qb->getSQL()); + dump($sql = $qb->getSQL()); $parameters = $qb->getParameters(); foreach($parameters as $parameter=>$value) { $sql = str_replace(':' . $parameter, $value, $sql); } var_dump($sql); // die(); - + return null; return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 7c79ce1b3a8..9faae50fbe6 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -200,45 +200,8 @@ private function getFilterValue(LeadSegmentFilter $filter, $parameterHolder, Col throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getType())); } - private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) - { - $parameters = []; - - //@todo cache metadata - $schema = $this->entityManager->getConnection() - ->getSchemaManager() - ; - $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) - ->getTableName() - ; - - - /** @var Column $dbColumn */ - $dbColumn = isset($schema->listTableColumns($tableName)[$filter->getField()]) - ? $schema->listTableColumns($tableName)[$filter->getField()] - : false; - - - if ($dbColumn) { - $dbField = $dbColumn->getFullQualifiedName(ucfirst($filter->getObject())); - } - else { - $translated = isset($this->translator[$filter->getField()]) - ? $this->translator[$filter->getField()] - : false; - - if (!$translated) { - var_dump('Unknown field: ' . $filter->getField()); - var_dump($filter); - return $qb; - throw new \Exception('Unknown field: ' . $filter->getField()); - } - - var_dump($translated); - $dbColumn = $schema->listTableColumns($translated['foreign_table'])[$translated['field']]; - } - + private function addForeignTableQuery(QueryBuilder $qb, $translated) { $parameterHolder = $this->generateRandomParameterName(); if (isset($translated) && $translated) { @@ -287,9 +250,48 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $qb->setParameter($parameterHolder, $filter->getFilter()); $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); + } + } - var_dump($translated); + private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) + { + $parameters = []; + + //@todo cache metadata + $schema = $this->entityManager->getConnection() + ->getSchemaManager() + ; + $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) + ->getTableName() + ; + + + /** @var Column $dbColumn */ + $dbColumn = isset($schema->listTableColumns($tableName)[$filter->getField()]) + ? $schema->listTableColumns($tableName)[$filter->getField()] + : false; + + + if ($dbColumn) { + $dbField = $dbColumn->getFullQualifiedName(ucfirst($filter->getObject())); } + else { + $translated = isset($this->translator[$filter->getField()]) + ? $this->translator[$filter->getField()] + : false; + + if (!$translated) { + var_dump('Unknown field: ' . $filter->getField()); + var_dump($filter); + return $qb; + throw new \Exception('Unknown field: ' . $filter->getField()); + } + + $dbColumn = $schema->listTableColumns($translated['foreign_table'])[$translated['field']]; + } + + + return $qb; From e2c3a0753ad26e19b13d62651b0378b6cb4f4b6c Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 8 Jan 2018 12:36:35 +0100 Subject: [PATCH 010/778] creae segment filter functions to better describe it self and rework query builder filter knows its operator split functions for creating query implement tranlsation object which describes better the relations and conditions to be added to basic query builder --- app/bundles/LeadBundle/Config/config.php | 5 + .../LeadBundle/Segment/LeadSegmentFilter.php | 97 ++++++- .../Services/LeadSegmentFilterDescriptor.php | 42 ++++ .../Services/LeadSegmentQueryBuilder.php | 237 +++++++++++++----- 4 files changed, 306 insertions(+), 75 deletions(-) create mode 100644 app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index b312df6b054..087c9b20f36 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -758,11 +758,16 @@ 'mautic.lead.model.lead_segment_service', ], ], + 'mautic.lead.repository.lead_segment_filter_descriptor' => [ + 'class' => \Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor::class, + 'arguments' => [], + ], 'mautic.lead.repository.lead_segment_query_builder' => [ 'class' => \Mautic\LeadBundle\Services\LeadSegmentQueryBuilder::class, 'arguments' => [ 'doctrine.orm.entity_manager', 'mautic.lead.model.random_parameter_name', + 'mautic.lead.repository.lead_segment_filter_descriptor' ], ], 'mautic.lead.model.lead_segment_service' => [ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 76de02ea6a9..fb824856bcf 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -11,9 +11,11 @@ namespace Mautic\LeadBundle\Segment; +use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; + class LeadSegmentFilter { - const LEAD_OBJECT = 'lead'; + const LEAD_OBJECT = 'lead'; const COMPANY_OBJECT = 'company'; /** @@ -56,13 +58,21 @@ class LeadSegmentFilter */ private $func; + /** @var LeadSegmentFilterDescriptor $translator */ + private $translator; + + /** + * @var array + */ + private $queryDescription = null; + public function __construct(array $filter) { - $this->glue = isset($filter['glue']) ? $filter['glue'] : null; - $this->field = isset($filter['field']) ? $filter['field'] : null; - $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; - $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->display = isset($filter['display']) ? $filter['display'] : null; + $this->glue = isset($filter['glue']) ? $filter['glue'] : null; + $this->field = isset($filter['field']) ? $filter['field'] : null; + $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; + $this->type = isset($filter['type']) ? $filter['type'] : null; + $this->display = isset($filter['display']) ? $filter['display'] : null; $operatorValue = isset($filter['operator']) ? $filter['operator'] : null; $this->setOperator($operatorValue); @@ -71,6 +81,42 @@ public function __construct(array $filter) $this->setFilter($filterValue); } + /** + * @return string + * @throws \Exception + */ + public function getSQLOperator() + { + switch ($this->getOperator()) { + case 'gt': + return '>'; + case 'eq': + return '='; + case 'gt': + return '>'; + case 'gte': + return '>='; + case 'lt': + return '<'; + case 'lte': + return '<='; + } + throw new \Exception(sprintf('Unknown operator \'%s\'.', $filter->getOperator())); + } + + public function getFilterConditionValue($argument = null) { + switch ($this->getType()) { + case 'number': + return ":" . $argument; + case 'datetime': + return sprintf('":%s"', $argument); + default: + var_dump($dbColumn->getType()); + die(); + } + throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getType())); + } + /** * @return string|null */ @@ -207,14 +253,49 @@ private function sanitizeFilter($filter) switch ($this->getType()) { case 'number': - $filter = (float) $filter; + $filter = (float)$filter; break; case 'boolean': - $filter = (bool) $filter; + $filter = (bool)$filter; break; } return $filter; } + + /** + * @return array + */ + public function getQueryDescription($translator = null) + { + $this->translator = is_null($translator) ? new LeadSegmentFilterDescriptor() : $translator; + + if (is_null($this->queryDescription)) { + $this->assembleQueryDescription(); + } + return $this->queryDescription; + } + + /** + * @param array $queryDescription + * @return LeadSegmentFilter + */ + public function setQueryDescription($queryDescription) + { + $this->queryDescription = $queryDescription; + return $this; + } + + /** + * @return $this + */ + private function assembleQueryDescription() { + + $this->queryDescription = isset($this->translator[$this->getField()]) + ? $this->translator[$this->getField()] + : false; + + return $this; + } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php new file mode 100644 index 00000000000..bcce61e1c71 --- /dev/null +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -0,0 +1,42 @@ +translations['lead_email_read_count'] = [ + 'type' => 'foreign_aggr', + 'foreign_table' => 'email_stats', + 'foreign_table_field' => 'lead_id', + 'table' => 'leads', + 'table_field' => 'id', + 'func' => 'sum', + 'field' => 'open_count' + ]; + + $this->translations['lead_email_read_date'] = [ + 'type' => 'foreign', + 'foreign_table' => 'page_hits', + 'foreign_table_field' => 'lead_id', + 'table' => 'leads', + 'table_field' => 'id', + 'field' => 'date_hit' + ]; + + parent::__construct($this->translations); + } + +} diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 9faae50fbe6..41cbaad1361 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -33,37 +33,29 @@ class LeadSegmentQueryBuilder private $tableAliases = []; - private $translator = []; + /** @var LeadSegmentFilterDescriptor */ + private $translator; + + /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ + private $schema; public function __construct( EntityManager $entityManager, - RandomParameterName $randomParameterName + RandomParameterName $randomParameterName, + LeadSegmentFilterDescriptor $translator ) { $this->entityManager = $entityManager; $this->randomParameterName = $randomParameterName; + $this->schema = $this->entityManager->getConnection() + ->getSchemaManager() + ; + $this->translator = $translator; //@todo Will be generate automatically, just as POC $this->tableAliases['leads'] = 'l'; $this->tableAliases['email_stats'] = 'es'; - $this->tableAliases['page_hits'] = 'ph'; - - $this->translator['lead_email_read_count'] = [ - 'foreign_table' => 'email_stats', - 'foreign_table_field' => 'lead_id', - 'table' => 'leads', - 'table_field' => 'id', - 'func' => 'sum', - 'field' => 'open_count' - ]; - - $this->translator['lead_email_read_date'] = [ - 'foreign_table' => 'page_hits', - 'foreign_table_field' => 'lead_id', - 'table' => 'leads', - 'table_field' => 'id', - 'field' => 'date_hit' - ]; + $this->tableAliases['page_hits'] = 'ph'; } //object(Mautic\LeadBundle\Segment\LeadSegmentFilter)[841] @@ -167,42 +159,12 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters return $leads; } - private function getFilterOperator(LeadSegmentFilter $filter, $translated = false) + private function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) { - switch ($filter->getOperator()) { - case 'gt': - return '>'; - case 'eq': - return '='; - case 'gt': - return '>'; - case 'gte': - return '>='; - case 'lt': - return '<'; - case 'lte': - return '<='; - } - throw new \Exception(sprintf('Unknown operator \'%s\'.', $filter->getOperator())); - } - - private function getFilterValue(LeadSegmentFilter $filter, $parameterHolder, Column $dbColumn = null) - { - switch ($filter->getType()) { - case 'number': - return ":" . $parameterHolder; - case 'datetime': - return sprintf('":%s"', $parameterHolder); - default: - var_dump($dbColumn->getType()); - die(); - } - throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getType())); - } + $translated = $filter->getQueryDescription($this->getTranslator()); - - private function addForeignTableQuery(QueryBuilder $qb, $translated) { $parameterHolder = $this->generateRandomParameterName(); + $dbColumn = $this->schema->listTableColumns($translated['foreign_table'])[$translated['field']]; if (isset($translated) && $translated) { if (isset($translated['func'])) { @@ -223,12 +185,13 @@ private function addForeignTableQuery(QueryBuilder $qb, $translated) { $qb->andHaving( isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], - $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder)) + $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn)) ); - } else { + } + else { //@todo rewrite with getFullQualifiedName $qb->innerJoin( $this->tableAliases[$translated['table']], @@ -240,35 +203,136 @@ private function addForeignTableQuery(QueryBuilder $qb, $translated) { $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], - $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn)) + $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) ) ); } - $qb->setParameter($parameterHolder, $filter->getFilter()); $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); } } + private function addLeadListQuery() { + $table = 'lead_lists_leads'; + $column = 'leadlist_id'; + $falseParameter = $this->generateRandomParameterName(); + $parameters[$falseParameter] = false; + $trueParameter = $this->generateRandomParameterName(); + $parameters[$trueParameter] = true; + $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; + $ignoreAutoFilter = true; + + if ($filterListIds = (array)$leadSegmentFilter->getFilter()) { + $listQb = $this->entityManager->getConnection() + ->createQueryBuilder() + ->select('l.id, l.filters') + ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l') + ; + $listQb->where( + $listQb->expr() + ->in('l.id', $filterListIds) + ); + $filterLists = $listQb->execute() + ->fetchAll() + ; + $not = 'NOT EXISTS' === $func; + + // Each segment's filters must be appended as ORs so that each list is evaluated individually + $existsExpr = $not ? $listQb->expr() + ->andX() : $listQb->expr() + ->orX() + ; + + foreach ($filterLists as $list) { + $alias = $this->generateRandomParameterName(); + $id = (int)$list['id']; + if ($id === (int)$listId) { + // Ignore as somehow self is included in the list + continue; + } + + $listFilters = unserialize($list['filters']); + if (empty($listFilters)) { + // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $id, + $parameters, + [ + $alias . '.manually_removed' => $falseParameter, + ] + ); + } + else { + // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet + // but also leverage the current membership to take into account those manually added or removed from the segment + + // Build a "live" query based on current filters to catch those that have not been processed yet + $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); + $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); + + // Left join membership to account for manually added and removed + $membershipAlias = $this->generateRandomParameterName(); + $subQb->leftJoin( + $alias, + MAUTIC_TABLE_PREFIX . $table, + $membershipAlias, + "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" + ) + ->where( + $subQb->expr() + ->orX( + $filterExpr, + $subQb->expr() + ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added + ) + ) + ->andWhere( + $subQb->expr() + ->eq("$alias.id", 'l.id'), + $subQb->expr() + ->orX( + $subQb->expr() + ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr() + ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + ) + ) + ; + } + + $existsExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); + } + + if ($existsExpr->count()) { + $groupExpr->add($existsExpr); + } + } + + break; + } + + private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $parameters = []; //@todo cache metadata - $schema = $this->entityManager->getConnection() - ->getSchemaManager() - ; $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) ->getTableName() ; /** @var Column $dbColumn */ - $dbColumn = isset($schema->listTableColumns($tableName)[$filter->getField()]) - ? $schema->listTableColumns($tableName)[$filter->getField()] + $dbColumn = isset($this->schema->listTableColumns($tableName)[$filter->getField()]) + ? $this->schema->listTableColumns($tableName)[$filter->getField()] : false; @@ -276,9 +340,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $dbField = $dbColumn->getFullQualifiedName(ucfirst($filter->getObject())); } else { - $translated = isset($this->translator[$filter->getField()]) - ? $this->translator[$filter->getField()] - : false; + $translated = $filter->getQueryDescription(); if (!$translated) { var_dump('Unknown field: ' . $filter->getField()); @@ -287,10 +349,15 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) throw new \Exception('Unknown field: ' . $filter->getField()); } - $dbColumn = $schema->listTableColumns($translated['foreign_table'])[$translated['field']]; - } + switch ($translated['type']) { + case 'foreign': + case 'foreign_aggr': + $this->addForeignTableQuery($qb, $filter); + break; + } + } return $qb; @@ -1395,5 +1462,41 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter return $expr; } + /** + * @return LeadSegmentFilterDescriptor + */ + public function getTranslator() + { + return $this->translator; + } + + /** + * @param LeadSegmentFilterDescriptor $translator + * @return LeadSegmentQueryBuilder + */ + public function setTranslator($translator) + { + $this->translator = $translator; + return $this; + } + + /** + * @return \Doctrine\DBAL\Schema\AbstractSchemaManager + */ + public function getSchema() + { + return $this->schema; + } + + /** + * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @return LeadSegmentQueryBuilder + */ + public function setSchema($schema) + { + $this->schema = $schema; + return $this; + } + } From a1f0ab0c0585d2f4242bdbc84654e667770fb672 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 8 Jan 2018 19:36:41 +0100 Subject: [PATCH 011/778] move some logic to filter object, resolve strange DI --- app/bundles/LeadBundle/Config/config.php | 5 +- .../LeadBundle/Segment/LeadSegmentFilter.php | 44 +- .../Segment/LeadSegmentFilterFactory.php | 19 +- .../LeadBundle/Segment/LeadSegmentService.php | 2 + .../Services/LeadSegmentQueryBuilder.php | 927 +++++------------- 5 files changed, 302 insertions(+), 695 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 087c9b20f36..20a3648f3b6 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -766,8 +766,7 @@ 'class' => \Mautic\LeadBundle\Services\LeadSegmentQueryBuilder::class, 'arguments' => [ 'doctrine.orm.entity_manager', - 'mautic.lead.model.random_parameter_name', - 'mautic.lead.repository.lead_segment_filter_descriptor' + 'mautic.lead.model.random_parameter_name' ], ], 'mautic.lead.model.lead_segment_service' => [ @@ -783,6 +782,8 @@ 'arguments' => [ 'mautic.lead.model.lead_segment_filter_date', 'mautic.lead.model.lead_segment_filter_operator', + 'mautic.lead.repository.lead_segment_filter_descriptor', + 'doctrine.orm.entity_manager' ], ], 'mautic.lead.model.relative_date' => [ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index fb824856bcf..404c60f62df 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -11,6 +11,8 @@ namespace Mautic\LeadBundle\Segment; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\Column; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class LeadSegmentFilter @@ -58,16 +60,21 @@ class LeadSegmentFilter */ private $func; - /** @var LeadSegmentFilterDescriptor $translator */ - private $translator; - /** * @var array */ private $queryDescription = null; - public function __construct(array $filter) + /** @var Column */ + private $dbColumn; + + private $schema; + + public function __construct(array $filter, \ArrayIterator $dictionary = null, AbstractSchemaManager $schema = null) { + if (is_null($schema)) { + throw new \Exception('No schema'); + } $this->glue = isset($filter['glue']) ? $filter['glue'] : null; $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; @@ -79,6 +86,10 @@ public function __construct(array $filter) $filterValue = isset($filter['filter']) ? $filter['filter'] : null; $this->setFilter($filterValue); + $this->schema = $schema; + if (!is_null($dictionary)) { + $this->translateQueryDescription($dictionary); + } } /** @@ -101,7 +112,7 @@ public function getSQLOperator() case 'lte': return '<='; } - throw new \Exception(sprintf('Unknown operator \'%s\'.', $filter->getOperator())); + throw new \Exception(sprintf('Unknown operator \'%s\'.', $this->getOperator())); } public function getFilterConditionValue($argument = null) { @@ -111,12 +122,18 @@ public function getFilterConditionValue($argument = null) { case 'datetime': return sprintf('":%s"', $argument); default: - var_dump($dbColumn->getType()); + var_dump($this->getDBColumn()->getType()); die(); } throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getType())); } + + public function getDBColumn() { + $dbColumn = $this->schema->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; + var_dump($dbColumn); + } + /** * @return string|null */ @@ -267,12 +284,10 @@ private function sanitizeFilter($filter) /** * @return array */ - public function getQueryDescription($translator = null) + public function getQueryDescription($dictionary = null) { - $this->translator = is_null($translator) ? new LeadSegmentFilterDescriptor() : $translator; - if (is_null($this->queryDescription)) { - $this->assembleQueryDescription(); + $this->assembleQueryDescription($dictionary); } return $this->queryDescription; } @@ -290,10 +305,13 @@ public function setQueryDescription($queryDescription) /** * @return $this */ - private function assembleQueryDescription() { + public function translateQueryDescription(\ArrayIterator $dictionary) { + if (is_null($this->schema)) { + throw new \Exception('You need to pass database schema manager along with dictionary'); + } - $this->queryDescription = isset($this->translator[$this->getField()]) - ? $this->translator[$this->getField()] + $this->queryDescription = isset($dictionary[$this->getField()]) + ? $dictionary[$this->getField()] : false; return $this; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 7f87d41867c..aff043598c1 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -11,7 +11,9 @@ namespace Mautic\LeadBundle\Segment; +use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; +use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class LeadSegmentFilterFactory { @@ -25,10 +27,23 @@ class LeadSegmentFilterFactory */ private $leadSegmentFilterOperator; - public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSegmentFilterOperator $leadSegmentFilterOperator) + /** @var LeadSegmentFilterDescriptor */ + private $dictionary; + + /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ + private $schema; + + public function __construct( + LeadSegmentFilterDate $leadSegmentFilterDate, + LeadSegmentFilterOperator $leadSegmentFilterOperator, + LeadSegmentFilterDescriptor $dictionary, + EntityManager $entityManager +) { $this->leadSegmentFilterDate = $leadSegmentFilterDate; $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; + $this->dictionary = $dictionary; + $this->schema = $entityManager->getConnection()->getSchemaManager(); } /** @@ -42,7 +57,7 @@ public function getLeadListFilters(LeadList $leadList) $filters = $leadList->getFilters(); foreach ($filters as $filter) { - $leadSegmentFilter = new LeadSegmentFilter($filter); + $leadSegmentFilter = new LeadSegmentFilter($filter, $this->dictionary, $this->schema); $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilter); $this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 9443e6f8eb5..f972bf8480c 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -50,6 +50,8 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); dump($sql = $qb->getSQL()); + + $parameters = $qb->getParameters(); foreach($parameters as $parameter=>$value) { $sql = str_replace(':' . $parameter, $value, $sql); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 41cbaad1361..be0e00b7c2d 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -23,8 +23,7 @@ use Mautic\LeadBundle\Segment\LeadSegmentFilters; use Mautic\LeadBundle\Segment\RandomParameterName; -class LeadSegmentQueryBuilder -{ +class LeadSegmentQueryBuilder { /** @var EntityManager */ private $entityManager; @@ -33,24 +32,13 @@ class LeadSegmentQueryBuilder private $tableAliases = []; - /** @var LeadSegmentFilterDescriptor */ - private $translator; - /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ private $schema; - public function __construct( - EntityManager $entityManager, - RandomParameterName $randomParameterName, - LeadSegmentFilterDescriptor $translator - ) - { + public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName) { $this->entityManager = $entityManager; $this->randomParameterName = $randomParameterName; - $this->schema = $this->entityManager->getConnection() - ->getSchemaManager() - ; - $this->translator = $translator; + $this->schema = $this->entityManager->getConnection()->getSchemaManager(); //@todo Will be generate automatically, just as POC $this->tableAliases['leads'] = 'l'; @@ -58,26 +46,12 @@ public function __construct( $this->tableAliases['page_hits'] = 'ph'; } -//object(Mautic\LeadBundle\Segment\LeadSegmentFilter)[841] -//private 'glue' => string 'and' (length=3) -//private 'field' => string 'lead_email_read_date' (length=20) -//private 'object' => string 'lead' (length=4) -//private 'type' => string 'datetime' (length=8) -//private 'filter' => string '2017-09-10 23:14' (length=16) -//private 'display' => null -//private 'operator' => string 'gt' (length=2) -//private 'func' => string 'gt' (length=2) - - public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) - { + + public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { /** @var QueryBuilder $qb */ - $qb = $this->entityManager->getConnection() - ->createQueryBuilder() - ; + $qb = $this->entityManager->getConnection()->createQueryBuilder(); - $qb->select('*') - ->from('MauticLeadBundle:Lead', 'l') - ; + $qb->select('*')->from('MauticLeadBundle:Lead', 'l'); foreach ($leadSegmentFilters as $filter) { $qb = $this->getQueryPart($filter, $qb); @@ -92,33 +66,16 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters // Leads that do not have any record in the lead_lists_leads table for this lead list // For non null fields - it's apparently better to use left join over not exists due to not using nullable // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ - $listOnExpr = $q->expr() - ->andX( - $q->expr() - ->eq('ll.leadlist_id', $id), - $q->expr() - ->eq('ll.lead_id', 'l.id') - ) - ; + $listOnExpr = $q->expr()->andX($q->expr()->eq('ll.leadlist_id', $id), $q->expr()->eq('ll.lead_id', 'l.id')); if (!empty($batchLimiters['dateTime'])) { // Only leads in the list at the time of count - $listOnExpr->add( - $q->expr() - ->lte('ll.date_added', $q->expr() - ->literal($batchLimiters['dateTime'])) - ); + $listOnExpr->add($q->expr()->lte('ll.date_added', $q->expr()->literal($batchLimiters['dateTime']))); } - $q->leftJoin( - 'l', - MAUTIC_TABLE_PREFIX . 'lead_lists_leads', - 'll', - $listOnExpr - ); + $q->leftJoin('l', MAUTIC_TABLE_PREFIX . 'lead_lists_leads', 'll', $listOnExpr); - $expr->add($q->expr() - ->isNull('ll.lead_id')); + $expr->add($q->expr()->isNull('ll.lead_id')); if ($batchExpr->count()) { $expr->add($batchExpr); @@ -129,9 +86,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters } if (!empty($limit)) { - $q->setFirstResult($start) - ->setMaxResults($limit) - ; + $q->setFirstResult($start)->setMaxResults($limit); } // remove any possible group by @@ -141,16 +96,11 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters echo 'SQL parameters:'; dump($q->getParameters()); - $results = $q->execute() - ->fetchAll() - ; + $results = $q->execute()->fetchAll(); $leads = []; foreach ($results as $r) { - $leads = [ - 'count' => $r['lead_count'], - 'maxId' => $r['max_id'], - ]; + $leads = ['count' => $r['lead_count'], 'maxId' => $r['max_id'],]; if ($withMinId) { $leads['minId'] = $r['min_id']; } @@ -159,8 +109,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters return $leads; } - private function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) - { + private function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) { $translated = $filter->getQueryDescription($this->getTranslator()); $parameterHolder = $this->generateRandomParameterName(); @@ -169,43 +118,15 @@ private function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filte if (isset($translated) && $translated) { if (isset($translated['func'])) { //@todo rewrite with getFullQualifiedName - $qb->leftJoin( - $this->tableAliases[$translated['table']], - $translated['foreign_table'], - $this->tableAliases[$translated['foreign_table']], - sprintf('%s.%s = %s.%s', - $this->tableAliases[$translated['table']], - $translated['table_field'], - $this->tableAliases[$translated['foreign_table']], - $translated['foreign_table_field'] - ) - ); + $qb->leftJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'])); //@todo rewrite with getFullQualifiedName - $qb->andHaving( - isset($translated['func']) - ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], - $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) - : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], - $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn)) - ); + $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); } else { //@todo rewrite with getFullQualifiedName - $qb->innerJoin( - $this->tableAliases[$translated['table']], - $translated['foreign_table'], - $this->tableAliases[$translated['foreign_table']], - sprintf('%s.%s = %s.%s and %s', - $this->tableAliases[$translated['table']], - $translated['table_field'], - $this->tableAliases[$translated['foreign_table']], - $translated['foreign_table_field'], - sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], - $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) - ) - ); + $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); } @@ -215,125 +136,86 @@ private function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filte } } - private function addLeadListQuery() { - $table = 'lead_lists_leads'; - $column = 'leadlist_id'; - $falseParameter = $this->generateRandomParameterName(); - $parameters[$falseParameter] = false; - $trueParameter = $this->generateRandomParameterName(); - $parameters[$trueParameter] = true; - $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; - $ignoreAutoFilter = true; - - if ($filterListIds = (array)$leadSegmentFilter->getFilter()) { - $listQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l') - ; - $listQb->where( - $listQb->expr() - ->in('l.id', $filterListIds) - ); - $filterLists = $listQb->execute() - ->fetchAll() - ; - $not = 'NOT EXISTS' === $func; - - // Each segment's filters must be appended as ORs so that each list is evaluated individually - $existsExpr = $not ? $listQb->expr() - ->andX() : $listQb->expr() - ->orX() - ; - foreach ($filterLists as $list) { - $alias = $this->generateRandomParameterName(); - $id = (int)$list['id']; - if ($id === (int)$listId) { - // Ignore as somehow self is included in the list - continue; - } - - $listFilters = unserialize($list['filters']); - if (empty($listFilters)) { - // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list - $subQb = $this->createFilterExpressionSubQuery( - $table, - $alias, - $column, - $id, - $parameters, - [ - $alias . '.manually_removed' => $falseParameter, - ] - ); - } - else { - // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet - // but also leverage the current membership to take into account those manually added or removed from the segment - // Build a "live" query based on current filters to catch those that have not been processed yet - $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); - $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); + /** + * @todo this is just copy of existing as template, not a real function + */ + private function addLeadListQuery($filter) { + $table = 'lead_lists_leads'; + $column = 'leadlist_id'; + $falseParameter = $this->generateRandomParameterName(); + $parameters[$falseParameter] = false; + $trueParameter = $this->generateRandomParameterName(); + $parameters[$trueParameter] = true; + $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; + $ignoreAutoFilter = true; + + if ($filterListIds = (array)$filter->getFilter()) { + $listQb = $this->entityManager->getConnection()->createQueryBuilder()->select('l.id, l.filters') + ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l'); + $listQb->where($listQb->expr()->in('l.id', $filterListIds)); + $filterLists = $listQb->execute()->fetchAll(); + $not = 'NOT EXISTS' === $func; + + // Each segment's filters must be appended as ORs so that each list is evaluated individually + $existsExpr = $not ? $listQb->expr()->andX() : $listQb->expr()->orX(); + + foreach ($filterLists as $list) { + $alias = $this->generateRandomParameterName(); + $id = (int)$list['id']; + if ($id === (int)$listId) { + // Ignore as somehow self is included in the list + continue; + } - // Left join membership to account for manually added and removed - $membershipAlias = $this->generateRandomParameterName(); - $subQb->leftJoin( - $alias, - MAUTIC_TABLE_PREFIX . $table, - $membershipAlias, - "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" - ) - ->where( - $subQb->expr() - ->orX( - $filterExpr, - $subQb->expr() - ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ) - ) - ->andWhere( - $subQb->expr() - ->eq("$alias.id", 'l.id'), - $subQb->expr() - ->orX( - $subQb->expr() - ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr() - ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - ) - ) - ; - } + $listFilters = unserialize($list['filters']); + if (empty($listFilters)) { + // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list + $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $id, $parameters, [$alias . '.manually_removed' => $falseParameter,]); + } + else { + // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet + // but also leverage the current membership to take into account those manually added or removed from the segment + + // Build a "live" query based on current filters to catch those that have not been processed yet + $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); + $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); + + // Left join membership to account for manually added and removed + $membershipAlias = $this->generateRandomParameterName(); + $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX . $table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") + ->where($subQb->expr()->orX($filterExpr, $subQb->expr() + ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added + ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr()->orX($subQb->expr() + ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr() + ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + )); + } - $existsExpr->add( - sprintf('%s (%s)', $func, $subQb->getSQL()) - ); - } + $existsExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); + } - if ($existsExpr->count()) { - $groupExpr->add($existsExpr); - } - } + if ($existsExpr->count()) { + $groupExpr->add($existsExpr); + } + } - break; + break; } - private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) - { + private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $parameters = []; //@todo cache metadata $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) - ->getTableName() - ; + ->getTableName(); /** @var Column $dbColumn */ - $dbColumn = isset($this->schema->listTableColumns($tableName)[$filter->getField()]) - ? $this->schema->listTableColumns($tableName)[$filter->getField()] - : false; + $dbColumn = isset($this->schema->listTableColumns($tableName)[$filter->getField()]) ? $this->schema->listTableColumns($tableName)[$filter->getField()] : false; if ($dbColumn) { @@ -342,6 +224,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) else { $translated = $filter->getQueryDescription(); + var_dump($filter); + if (!$translated) { var_dump('Unknown field: ' . $filter->getField()); var_dump($filter); @@ -354,7 +238,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) case 'foreign_aggr': $this->addForeignTableQuery($qb, $filter); break; - } + } } @@ -362,29 +246,29 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) return $qb; -// //the next one will determine the group -// if ($leadSegmentFilter->getGlue() === 'or') { -// // Create a new group of andX expressions -// if ($groupExpr->count()) { -// $groups[] = $groupExpr; -// $groupExpr = $q->expr() -// ->andX() -// ; -// } -// } + // //the next one will determine the group + // if ($leadSegmentFilter->getGlue() === 'or') { + // // Create a new group of andX expressions + // if ($groupExpr->count()) { + // $groups[] = $groupExpr; + // $groupExpr = $q->expr() + // ->andX() + // ; + // } + // } $parameterName = $this->generateRandomParameterName(); //@todo what is this? -// $ignoreAutoFilter = false; -// -// $func = $filter->getFunc(); -// -// // Generate a unique alias -// $alias = $this->generateRandomParameterName(); -// -// var_dump($func . ":" . $leadSegmentFilter->getField()); -// var_dump($exprParameter); + // $ignoreAutoFilter = false; + // + // $func = $filter->getFunc(); + // + // // Generate a unique alias + // $alias = $this->generateRandomParameterName(); + // + // var_dump($func . ":" . $leadSegmentFilter->getField()); + // var_dump($exprParameter); switch ($leadSegmentFilter->getField()) { @@ -393,18 +277,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) case 'source': case 'source_id': case 'url_title': - $operand = in_array( - $func, - [ - 'eq', - 'like', - 'regexp', - 'notRegexp', - 'startsWith', - 'endsWith', - 'contains', - ] - ) ? 'EXISTS' : 'NOT EXISTS'; + $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp', 'startsWith', 'endsWith', 'contains',]) ? 'EXISTS' : 'NOT EXISTS'; $ignoreAutoFilter = true; $column = $leadSegmentFilter->getField(); @@ -413,38 +286,23 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $column = 'url'; } - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX . 'page_hits', $alias) - ; + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') + ->from(MAUTIC_TABLE_PREFIX . 'page_hits', $alias); switch ($func) { case 'eq': case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.' . $column, $exprParameter), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->eq($alias . '.' . $column, $exprParameter), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); break; case 'regexp': case 'notRegexp': $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); $not = ($func === 'notRegexp') ? ' NOT' : ''; - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.lead_id', 'l.id'), - $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->eq($alias . '.lead_id', 'l.id'), $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter)); break; case 'like': case 'notLike': @@ -465,15 +323,9 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) break; } - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->like($alias . '.' . $column, $exprParameter), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->like($alias . '.' . $column, $exprParameter), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); break; } @@ -484,50 +336,29 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp']) ? 'EXISTS' : 'NOT EXISTS'; $column = $leadSegmentFilter->getField(); - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX . 'lead_devices', $alias) - ; + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') + ->from(MAUTIC_TABLE_PREFIX . 'lead_devices', $alias); switch ($func) { case 'eq': case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.' . $column, $exprParameter), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->eq($alias . '.' . $column, $exprParameter), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); break; case 'like': case '!like': $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->like($alias . '.' . $column, $exprParameter), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->like($alias . '.' . $column, $exprParameter), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); break; case 'regexp': case 'notRegexp': $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); $not = ($func === 'notRegexp') ? ' NOT' : ''; - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.lead_id', 'l.id'), - $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->eq($alias . '.lead_id', 'l.id'), $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter)); break; } @@ -550,11 +381,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) var_dump($func); } - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX . $table, $alias) - ; + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') + ->from(MAUTIC_TABLE_PREFIX . $table, $alias); switch ($func) { @@ -562,15 +390,9 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.' . $column, $exprParameter), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->eq($alias . '.' . $column, $exprParameter), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); break; case 'between': case 'notBetween': @@ -583,30 +405,16 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $field = $column; if ($func === 'between') { - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->gte($alias . '.' . $field, $exprParameter), - $q->expr() - ->lt($alias . '.' . $field, $exprParameter2), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->gte($alias . '.' . $field, $exprParameter), $q->expr() + ->lt($alias . '.' . $field, $exprParameter2), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); } else { - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->lt($alias . '.' . $field, $exprParameter), - $q->expr() - ->gte($alias . '.' . $field, $exprParameter2), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->lt($alias . '.' . $field, $exprParameter), $q->expr() + ->gte($alias . '.' . $field, $exprParameter2), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); } break; default: @@ -617,18 +425,9 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) } $parameters[$parameter2] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->$func( - $alias . '.' . $column, - $parameter2 - ), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr() + ->$func($alias . '.' . $column, $parameter2), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); break; } $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); @@ -647,33 +446,16 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $column = 'id'; } - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX . $table, $alias) - ; + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) + ->from(MAUTIC_TABLE_PREFIX . $table, $alias); if ($leadSegmentFilter->getFilter() == 1) { - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->isNotNull($alias . '.' . $column), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr()->isNotNull($alias . '.' . $column), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); } else { - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->isNull($alias . '.' . $column), - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr()->isNull($alias . '.' . $column), $q->expr() + ->eq($alias . '.lead_id', 'l.id'))); } $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); @@ -682,45 +464,22 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $operand = 'EXISTS'; $table = 'page_hits'; $select = 'COUNT(id)'; - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX . $table, $alias) - ; + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) + ->from(MAUTIC_TABLE_PREFIX . $table, $alias); $alias2 = $this->generateRandomParameterName(); - $subqb2 = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($alias2 . '.id') - ->from(MAUTIC_TABLE_PREFIX . $table, $alias2) - ; - - $subqb2->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias2 . '.lead_id', 'l.id'), - $q->expr() - ->gt($alias2 . '.date_hit', '(' . $alias . '.date_hit - INTERVAL 30 MINUTE)'), - $q->expr() - ->lt($alias2 . '.date_hit', $alias . '.date_hit') - ) - ); + $subqb2 = $this->entityManager->getConnection()->createQueryBuilder()->select($alias2 . '.id') + ->from(MAUTIC_TABLE_PREFIX . $table, $alias2); + + $subqb2->where($q->expr()->andX($q->expr()->eq($alias2 . '.lead_id', 'l.id'), $q->expr() + ->gt($alias2 . '.date_hit', '(' . $alias . '.date_hit - INTERVAL 30 MINUTE)'), $q->expr() + ->lt($alias2 . '.date_hit', $alias . '.date_hit'))); $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.lead_id', 'l.id'), - $q->expr() - ->isNull($alias . '.email_id'), - $q->expr() - ->isNull($alias . '.redirect_id'), - sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()) - ) - ); + $subqb->where($q->expr()->andX($q->expr()->eq($alias . '.lead_id', 'l.id'), $q->expr() + ->isNull($alias . '.email_id'), $q->expr() + ->isNull($alias . '.redirect_id'), sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()))); $opr = ''; switch ($func) { @@ -755,20 +514,11 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $table = 'email_stats'; $select = 'COALESCE(SUM(open_count),0)'; } - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX . $table, $alias) - ; + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) + ->from(MAUTIC_TABLE_PREFIX . $table, $alias); $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.lead_id', 'l.id') - ) - ); + $subqb->where($q->expr()->andX($q->expr()->eq($alias . '.lead_id', 'l.id'))); $opr = ''; switch ($func) { @@ -812,26 +562,14 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) } $channelParameter = $this->generateRandomParameterName(); - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') + $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') ->from(MAUTIC_TABLE_PREFIX . 'lead_donotcontact', $alias) - ->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.reason', $exprParameter), - $q->expr() - ->eq($alias . '.lead_id', 'l.id'), - $q->expr() - ->eq($alias . '.channel', ":$channelParameter") - ) - ) - ; - - $groupExpr->add( - sprintf('%s (%s)', $func, $subqb->getSQL()) - ); + ->where($q->expr()->andX($q->expr() + ->eq($alias . '.reason', $exprParameter), $q->expr() + ->eq($alias . '.lead_id', 'l.id'), $q->expr() + ->eq($alias . '.channel', ":$channelParameter"))); + + $groupExpr->add(sprintf('%s (%s)', $func, $subqb->getSQL())); // Filter will always be true and differentiated via EXISTS/NOT EXISTS $leadSegmentFilter->setFilter(true); @@ -854,25 +592,14 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $ignoreAutoFilter = true; if ($filterListIds = (array)$leadSegmentFilter->getFilter()) { - $listQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l') - ; - $listQb->where( - $listQb->expr() - ->in('l.id', $filterListIds) - ); - $filterLists = $listQb->execute() - ->fetchAll() - ; + $listQb = $this->entityManager->getConnection()->createQueryBuilder()->select('l.id, l.filters') + ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l'); + $listQb->where($listQb->expr()->in('l.id', $filterListIds)); + $filterLists = $listQb->execute()->fetchAll(); $not = 'NOT EXISTS' === $func; // Each segment's filters must be appended as ORs so that each list is evaluated individually - $existsExpr = $not ? $listQb->expr() - ->andX() : $listQb->expr() - ->orX() - ; + $existsExpr = $not ? $listQb->expr()->andX() : $listQb->expr()->orX(); foreach ($filterLists as $list) { $alias = $this->generateRandomParameterName(); @@ -885,16 +612,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $listFilters = unserialize($list['filters']); if (empty($listFilters)) { // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list - $subQb = $this->createFilterExpressionSubQuery( - $table, - $alias, - $column, - $id, - $parameters, - [ - $alias . '.manually_removed' => $falseParameter, - ] - ); + $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $id, $parameters, [$alias . '.manually_removed' => $falseParameter,]); } else { // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet @@ -906,37 +624,18 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) // Left join membership to account for manually added and removed $membershipAlias = $this->generateRandomParameterName(); - $subQb->leftJoin( - $alias, - MAUTIC_TABLE_PREFIX . $table, - $membershipAlias, - "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" - ) - ->where( - $subQb->expr() - ->orX( - $filterExpr, - $subQb->expr() - ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ) - ) - ->andWhere( - $subQb->expr() - ->eq("$alias.id", 'l.id'), - $subQb->expr() - ->orX( - $subQb->expr() - ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr() - ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - ) - ) - ; + $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX . $table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") + ->where($subQb->expr()->orX($filterExpr, $subQb->expr() + ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added + ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr() + ->orX($subQb->expr() + ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr() + ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + )); } - $existsExpr->add( - sprintf('%s (%s)', $func, $subQb->getSQL()) - ); + $existsExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); } if ($existsExpr->count()) { @@ -994,18 +693,9 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) break; } - $subQb = $this->createFilterExpressionSubQuery( - $table, - $alias, - $column, - $leadSegmentFilter->getFilter(), - $parameters, - $subQueryFilters - ); - - $groupExpr->add( - sprintf('%s (%s)', $func, $subQb->getSQL()) - ); + $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $leadSegmentFilter->getFilter(), $parameters, $subQueryFilters); + + $groupExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); break; case 'stage': // A note here that SQL EXISTS is being used for the eq and neq cases. @@ -1013,51 +703,28 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) // for every row in the outer query's table. This might have to be refactored later on // if performance is desired. - $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX . 'stages', $alias) - ; + $subQb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') + ->from(MAUTIC_TABLE_PREFIX . 'stages', $alias); switch ($func) { case 'empty': - $groupExpr->add( - $q->expr() - ->isNull('l.stage_id') - ); + $groupExpr->add($q->expr()->isNull('l.stage_id')); break; case 'notEmpty': - $groupExpr->add( - $q->expr() - ->isNotNull('l.stage_id') - ); + $groupExpr->add($q->expr()->isNotNull('l.stage_id')); break; case 'eq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subQb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.id', 'l.stage_id'), - $q->expr() - ->eq($alias . '.id', ":$parameter") - ) - ); + $subQb->where($q->expr()->andX($q->expr()->eq($alias . '.id', 'l.stage_id'), $q->expr() + ->eq($alias . '.id', ":$parameter"))); $groupExpr->add(sprintf('EXISTS (%s)', $subQb->getSQL())); break; case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subQb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.id', 'l.stage_id'), - $q->expr() - ->eq($alias . '.id', ":$parameter") - ) - ); + $subQb->where($q->expr()->andX($q->expr()->eq($alias . '.id', 'l.stage_id'), $q->expr() + ->eq($alias . '.id', ":$parameter"))); $groupExpr->add(sprintf('NOT EXISTS (%s)', $subQb->getSQL())); break; } @@ -1068,11 +735,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $operand = in_array($func, ['eq', 'neq']) ? 'EXISTS' : 'NOT EXISTS'; $ignoreAutoFilter = true; - $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX . 'integration_entity', $alias) - ; + $subQb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') + ->from(MAUTIC_TABLE_PREFIX . 'integration_entity', $alias); switch ($func) { case 'eq': case 'neq': @@ -1087,21 +751,12 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $parameters[$parameter] = $campaignId; $parameters[$parameter2] = $integrationName; - $subQb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias . '.integration', ":$parameter2"), - $q->expr() - ->eq($alias . '.integration_entity', "'CampaignMember'"), - $q->expr() - ->eq($alias . '.integration_entity_id', ":$parameter"), - $q->expr() - ->eq($alias . '.internal_entity', "'lead'"), - $q->expr() - ->eq($alias . '.internal_entity_id', 'l.id') - ) - ); + $subQb->where($q->expr()->andX($q->expr() + ->eq($alias . '.integration', ":$parameter2"), $q->expr() + ->eq($alias . '.integration_entity', "'CampaignMember'"), $q->expr() + ->eq($alias . '.integration_entity_id', ":$parameter"), $q->expr() + ->eq($alias . '.internal_entity', "'lead'"), $q->expr() + ->eq($alias . '.internal_entity_id', 'l.id'))); break; } @@ -1125,59 +780,32 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $ignoreAutoFilter = true; if ($func === 'between') { - $groupExpr->add( - $q->expr() - ->andX( - $q->expr() - ->gte($field, $exprParameter), - $q->expr() - ->lt($field, $exprParameter2) - ) - ); + $groupExpr->add($q->expr()->andX($q->expr()->gte($field, $exprParameter), $q->expr() + ->lt($field, $exprParameter2))); } else { - $groupExpr->add( - $q->expr() - ->andX( - $q->expr() - ->lt($field, $exprParameter), - $q->expr() - ->gte($field, $exprParameter2) - ) - ); + $groupExpr->add($q->expr()->andX($q->expr()->lt($field, $exprParameter), $q->expr() + ->gte($field, $exprParameter2))); } break; case 'notEmpty': - $groupExpr->add( - $q->expr() - ->andX( - $q->expr() - ->isNotNull($field), - $q->expr() - ->neq($field, $q->expr() - ->literal('')) - ) - ); + $groupExpr->add($q->expr()->andX($q->expr()->isNotNull($field), $q->expr() + ->neq($field, $q->expr() + ->literal('')))); $ignoreAutoFilter = true; break; case 'empty': $leadSegmentFilter->setFilter(''); - $groupExpr->add( - $this->generateFilterExpression($q, $field, 'eq', $exprParameter, true) - ); + $groupExpr->add($this->generateFilterExpression($q, $field, 'eq', $exprParameter, true)); break; case 'in': case 'notIn': $cleanFilter = []; foreach ($leadSegmentFilter->getFilter() as $key => $value) { - $cleanFilter[] = $q->expr() - ->literal( - InputHelper::clean($value) - ) - ; + $cleanFilter[] = $q->expr()->literal(InputHelper::clean($value)); } $leadSegmentFilter->setFilter($cleanFilter); @@ -1192,23 +820,17 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $operator = 'REGEXP'; } - $groupExpr->add( - $field . " $operator '\\\\|?$filter\\\\|?'" - ); + $groupExpr->add($field . " $operator '\\\\|?$filter\\\\|?'"); } } else { - $groupExpr->add( - $this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null) - ); + $groupExpr->add($this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null)); } $ignoreAutoFilter = true; break; case 'neq': - $groupExpr->add( - $this->generateFilterExpression($q, $field, $func, $exprParameter, null) - ); + $groupExpr->add($this->generateFilterExpression($q, $field, $func, $exprParameter, null)); break; case 'like': @@ -1221,8 +843,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) switch ($func) { case 'like': case 'notLike': - $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%' . $leadSegmentFilter->getFilter() . '%' - : $leadSegmentFilter->getFilter(); + $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%' . $leadSegmentFilter->getFilter() . '%' : $leadSegmentFilter->getFilter(); break; case 'startsWith': $func = 'like'; @@ -1238,24 +859,19 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) break; } - $groupExpr->add( - $this->generateFilterExpression($q, $field, $func, $exprParameter, null) - ); + $groupExpr->add($this->generateFilterExpression($q, $field, $func, $exprParameter, null)); break; case 'regexp': case 'notRegexp': $ignoreAutoFilter = true; $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); $not = ($func === 'notRegexp') ? ' NOT' : ''; - $groupExpr->add( - // Escape single quotes while accounting for those that may already be escaped - $field . $not . ' REGEXP ' . $exprParameter - ); + $groupExpr->add(// Escape single quotes while accounting for those that may already be escaped + $field . $not . ' REGEXP ' . $exprParameter); break; default: $ignoreAutoFilter = true; - $groupExpr->add($q->expr() - ->$func($field, $exprParameter)); + $groupExpr->add($q->expr()->$func($field, $exprParameter)); $parameters[$exprParameter] = $leadSegmentFilter->getFilter(); } } @@ -1283,15 +899,11 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) } elseif (count($groups) > 1) { // Sets of expressions grouped by OR - $orX = $q->expr() - ->orX() - ; + $orX = $q->expr()->orX(); $orX->addMultiple($groups); // Wrap in a andX for other functions to append - $expr = $q->expr() - ->andX($orX) - ; + $expr = $q->expr()->andX($orX); } else { $expr = $groupExpr; @@ -1317,22 +929,20 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) * * @return string */ - private function generateRandomParameterName() - { + private function generateRandomParameterName() { return $this->randomParameterName->generateRandomParameterName(); } /** * @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q - * @param $column - * @param $operator - * @param $parameter - * @param $includeIsNull true/false or null to auto determine based on operator + * @param $column + * @param $operator + * @param $parameter + * @param $includeIsNull true/false or null to auto determine based on operator * * @return mixed */ - public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) - { + public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) { // in/notIn for dbal will use a raw array if (!is_array($parameter) && strpos($parameter, ':') !== 0) { $parameter = ":$parameter"; @@ -1344,19 +954,10 @@ public function generateFilterExpression($q, $column, $operator, $parameter, $in } if ($includeIsNull) { - $expr = $q->expr() - ->orX( - $q->expr() - ->$operator($column, $parameter), - $q->expr() - ->isNull($column) - ) - ; + $expr = $q->expr()->orX($q->expr()->$operator($column, $parameter), $q->expr()->isNull($column)); } else { - $expr = $q->expr() - ->$operator($column, $parameter) - ; + $expr = $q->expr()->$operator($column, $parameter); } return $expr; @@ -1368,32 +969,21 @@ public function generateFilterExpression($q, $column, $operator, $parameter, $in * @param $column * @param $value * @param array $parameters - * @param null $leadId + * @param null $leadId * @param array $subQueryFilters * * @return QueryBuilder */ - protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) - { - $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ; - $subExpr = $subQb->expr() - ->andX() - ; + protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) { + $subQb = $this->entityManager->getConnection()->createQueryBuilder(); + $subExpr = $subQb->expr()->andX(); if ('leads' !== $table) { - $subExpr->add( - $subQb->expr() - ->eq($alias . '.lead_id', 'l.id') - ); + $subExpr->add($subQb->expr()->eq($alias . '.lead_id', 'l.id')); } foreach ($subQueryFilters as $subColumn => $subParameter) { - $subExpr->add( - $subQb->expr() - ->eq($subColumn, ":$subParameter") - ); + $subExpr->add($subQb->expr()->eq($subColumn, ":$subParameter")); } if (null !== $value && !empty($column)) { @@ -1401,26 +991,17 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu $subFunc = 'eq'; if (is_array($value)) { $subFunc = 'in'; - $subExpr->add( - $subQb->expr() - ->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") - ); + $subExpr->add($subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter")); $parameters[$subFilterParamter] = ['value' => $value, 'type' => \Doctrine\DBAL\Connection::PARAM_STR_ARRAY]; } else { $parameters[$subFilterParamter] = $value; } - $subExpr->add( - $subQb->expr() - ->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") - ); + $subExpr->add($subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter")); } - $subQb->select('null') - ->from(MAUTIC_TABLE_PREFIX . $table, $alias) - ->where($subExpr) - ; + $subQb->select('null')->from(MAUTIC_TABLE_PREFIX . $table, $alias)->where($subExpr); return $subQb; } @@ -1429,29 +1010,21 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. * - * @param QueryBuilder $q + * @param QueryBuilder $q * @param LeadSegmentFilters $leadSegmentFilters */ - private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) - { + private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) { $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; // Join company tables for query optimization $q->$joinType('l', MAUTIC_TABLE_PREFIX . 'companies_leads', 'cl', 'l.id = cl.lead_id') - ->$joinType( - 'cl', - MAUTIC_TABLE_PREFIX . 'companies', - 'comp', - 'cl.company_id = comp.id' - ) - ; + ->$joinType('cl', MAUTIC_TABLE_PREFIX . 'companies', 'comp', 'cl.company_id = comp.id'); // Return only unique contacts $q->groupBy('l.id'); } - private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) - { + private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) { var_dump(debug_backtrace()[1]['function']); $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); @@ -1465,17 +1038,16 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter /** * @return LeadSegmentFilterDescriptor */ - public function getTranslator() - { + public function getTranslator() { return $this->translator; } /** * @param LeadSegmentFilterDescriptor $translator + * * @return LeadSegmentQueryBuilder */ - public function setTranslator($translator) - { + public function setTranslator($translator) { $this->translator = $translator; return $this; } @@ -1483,17 +1055,16 @@ public function setTranslator($translator) /** * @return \Doctrine\DBAL\Schema\AbstractSchemaManager */ - public function getSchema() - { + public function getSchema() { return $this->schema; } /** * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * * @return LeadSegmentQueryBuilder */ - public function setSchema($schema) - { + public function setSchema($schema) { $this->schema = $schema; return $this; } From 95a0de476acce229c82a2383389bd5da515c56b0 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 10 Jan 2018 13:24:16 +0100 Subject: [PATCH 012/778] workinf POW, try now --- app/bundles/CoreBundle/Config/config.php | 1 + .../Cipher/Symmetric/OpenSSLCipher.php | 46 ++-- .../LeadBundle/Entity/LeadListRepository.php | 1 + .../LeadBundle/Segment/LeadSegmentFilter.php | 121 +++++++++-- .../Segment/LeadSegmentFilterFactory.php | 24 +-- .../LeadBundle/Segment/LeadSegmentService.php | 1 + .../LeadSegmentFilterQueryBuilderTrait.php | 196 ++++++++++++++++++ .../Services/LeadSegmentQueryBuilder.php | 133 ++++++------ composer.lock | 156 ++++++++++---- 9 files changed, 514 insertions(+), 165 deletions(-) create mode 100644 app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php diff --git a/app/bundles/CoreBundle/Config/config.php b/app/bundles/CoreBundle/Config/config.php index 16d4c1f5bb1..61841c9a95f 100644 --- a/app/bundles/CoreBundle/Config/config.php +++ b/app/bundles/CoreBundle/Config/config.php @@ -607,6 +607,7 @@ ], 'mautic.cipher.openssl' => [ 'class' => \Mautic\CoreBundle\Security\Cryptography\Cipher\Symmetric\OpenSSLCipher::class, + 'arguments' => ["%kernel.environment%"] ], 'mautic.factory' => [ 'class' => 'Mautic\CoreBundle\Factory\MauticFactory', diff --git a/app/bundles/CoreBundle/Security/Cryptography/Cipher/Symmetric/OpenSSLCipher.php b/app/bundles/CoreBundle/Security/Cryptography/Cipher/Symmetric/OpenSSLCipher.php index f4e2e3c1576..46ed42b40fd 100644 --- a/app/bundles/CoreBundle/Security/Cryptography/Cipher/Symmetric/OpenSSLCipher.php +++ b/app/bundles/CoreBundle/Security/Cryptography/Cipher/Symmetric/OpenSSLCipher.php @@ -16,11 +16,22 @@ /** * Class OpenSSLCryptography. */ -class OpenSSLCipher implements SymmetricCipherInterface -{ +class OpenSSLCipher implements SymmetricCipherInterface { /** @var string */ private $cipher = 'AES-256-CBC'; + /** @var string */ + private $environment; + + /** + * OpenSSLCipher constructor. + * + * @param string $environment + */ + public function __construct($environment = 'prod') { + $this->environment = $environment; + } + /** * @param string $secretMessage * @param string $key @@ -28,10 +39,9 @@ class OpenSSLCipher implements SymmetricCipherInterface * * @return string */ - public function encrypt($secretMessage, $key, $randomInitVector) - { + public function encrypt($secretMessage, $key, $randomInitVector) { $key = pack('H*', $key); - $data = $secretMessage.$this->getHash($secretMessage, $this->getHashKey($key)); + $data = $secretMessage . $this->getHash($secretMessage, $this->getHashKey($key)); return openssl_encrypt($data, $this->cipher, $key, $options = 0, $randomInitVector); } @@ -45,8 +55,7 @@ public function encrypt($secretMessage, $key, $randomInitVector) * * @throws InvalidDecryptionException */ - public function decrypt($encryptedMessage, $key, $originalInitVector) - { + public function decrypt($encryptedMessage, $key, $originalInitVector) { if (strlen($originalInitVector) !== $this->getInitVectorSize()) { throw new InvalidDecryptionException(); } @@ -55,7 +64,13 @@ public function decrypt($encryptedMessage, $key, $originalInitVector) $sha256Length = 64; $secretMessage = substr($decrypted, 0, -$sha256Length); $originalHash = substr($decrypted, -$sha256Length); - $newHash = $this->getHash($secretMessage, $this->getHashKey($key)); + /** + * This serves dev purposes and allows to operate on imported databases + */ + if ($originalHash === false and $this->environment == 'dev') { + return serialize('dev-invalid-key|' . $secretMessage); + } + $newHash = $this->getHash($secretMessage, $this->getHashKey($key)); if (!hash_equals($originalHash, $newHash)) { throw new InvalidDecryptionException(); } @@ -66,16 +81,14 @@ public function decrypt($encryptedMessage, $key, $originalInitVector) /** * @return string */ - public function getRandomInitVector() - { + public function getRandomInitVector() { return openssl_random_pseudo_bytes($this->getInitVectorSize()); } /** * @return bool */ - public function isSupported() - { + public function isSupported() { if (!extension_loaded('openssl')) { return false; } @@ -87,8 +100,7 @@ public function isSupported() /** * @return int */ - private function getInitVectorSize() - { + private function getInitVectorSize() { return openssl_cipher_iv_length($this->cipher); } @@ -98,8 +110,7 @@ private function getInitVectorSize() * * @return string */ - private function getHash($data, $key) - { + private function getHash($data, $key) { return hash_hmac('sha256', $data, $key); } @@ -108,8 +119,7 @@ private function getHash($data, $key) * * @return string */ - private function getHashKey($binaryKey) - { + private function getHashKey($binaryKey) { $hexKey = bin2hex($binaryKey); // Get second half of hexKey version (stable but different than original key) return substr($hexKey, -32); diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index e5abbcc4ebe..b1aebff0b76 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -660,6 +660,7 @@ protected function generateSegmentExpression(array $filters, array &$parameters, */ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNot = false, $leadId = null, $object = 'lead', $listId = null) { + dump($filters); if (!count($filters)) { return $q->expr()->andX(); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 404c60f62df..99a7be49f6c 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -11,12 +11,19 @@ namespace Mautic\LeadBundle\Segment; +use Doctrine\Common\Persistence\Mapping\MappingException; +use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Column; +use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; +use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; class LeadSegmentFilter { + use LeadSegmentFilterQueryBuilderTrait; + const LEAD_OBJECT = 'lead'; const COMPANY_OBJECT = 'company'; @@ -68,25 +75,23 @@ class LeadSegmentFilter /** @var Column */ private $dbColumn; - private $schema; + /** @var EntityManager */ + private $em; - public function __construct(array $filter, \ArrayIterator $dictionary = null, AbstractSchemaManager $schema = null) + public function __construct(array $filter, \ArrayIterator $dictionary = null, EntityManager $em = null) { - if (is_null($schema)) { - throw new \Exception('No schema'); - } $this->glue = isset($filter['glue']) ? $filter['glue'] : null; $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; $this->display = isset($filter['display']) ? $filter['display'] : null; - + $this->func = isset($filter['func']) ? $filter['func'] : null; $operatorValue = isset($filter['operator']) ? $filter['operator'] : null; $this->setOperator($operatorValue); $filterValue = isset($filter['filter']) ? $filter['filter'] : null; $this->setFilter($filterValue); - $this->schema = $schema; + $this->em = $em; if (!is_null($dictionary)) { $this->translateQueryDescription($dictionary); } @@ -116,22 +121,106 @@ public function getSQLOperator() } public function getFilterConditionValue($argument = null) { - switch ($this->getType()) { + switch ($this->getDBColumn()->getType()->getName()) { case 'number': + case 'integer': return ":" . $argument; case 'datetime': return sprintf('":%s"', $argument); + case 'string': + switch ($this->getFunc()) { + case 'eq': + case 'ne': + return sprintf("':%s'", $argument); + default: + throw new \Exception('Unknown operator ' . $this->getFunc()); + } default: - var_dump($this->getDBColumn()->getType()); + var_dump($this->getDBColumn()->getType()->getName()); + var_dump($this); die(); } - throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getType())); + var_dump($filter); + throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); + } + + + public function createQuery(QueryBuilder $queryBuilder, $alias = false) { + $glueFunc = $this->getGlue() . 'Where'; + + $parameterName = $this->generateRandomParameterName(); + + $queryBuilder = $queryBuilder->$glueFunc( + $this->createExpression($queryBuilder, $parameterName, $this->getFunc()) + ); + + $queryBuilder->setParameter($parameterName, $this->getFilter()); + + return $queryBuilder; + } + + public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) { + dump('creating expression'); dump($this); + $func = is_null($func) ? $this->getFunc() : $func; + $alias = $this->getTableAlias($this->getEntityName(), $queryBuilder); + if (!$alias) { + $expr = $queryBuilder->expr()->$func($this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $this->generateRandomParameterName(), $expr); + } else { + $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + if ($alias != 'l') { + $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); + + } else { + dump('lead restriction'); + $queryBuilder = $queryBuilder->andWhere($expr); + } + } + + dump($queryBuilder->getQueryParts()); //die(); + + return $queryBuilder; + } + + public function getDBTable() { + //@todo cache metadata + try { + $tableName = $this->em->getClassMetadata($this->getEntityName())->getTableName(); + } catch (MappingException $e) { + return $this->getObject(); + } + + + return $tableName; } + public function getEntityName() { + $converter = new CamelCaseToSnakeCaseNameConverter(); + $entity = sprintf('MauticLeadBundle:%s',ucfirst($converter->denormalize($this->getObject()))); + return $entity; + } + /** + * @return Column + * @throws \Exception + */ public function getDBColumn() { - $dbColumn = $this->schema->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; - var_dump($dbColumn); + if (is_null($this->dbColumn)) { + if($this->getQueryDescription()) { + $this->dbColumn = $this->schema->listTableColumns($this->getDBTable())[$this->queryDescription['field']]; + } else { + $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); + if (!$dbTableColumns) { + throw new \Exception('Unknown database table and no translation provided for type "' . $this->getType() .'"'); + } + if (!isset($dbTableColumns[$this->getField()])) { + throw new \Exception('Unknown database column and no translation provided for type "' . $this->getType() .'"'); + } + $this->dbColumn = $dbTableColumns[$this->getField()]; + } + } + + return $this->dbColumn; } /** @@ -287,7 +376,7 @@ private function sanitizeFilter($filter) public function getQueryDescription($dictionary = null) { if (is_null($this->queryDescription)) { - $this->assembleQueryDescription($dictionary); + $this->translateQueryDescription($dictionary); } return $this->queryDescription; } @@ -305,11 +394,7 @@ public function setQueryDescription($queryDescription) /** * @return $this */ - public function translateQueryDescription(\ArrayIterator $dictionary) { - if (is_null($this->schema)) { - throw new \Exception('You need to pass database schema manager along with dictionary'); - } - + public function translateQueryDescription(\ArrayIterator $dictionary = null) { $this->queryDescription = isset($dictionary[$this->getField()]) ? $dictionary[$this->getField()] : false; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index aff043598c1..0126bbddec2 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -15,8 +15,7 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; -class LeadSegmentFilterFactory -{ +class LeadSegmentFilterFactory { /** * @var LeadSegmentFilterDate */ @@ -27,23 +26,17 @@ class LeadSegmentFilterFactory */ private $leadSegmentFilterOperator; - /** @var LeadSegmentFilterDescriptor */ - private $dictionary; + /** @var LeadSegmentFilterDescriptor */ + public $dictionary; /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ - private $schema; + private $entityManager; - public function __construct( - LeadSegmentFilterDate $leadSegmentFilterDate, - LeadSegmentFilterOperator $leadSegmentFilterOperator, - LeadSegmentFilterDescriptor $dictionary, - EntityManager $entityManager -) - { + public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSegmentFilterOperator $leadSegmentFilterOperator, LeadSegmentFilterDescriptor $dictionary, EntityManager $entityManager) { $this->leadSegmentFilterDate = $leadSegmentFilterDate; $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; $this->dictionary = $dictionary; - $this->schema = $entityManager->getConnection()->getSchemaManager(); + $this->entityManager = $entityManager; } /** @@ -51,13 +44,12 @@ public function __construct( * * @return LeadSegmentFilters */ - public function getLeadListFilters(LeadList $leadList) - { + public function getLeadListFilters(LeadList $leadList) { $leadSegmentFilters = new LeadSegmentFilters(); $filters = $leadList->getFilters(); foreach ($filters as $filter) { - $leadSegmentFilter = new LeadSegmentFilter($filter, $this->dictionary, $this->schema); + $leadSegmentFilter = new LeadSegmentFilter($filter, $this->dictionary, $this->entityManager); $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilter); $this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index f972bf8480c..045d9f29c91 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -49,6 +49,7 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); + $qb = $this->queryBuilder->addLeadListRestrictions($qb, $batchLimiters, $entity->getId(), $this->leadSegmentFilterFactory->dictionary); dump($sql = $qb->getSQL()); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php new file mode 100644 index 00000000000..4ee9dae5e71 --- /dev/null +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -0,0 +1,196 @@ +getTableAliases($queryBuilder); + + if (!in_array($tableEntity, $tables)) { + var_dump(sprintf('table entity ' . $tableEntity . ' not found in "%s"', join(', ', array_keys($tables)))); + } + + return isset($tables[$tableEntity]) ? $tables[$tableEntity] : false; + } + + public function getTableAliases(QueryBuilder $queryBuilder) { + $queryParts = $queryBuilder->getQueryParts(); + $tables = array_reduce($queryParts['from'], function ($result, $item) { + $result[$item['table']] = $item['alias']; + return $result; + }, array()); + + foreach ($queryParts['join'] as $join) { + foreach($join as $joinPart) { + $tables[$joinPart['joinTable']] = $joinPart['joinAlias']; + } + } + var_dump($tables); + var_dump($queryParts['join']); + + return $tables; + } + + /** + * Generate a unique parameter name. + * + * @return string + */ + protected function generateRandomParameterName() + { + $alpha_numeric = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + $paramName = substr(str_shuffle($alpha_numeric), 0, 8); + + if (!in_array($paramName, $this->parameterAliases )) { + $this->parameterAliases[] = $paramName; + + return $paramName; + } + + return $this->generateRandomParameterName(); + } + + // should be used by filter + protected function createJoin(QueryBuilder $queryBuilder, $target, $alias, $joinOn = '', $from = 'MauticLeadBundle:Lead') { + $queryBuilder = $queryBuilder->leftJoin($this->getTableAlias($from, $queryBuilder), $target, $alias, sprintf( + '%s.id = %s.lead_id' . ( $joinOn ? " and $joinOn" : ""), + $this->getTableAlias($from, $queryBuilder), + $alias + )); + + return $queryBuilder; + } + + protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter, $reuseTable = true) { + $filter->createJoin($qb, $alias); + if (isset($translated) && $translated) { + if (isset($translated['func'])) { + //@todo rewrite with getFullQualifiedName + $qb->leftJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'])); + + //@todo rewrite with getFullQualifiedName + $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); + + } + else { + //@todo rewrite with getFullQualifiedName + $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); + } + + + $qb->setParameter($parameterHolder, $filter->getFilter()); + + $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); + } else { + // Default behaviour, translation not necessary + + } + } + + /** + * @param QueryBuilder $qb + * @param $filter + * @param null $alias use alias to extend current query + * + * @throws \Exception + */ + private function addForeignTableQueryWhere(QueryBuilder $qb, $filter, $alias = null) { + dump($filter); + if (is_array($filter)) { + $alias = is_null($alias) ? $this->generateRandomParameterName() : $alias; + foreach ($filter as $singleFilter) { + $qb = $this->addForeignTableQueryWhere($qb, $singleFilter, $alias); + } + return $qb; + } + + $parameterHolder = $this->generateRandomParameterName(); + $qb = $filter->createExpression($qb, $parameterHolder); + + return $qb; + dump($expr); + die(); + + + + //$qb = $qb->andWhere($expr); + $qb->setParameter($parameterHolder, $filter->getFilter()); + + //var_dump($qb->getSQL()); die(); + +// die(); +// +// if (isset($translated) && $translated) { +// if (isset($translated['func'])) { +// //@todo rewrite with getFullQualifiedName +// $qb->leftJoin($this->getTableAlias($filter->get), $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'])); +// +// //@todo rewrite with getFullQualifiedName +// $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); +// +// } +// else { +// //@todo rewrite with getFullQualifiedName +// $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); +// } +// +// +// $qb->setParameter($parameterHolder, $filter->getFilter()); +// +// $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); +// } + } + + protected function getJoinCondition(QueryBuilder $qb, $alias) { + $parts = $qb->getQueryParts(); + foreach ($parts['join']['l'] as $joinedTable) { + if ($joinedTable['joinAlias']==$alias) { + return $joinedTable['joinCondition']; + } + } + throw new \Exception(sprintf('Join alias "%s" doesn\'t exist',$alias)); + } + + protected function addJoinCondition(QueryBuilder $qb, $alias, $expr) { + $result = $parts = $qb->getQueryPart('join'); + + + foreach ($parts['l'] as $key=>$part) { + if ($part['joinAlias'] == $alias) { + $result['l'][$key]['joinCondition'] = $part['joinCondition'] . " and " . $expr; + } + } + + $qb->setQueryPart('join', $result); + dump($qb->getQueryParts()); die(); + return $qb; + } + + protected function replaceJoinCondition(QueryBuilder $qb, $alias, $expr) { + $parts = $qb->getQueryPart('join'); + foreach ($parts['l'] as $key=>$part) { + if ($part['joinAlias']==$alias) { + $parts['l'][$key]['joinCondition'] = $expr; + } + } + + $qb->setQueryPart('join', $parts); + return $qb; + } + +} \ No newline at end of file diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index be0e00b7c2d..2cabfc91c37 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -24,6 +24,8 @@ use Mautic\LeadBundle\Segment\RandomParameterName; class LeadSegmentQueryBuilder { + use LeadSegmentFilterQueryBuilderTrait; + /** @var EntityManager */ private $entityManager; @@ -47,6 +49,36 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r } + public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $leadListId, $dictionary) { + $filter_list_id = new LeadSegmentFilter([ + 'glue' => 'and', + 'field' => 'leadlist_id', + 'object' => 'lead_lists_leads', + 'type' => 'number', + 'filter' => intval($leadListId), + 'operator' => "=", + 'func' => "eq" + ], $dictionary, $this->entityManager); + + $filter_list_added = new LeadSegmentFilter([ + 'glue' => 'and', + 'field' => 'date_added', + 'object' => 'lead_lists_leads', + 'type' => 'date', + 'filter' => $whatever, + 'operator' => "=", + 'func' => "lte" + ], $dictionary, $this->entityManager); + + $queryBuilder = $this->addForeignTableQueryWhere($queryBuilder, [$filter_list_id, $filter_list_added]); + // SELECT count(l.id) as lead_count, max(l.id) as max_id FROM leads l + // LEFT JOIN lead_lists_leads ll ON (ll.leadlist_id = 28) AND (ll.lead_id = l.id) AND (ll.date_added <= '2018-01-09 14:48:54') + // WHERE (l.propertytype = :MglShQLG) AND (ll.lead_id IS NULL) + var_dump($whatever); + return $queryBuilder; + die(); + } + public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { /** @var QueryBuilder $qb */ $qb = $this->entityManager->getConnection()->createQueryBuilder(); @@ -54,6 +86,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters $qb->select('*')->from('MauticLeadBundle:Lead', 'l'); foreach ($leadSegmentFilters as $filter) { + dump($filter); $qb = $this->getQueryPart($filter, $qb); } @@ -109,35 +142,6 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters return $leads; } - private function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) { - $translated = $filter->getQueryDescription($this->getTranslator()); - - $parameterHolder = $this->generateRandomParameterName(); - $dbColumn = $this->schema->listTableColumns($translated['foreign_table'])[$translated['field']]; - - if (isset($translated) && $translated) { - if (isset($translated['func'])) { - //@todo rewrite with getFullQualifiedName - $qb->leftJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'])); - - //@todo rewrite with getFullQualifiedName - $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); - - } - else { - //@todo rewrite with getFullQualifiedName - $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); - } - - - $qb->setParameter($parameterHolder, $filter->getFilter()); - - $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); - } - } - - - /** * @todo this is just copy of existing as template, not a real function */ @@ -187,11 +191,11 @@ private function addLeadListQuery($filter) { $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX . $table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") ->where($subQb->expr()->orX($filterExpr, $subQb->expr() ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr()->orX($subQb->expr() - ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr() - ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - )); + ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr()->orX($subQb->expr() + ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr() + ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + )); } $existsExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); @@ -207,41 +211,36 @@ private function addLeadListQuery($filter) { private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { + var_dump('Asseting query type: ' . $filter->getFilter()); $parameters = []; - //@todo cache metadata + ///** @var Column $dbColumn */ + //$dbColumn = isset($this->schema->listTableColumns($tableName)[$filter->getField()]) ? $this->schema->listTableColumns($tableName)[$filter->getField()] : false; $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) ->getTableName(); + $dbField = $filter->getDBColumn()->getFullQualifiedName(ucfirst($filter->getObject())); - /** @var Column $dbColumn */ - $dbColumn = isset($this->schema->listTableColumns($tableName)[$filter->getField()]) ? $this->schema->listTableColumns($tableName)[$filter->getField()] : false; - - - if ($dbColumn) { - $dbField = $dbColumn->getFullQualifiedName(ucfirst($filter->getObject())); - } - else { - $translated = $filter->getQueryDescription(); - - var_dump($filter); - - if (!$translated) { - var_dump('Unknown field: ' . $filter->getField()); - var_dump($filter); - return $qb; - throw new \Exception('Unknown field: ' . $filter->getField()); - } - - switch ($translated['type']) { - case 'foreign': - case 'foreign_aggr': - $this->addForeignTableQuery($qb, $filter); - break; - } + var_dump($qb->getQueryParts()); - } + $qb = $filter->createQuery($qb); + // if (!$dbField) { + // $translated = $filter->getQueryDescription(); + // + // if (!$translated) { + // $filter->createQuery($qb); + // return $qb; + // throw new \Exception('Unknown field: ' . $filter->getField()); + // } + // + // switch ($translated['type']) { + // case 'foreign': + // case 'foreign_aggr': + // $this->addForeignTableQuery($qb, $filter); + // break; + // } + // } return $qb; @@ -627,12 +626,12 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX . $table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") ->where($subQb->expr()->orX($filterExpr, $subQb->expr() ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr() - ->orX($subQb->expr() - ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr() - ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - )); + ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr() + ->orX($subQb->expr() + ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet + $subQb->expr() + ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed + )); } $existsExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); diff --git a/composer.lock b/composer.lock index 70a8d45c876..9fea11b0e6c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "81292be6ce780f1e60caa2b49b6dd5eb", + "content-hash": "a47e0fb480e38aaf6c896172ccd1b236", "packages": [ { "name": "aws/aws-sdk-php", @@ -722,7 +722,7 @@ "orm", "persistence" ], - "time": "2017-10-04T22:58:25+00:00" + "time": "2017-10-11T19:48:03+00:00" }, { "name": "doctrine/doctrine-cache-bundle", @@ -810,7 +810,7 @@ "cache", "caching" ], - "time": "2017-09-29T14:39:10+00:00" + "time": "2017-10-12T17:23:29+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", @@ -1244,7 +1244,7 @@ "database", "orm" ], - "time": "2017-09-18T06:50:20+00:00" + "time": "2017-10-23T18:21:04+00:00" }, { "name": "egeloen/ordered-form", @@ -1615,7 +1615,7 @@ "geolocation", "maxmind" ], - "time": "2017-07-10T17:59:43+00:00" + "time": "2017-10-27T19:20:22+00:00" }, { "name": "giggsey/libphonenumber-for-php", @@ -1683,7 +1683,7 @@ "phonenumber", "validation" ], - "time": "2017-10-04T10:08:33+00:00" + "time": "2017-10-16T14:06:44+00:00" }, { "name": "giggsey/locale", @@ -2364,7 +2364,7 @@ "serialization", "xml" ], - "time": "2017-09-28T15:17:28+00:00" + "time": "2017-10-27T07:15:54+00:00" }, { "name": "jms/serializer-bundle", @@ -3127,7 +3127,7 @@ "geolocation", "maxmind" ], - "time": "2017-01-19T23:49:38+00:00" + "time": "2017-10-27T19:15:33+00:00" }, { "name": "maxmind/web-service-common", @@ -3483,7 +3483,7 @@ "plupload", "upload" ], - "time": "2017-09-19T09:38:39+00:00" + "time": "2017-10-10T08:09:38+00:00" }, { "name": "paragonie/random_compat", @@ -4848,7 +4848,7 @@ } ], "description": "A security checker for your composer.lock", - "time": "2017-08-22T22:18:16+00:00" + "time": "2017-10-29T18:48:08+00:00" }, { "name": "simshaun/recurr", @@ -5281,7 +5281,7 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2017-07-12T12:59:33+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/cache", @@ -5351,7 +5351,7 @@ "caching", "psr6" ], - "time": "2017-09-03T14:06:51+00:00" + "time": "2017-10-04T07:58:49+00:00" }, { "name": "symfony/class-loader", @@ -5404,7 +5404,7 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2017-07-05T06:50:35+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/config", @@ -5460,7 +5460,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:07:15+00:00" + "time": "2017-10-04T18:56:36+00:00" }, { "name": "symfony/console", @@ -5521,7 +5521,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-08-27T14:29:03+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/debug", @@ -5578,7 +5578,7 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-08-27T14:29:03+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/dependency-injection", @@ -5641,7 +5641,7 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2017-08-10T14:42:21+00:00" + "time": "2017-10-02T07:17:52+00:00" }, { "name": "symfony/doctrine-bridge", @@ -5718,7 +5718,7 @@ ], "description": "Symfony Doctrine Bridge", "homepage": "https://symfony.com", - "time": "2017-07-22T16:46:29+00:00" + "time": "2017-10-02T08:46:46+00:00" }, { "name": "symfony/dom-crawler", @@ -5774,7 +5774,7 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2017-08-15T13:30:53+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/dotenv", @@ -5891,7 +5891,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-06-02T07:47:27+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/expression-language", @@ -5940,7 +5940,7 @@ ], "description": "Symfony ExpressionLanguage Component", "homepage": "https://symfony.com", - "time": "2017-06-01T20:52:29+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/filesystem", @@ -5989,7 +5989,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-07-11T07:12:11+00:00" + "time": "2017-10-02T08:46:46+00:00" }, { "name": "symfony/finder", @@ -6038,7 +6038,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-06-01T20:52:29+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/form", @@ -6113,7 +6113,7 @@ ], "description": "Symfony Form Component", "homepage": "https://symfony.com", - "time": "2017-07-28T15:21:22+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/framework-bundle", @@ -6210,7 +6210,7 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "time": "2017-07-05T06:32:23+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/http-foundation", @@ -6265,7 +6265,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-08-10T07:04:10+00:00" + "time": "2017-10-05T23:06:47+00:00" }, { "name": "symfony/http-kernel", @@ -6348,7 +6348,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-08-28T19:21:40+00:00" + "time": "2017-10-05T23:24:02+00:00" }, { "name": "symfony/intl", @@ -6424,7 +6424,7 @@ "l10n", "localization" ], - "time": "2017-08-27T14:29:03+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/monolog-bridge", @@ -6487,7 +6487,7 @@ ], "description": "Symfony Monolog Bridge", "homepage": "https://symfony.com", - "time": "2017-04-12T14:07:15+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/monolog-bundle", @@ -6601,7 +6601,7 @@ "configuration", "options" ], - "time": "2017-04-12T14:07:15+00:00" + "time": "2017-09-11T20:39:16+00:00" }, { "name": "symfony/polyfill-apcu", @@ -6657,7 +6657,7 @@ "portable", "shim" ], - "time": "2017-07-05T15:09:33+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-intl-icu", @@ -6715,7 +6715,7 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -6832,7 +6832,7 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-php55", @@ -6888,7 +6888,7 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-php56", @@ -7104,7 +7104,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-07-03T08:04:30+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/property-access", @@ -7239,7 +7239,7 @@ "uri", "url" ], - "time": "2017-06-20T23:27:56+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/security", @@ -7451,7 +7451,71 @@ ], "description": "Symfony SecurityBundle", "homepage": "https://symfony.com", - "time": "2017-07-11T07:12:11+00:00" + "time": "2017-10-01T21:00:16+00:00" + }, + { + "name": "symfony/serializer", + "version": "v2.8.33", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "989ed5bce3cc508b19d3dea6143ea61117957c8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/989ed5bce3cc508b19d3dea6143ea61117957c8f", + "reference": "989ed5bce3cc508b19d3dea6143ea61117957c8f", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-php55": "~1.0" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.0", + "symfony/config": "~2.2|~3.0.0", + "symfony/property-access": "~2.3|~3.0.0", + "symfony/yaml": "^2.0.5|~3.0.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader and metadata cache.", + "symfony/config": "For using the XML mapping loader.", + "symfony/property-access": "For using the ObjectNormalizer.", + "symfony/yaml": "For using the default YAML mapping loader." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Serializer Component", + "homepage": "https://symfony.com", + "time": "2018-01-03T07:36:31+00:00" }, { "name": "symfony/stopwatch", @@ -7559,7 +7623,7 @@ ], "description": "Symfony SwiftmailerBundle", "homepage": "http://symfony.com", - "time": "2017-07-22T07:18:13+00:00" + "time": "2017-10-19T01:06:41+00:00" }, { "name": "symfony/templating", @@ -7825,7 +7889,7 @@ ], "description": "Symfony TwigBundle", "homepage": "https://symfony.com", - "time": "2017-07-11T07:12:11+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/validator", @@ -7898,7 +7962,7 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2017-08-27T14:29:03+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "symfony/yaml", @@ -7947,7 +8011,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-01T20:52:29+00:00" + "time": "2017-10-05T14:38:30+00:00" }, { "name": "twig/twig", @@ -8195,7 +8259,7 @@ "oauth", "server" ], - "time": "2017-05-10 00:39:21" + "time": "2017-05-10T00:39:21+00:00" } ], "packages-dev": [ @@ -8768,7 +8832,7 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2017-10-19T19:58:43+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -9363,7 +9427,7 @@ "testing", "xunit" ], - "time": "2017-09-24T07:23:38+00:00" + "time": "2017-10-15T06:13:55+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -10278,7 +10342,7 @@ "debug", "dump" ], - "time": "2017-08-27T14:52:21+00:00" + "time": "2017-10-02T06:42:24+00:00" }, { "name": "symfony/web-profiler-bundle", @@ -10337,7 +10401,7 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "time": "2017-07-19T17:48:51+00:00" + "time": "2017-10-01T21:00:16+00:00" }, { "name": "webfactory/exceptions-bundle", From 0fd2f67daad1ba512d94fd0ef756db87db720be0 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 10 Jan 2018 14:15:05 +0100 Subject: [PATCH 013/778] wuhaaaaa --- .../LeadBundle/Segment/LeadSegmentFilter.php | 4 ++ .../LeadSegmentFilterQueryBuilderTrait.php | 3 + .../Services/LeadSegmentQueryBuilder.php | 69 +------------------ 3 files changed, 8 insertions(+), 68 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 99a7be49f6c..c8faf878f0b 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -124,13 +124,17 @@ public function getFilterConditionValue($argument = null) { switch ($this->getDBColumn()->getType()->getName()) { case 'number': case 'integer': + case 'float': return ":" . $argument; case 'datetime': + case 'date': return sprintf('":%s"', $argument); + case 'text': case 'string': switch ($this->getFunc()) { case 'eq': case 'ne': + case 'neq': return sprintf("':%s'", $argument); default: throw new \Exception('Unknown operator ' . $this->getFunc()); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index 4ee9dae5e71..1cdd909214e 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -170,11 +170,14 @@ protected function addJoinCondition(QueryBuilder $qb, $alias, $expr) { $result = $parts = $qb->getQueryPart('join'); + dump(1); foreach ($parts['l'] as $key=>$part) { + dump($part); if ($part['joinAlias'] == $alias) { $result['l'][$key]['joinCondition'] = $part['joinCondition'] . " and " . $expr; } } + dump(2); $qb->setQueryPart('join', $result); dump($qb->getQueryParts()); die(); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 2cabfc91c37..22e597d00f3 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -85,6 +85,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters $qb->select('*')->from('MauticLeadBundle:Lead', 'l'); + $qb->execute(); foreach ($leadSegmentFilters as $filter) { dump($filter); $qb = $this->getQueryPart($filter, $qb); @@ -142,74 +143,6 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters return $leads; } - /** - * @todo this is just copy of existing as template, not a real function - */ - private function addLeadListQuery($filter) { - $table = 'lead_lists_leads'; - $column = 'leadlist_id'; - $falseParameter = $this->generateRandomParameterName(); - $parameters[$falseParameter] = false; - $trueParameter = $this->generateRandomParameterName(); - $parameters[$trueParameter] = true; - $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; - $ignoreAutoFilter = true; - - if ($filterListIds = (array)$filter->getFilter()) { - $listQb = $this->entityManager->getConnection()->createQueryBuilder()->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l'); - $listQb->where($listQb->expr()->in('l.id', $filterListIds)); - $filterLists = $listQb->execute()->fetchAll(); - $not = 'NOT EXISTS' === $func; - - // Each segment's filters must be appended as ORs so that each list is evaluated individually - $existsExpr = $not ? $listQb->expr()->andX() : $listQb->expr()->orX(); - - foreach ($filterLists as $list) { - $alias = $this->generateRandomParameterName(); - $id = (int)$list['id']; - if ($id === (int)$listId) { - // Ignore as somehow self is included in the list - continue; - } - - $listFilters = unserialize($list['filters']); - if (empty($listFilters)) { - // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list - $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $id, $parameters, [$alias . '.manually_removed' => $falseParameter,]); - } - else { - // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet - // but also leverage the current membership to take into account those manually added or removed from the segment - - // Build a "live" query based on current filters to catch those that have not been processed yet - $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); - $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); - - // Left join membership to account for manually added and removed - $membershipAlias = $this->generateRandomParameterName(); - $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX . $table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") - ->where($subQb->expr()->orX($filterExpr, $subQb->expr() - ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr()->orX($subQb->expr() - ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr() - ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - )); - } - - $existsExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); - } - - if ($existsExpr->count()) { - $groupExpr->add($existsExpr); - } - } - - break; - } - - private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { var_dump('Asseting query type: ' . $filter->getFilter()); $parameters = []; From fa716db3a6b4f0f2908db191745a2680e518de20 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 10 Jan 2018 15:22:17 +0100 Subject: [PATCH 014/778] prosim nic nepis :-) --- app/bundles/LeadBundle/Segment/LeadSegmentFilter.php | 6 ++++-- .../LeadBundle/Services/LeadSegmentQueryBuilder.php | 12 ------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index c8faf878f0b..eb7d6538cae 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -210,11 +210,13 @@ public function getEntityName() { */ public function getDBColumn() { if (is_null($this->dbColumn)) { - if($this->getQueryDescription()) { - $this->dbColumn = $this->schema->listTableColumns($this->getDBTable())[$this->queryDescription['field']]; + if($descr = $this->getQueryDescription()) { + $this->dbColumn = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; } else { + var_dump($this->getDBTable()); $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); if (!$dbTableColumns) { + var_dump($this); throw new \Exception('Unknown database table and no translation provided for type "' . $this->getType() .'"'); } if (!isset($dbTableColumns[$this->getField()])) { diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 22e597d00f3..e12d5e4fd99 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -85,7 +85,6 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters $qb->select('*')->from('MauticLeadBundle:Lead', 'l'); - $qb->execute(); foreach ($leadSegmentFilters as $filter) { dump($filter); $qb = $this->getQueryPart($filter, $qb); @@ -145,17 +144,6 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { var_dump('Asseting query type: ' . $filter->getFilter()); - $parameters = []; - - ///** @var Column $dbColumn */ - //$dbColumn = isset($this->schema->listTableColumns($tableName)[$filter->getField()]) ? $this->schema->listTableColumns($tableName)[$filter->getField()] : false; - $tableName = $this->entityManager->getClassMetadata('MauticLeadBundle:' . ucfirst($filter->getObject())) - ->getTableName(); - - $dbField = $filter->getDBColumn()->getFullQualifiedName(ucfirst($filter->getObject())); - - var_dump($qb->getQueryParts()); - $qb = $filter->createQuery($qb); // if (!$dbField) { From ca857b2ab7e9cf4f5c4b7ef426303116c3b30a59 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 10:39:16 +0100 Subject: [PATCH 015/778] working version with foreing relations and functions, reusing aliases *now we will refactor the filter factory, to remove methods of filter which are using external resources* * methods will be moved to new decorator * filter will be extended by different filter types, filter IS NOT a service, it's merely an entity * they will have certain predefined decorator, factory may change it * query building function will be present in this decorator, decorator IS a service, decorator MUST NOT retain own attributes --- .../Entity/LeadListSegmentRepository.php | 1 - .../LeadBundle/Segment/LeadSegmentFilter.php | 47 ++++++++++++++----- .../LeadSegmentFilterQueryBuilderTrait.php | 13 ++--- .../Services/LeadSegmentQueryBuilder.php | 24 ++-------- 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 1fb64f02f64..10b9c8a399c 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -149,7 +149,6 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) { - var_dump(debug_backtrace()[1]['function']); $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); if ($leadSegmentFilters->isHasCompanyFilter()) { diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index eb7d6538cae..ca7df89342f 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -150,39 +150,55 @@ public function getFilterConditionValue($argument = null) { public function createQuery(QueryBuilder $queryBuilder, $alias = false) { + dump('creating query:' . $this->getObject()); $glueFunc = $this->getGlue() . 'Where'; $parameterName = $this->generateRandomParameterName(); - $queryBuilder = $queryBuilder->$glueFunc( - $this->createExpression($queryBuilder, $parameterName, $this->getFunc()) - ); + $queryBuilder = $this->createExpression($queryBuilder, $parameterName, $this->getFunc()); $queryBuilder->setParameter($parameterName, $this->getFilter()); + + dump($queryBuilder->getSQL()); + return $queryBuilder; } public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) { - dump('creating expression'); dump($this); + dump('creating query:' . $this->getField()); $func = is_null($func) ? $this->getFunc() : $func; $alias = $this->getTableAlias($this->getEntityName(), $queryBuilder); + $desc = $this->getQueryDescription(); if (!$alias) { - $expr = $queryBuilder->expr()->$func($this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $this->generateRandomParameterName(), $expr); + if ($desc['func']) { + $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); + $expr = $queryBuilder->expr()->$func($desc['func'] . "(" . $alias . "." . $this->getDBColumn()->getName() . ")", $this->getFilterConditionValue($parameterName)); + $queryBuilder = $queryBuilder->andHaving($expr); + } else { + if ($alias != 'l') { + $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); + $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); + } else { + dump('lead restriction'); + $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + var_dump($expr); + die(); + $queryBuilder = $queryBuilder->andWhere($expr); + } + } + } else { - $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); if ($alias != 'l') { + $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); - } else { - dump('lead restriction'); + $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $queryBuilder->andWhere($expr); } } - dump($queryBuilder->getQueryParts()); //die(); - return $queryBuilder; } @@ -200,7 +216,13 @@ public function getDBTable() { public function getEntityName() { $converter = new CamelCaseToSnakeCaseNameConverter(); - $entity = sprintf('MauticLeadBundle:%s',ucfirst($converter->denormalize($this->getObject()))); + if ($this->getQueryDescription()) { + $table = $this->queryDescription['foreign_table']; + } else { + $table = $this->getObject(); + } + + $entity = sprintf('MauticLeadBundle:%s',ucfirst($converter->denormalize($table))); return $entity; } @@ -213,7 +235,6 @@ public function getDBColumn() { if($descr = $this->getQueryDescription()) { $this->dbColumn = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; } else { - var_dump($this->getDBTable()); $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); if (!$dbTableColumns) { var_dump($this); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index 1cdd909214e..23c787660ef 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -21,7 +21,7 @@ public function getTableAlias($tableEntity, QueryBuilder $queryBuilder) { $tables = $this->getTableAliases($queryBuilder); if (!in_array($tableEntity, $tables)) { - var_dump(sprintf('table entity ' . $tableEntity . ' not found in "%s"', join(', ', array_keys($tables)))); + //var_dump(sprintf('table entity ' . $tableEntity . ' not found in "%s"', join(', ', array_keys($tables)))); } return isset($tables[$tableEntity]) ? $tables[$tableEntity] : false; @@ -39,9 +39,7 @@ public function getTableAliases(QueryBuilder $queryBuilder) { $tables[$joinPart['joinTable']] = $joinPart['joinAlias']; } } - var_dump($tables); - var_dump($queryParts['join']); - + return $tables; } @@ -76,7 +74,7 @@ protected function createJoin(QueryBuilder $queryBuilder, $target, $alias, $join return $queryBuilder; } - protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter, $reuseTable = true) { + protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) { $filter->createJoin($qb, $alias); if (isset($translated) && $translated) { if (isset($translated['func'])) { @@ -170,17 +168,14 @@ protected function addJoinCondition(QueryBuilder $qb, $alias, $expr) { $result = $parts = $qb->getQueryPart('join'); - dump(1); foreach ($parts['l'] as $key=>$part) { - dump($part); if ($part['joinAlias'] == $alias) { $result['l'][$key]['joinCondition'] = $part['joinCondition'] . " and " . $expr; } } - dump(2); $qb->setQueryPart('join', $result); - dump($qb->getQueryParts()); die(); + return $qb; } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index e12d5e4fd99..53d55215502 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -75,7 +75,7 @@ public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $ // LEFT JOIN lead_lists_leads ll ON (ll.leadlist_id = 28) AND (ll.lead_id = l.id) AND (ll.date_added <= '2018-01-09 14:48:54') // WHERE (l.propertytype = :MglShQLG) AND (ll.lead_id IS NULL) var_dump($whatever); - return $queryBuilder; + return $queryBuilder; die(); } @@ -85,8 +85,9 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters $qb->select('*')->from('MauticLeadBundle:Lead', 'l'); + var_dump(count($leadSegmentFilters)); foreach ($leadSegmentFilters as $filter) { - dump($filter); + var_dump($filter->getField()); $qb = $this->getQueryPart($filter, $qb); } @@ -143,26 +144,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters } private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { - var_dump('Asseting query type: ' . $filter->getFilter()); - $qb = $filter->createQuery($qb); - // if (!$dbField) { - // $translated = $filter->getQueryDescription(); - // - // if (!$translated) { - // $filter->createQuery($qb); - // return $qb; - // throw new \Exception('Unknown field: ' . $filter->getField()); - // } - // - // switch ($translated['type']) { - // case 'foreign': - // case 'foreign_aggr': - // $this->addForeignTableQuery($qb, $filter); - // break; - // } - // } - return $qb; From bce935ddd3a958bec6163f1d7951f1d06d304179 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 12:38:30 +0100 Subject: [PATCH 016/778] team programming start --- .../Segment/Decorator/BaseDecorator.php | 13 ++ .../LeadBundle/Segment/LeadSegmentFilter.php | 119 ++++++++++-------- .../Segment/LeadSegmentFilterFactory.php | 37 +++++- .../QueryBuilder/BaseFilterQueryBuilder.php | 13 ++ 4 files changed, 124 insertions(+), 58 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php create mode 100644 app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php new file mode 100644 index 00000000000..a4a54016f99 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -0,0 +1,13 @@ +getOperator()) { - case 'gt': - return '>'; - case 'eq': - return '='; - case 'gt': - return '>'; - case 'gte': - return '>='; - case 'lt': - return '<'; - case 'lte': - return '<='; - } - throw new \Exception(sprintf('Unknown operator \'%s\'.', $this->getOperator())); - } - - public function getFilterConditionValue($argument = null) { switch ($this->getDBColumn()->getType()->getName()) { case 'number': case 'integer': case 'float': - return ":" . $argument; + return ':'.$argument; case 'datetime': case 'date': return sprintf('":%s"', $argument); @@ -137,7 +119,7 @@ public function getFilterConditionValue($argument = null) { case 'neq': return sprintf("':%s'", $argument); default: - throw new \Exception('Unknown operator ' . $this->getFunc()); + throw new \Exception('Unknown operator '.$this->getFunc()); } default: var_dump($this->getDBColumn()->getType()->getName()); @@ -148,10 +130,10 @@ public function getFilterConditionValue($argument = null) { throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); } - - public function createQuery(QueryBuilder $queryBuilder, $alias = false) { - dump('creating query:' . $this->getObject()); - $glueFunc = $this->getGlue() . 'Where'; + public function createQuery(QueryBuilder $queryBuilder, $alias = false) + { + dump('creating query:'.$this->getObject()); + $glueFunc = $this->getGlue().'Where'; $parameterName = $this->generateRandomParameterName(); @@ -159,42 +141,41 @@ public function createQuery(QueryBuilder $queryBuilder, $alias = false) { $queryBuilder->setParameter($parameterName, $this->getFilter()); - dump($queryBuilder->getSQL()); return $queryBuilder; } - public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) { - dump('creating query:' . $this->getField()); - $func = is_null($func) ? $this->getFunc() : $func; + public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) + { + dump('creating query:'.$this->getField()); + $func = is_null($func) ? $this->getFunc() : $func; $alias = $this->getTableAlias($this->getEntityName(), $queryBuilder); - $desc = $this->getQueryDescription(); + $desc = $this->getQueryDescription(); if (!$alias) { if ($desc['func']) { $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($desc['func'] . "(" . $alias . "." . $this->getDBColumn()->getName() . ")", $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($desc['func'].'('.$alias.'.'.$this->getDBColumn()->getName().')', $this->getFilterConditionValue($parameterName)); $queryBuilder = $queryBuilder->andHaving($expr); } else { if ($alias != 'l') { $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); } else { dump('lead restriction'); - $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); var_dump($expr); die(); $queryBuilder = $queryBuilder->andWhere($expr); } } - } else { if ($alias != 'l') { - $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); } else { - $expr = $queryBuilder->expr()->$func($alias . '.' . $this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $queryBuilder->andWhere($expr); } } @@ -202,7 +183,8 @@ public function createExpression(QueryBuilder $queryBuilder, $parameterName, $fu return $queryBuilder; } - public function getDBTable() { + public function getDBTable() + { //@todo cache metadata try { $tableName = $this->em->getClassMetadata($this->getEntityName())->getTableName(); @@ -210,11 +192,11 @@ public function getDBTable() { return $this->getObject(); } - return $tableName; } - public function getEntityName() { + public function getEntityName() + { $converter = new CamelCaseToSnakeCaseNameConverter(); if ($this->getQueryDescription()) { $table = $this->queryDescription['foreign_table']; @@ -222,26 +204,29 @@ public function getEntityName() { $table = $this->getObject(); } - $entity = sprintf('MauticLeadBundle:%s',ucfirst($converter->denormalize($table))); + $entity = sprintf('MauticLeadBundle:%s', ucfirst($converter->denormalize($table))); + return $entity; } /** * @return Column + * * @throws \Exception */ - public function getDBColumn() { + public function getDBColumn() + { if (is_null($this->dbColumn)) { - if($descr = $this->getQueryDescription()) { + if ($descr = $this->getQueryDescription()) { $this->dbColumn = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; } else { $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); if (!$dbTableColumns) { var_dump($this); - throw new \Exception('Unknown database table and no translation provided for type "' . $this->getType() .'"'); + throw new \Exception('Unknown database table and no translation provided for type "'.$this->getType().'"'); } if (!isset($dbTableColumns[$this->getField()])) { - throw new \Exception('Unknown database column and no translation provided for type "' . $this->getType() .'"'); + throw new \Exception('Unknown database column and no translation provided for type "'.$this->getType().'"'); } $this->dbColumn = $dbTableColumns[$this->getField()]; } @@ -386,11 +371,11 @@ private function sanitizeFilter($filter) switch ($this->getType()) { case 'number': - $filter = (float)$filter; + $filter = (float) $filter; break; case 'boolean': - $filter = (bool)$filter; + $filter = (bool) $filter; break; } @@ -405,27 +390,51 @@ public function getQueryDescription($dictionary = null) if (is_null($this->queryDescription)) { $this->translateQueryDescription($dictionary); } + return $this->queryDescription; } /** * @param array $queryDescription + * * @return LeadSegmentFilter */ public function setQueryDescription($queryDescription) { $this->queryDescription = $queryDescription; + return $this; } /** * @return $this */ - public function translateQueryDescription(\ArrayIterator $dictionary = null) { + public function translateQueryDescription(\ArrayIterator $dictionary = null) + { $this->queryDescription = isset($dictionary[$this->getField()]) ? $dictionary[$this->getField()] : false; return $this; } + + /** + * @return BaseFilterQueryBuilder + */ + public function getQueryBuilder() + { + return $this->queryBuilder; + } + + /** + * @param BaseFilterQueryBuilder $queryBuilder + * + * @return LeadSegmentFilter + */ + public function setQueryBuilder($queryBuilder) + { + $this->queryBuilder = $queryBuilder; + + return $this; + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 0126bbddec2..833e6fddb2d 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -13,9 +13,12 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; +use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; +use Mautic\LeadBundle\Segment\QueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; -class LeadSegmentFilterFactory { +class LeadSegmentFilterFactory +{ /** * @var LeadSegmentFilterDate */ @@ -32,7 +35,8 @@ class LeadSegmentFilterFactory { /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ private $entityManager; - public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSegmentFilterOperator $leadSegmentFilterOperator, LeadSegmentFilterDescriptor $dictionary, EntityManager $entityManager) { + public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSegmentFilterOperator $leadSegmentFilterOperator, LeadSegmentFilterDescriptor $dictionary, EntityManager $entityManager) + { $this->leadSegmentFilterDate = $leadSegmentFilterDate; $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; $this->dictionary = $dictionary; @@ -44,12 +48,19 @@ public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSe * * @return LeadSegmentFilters */ - public function getLeadListFilters(LeadList $leadList) { + public function getLeadListFilters(LeadList $leadList) + { $leadSegmentFilters = new LeadSegmentFilters(); $filters = $leadList->getFilters(); foreach ($filters as $filter) { $leadSegmentFilter = new LeadSegmentFilter($filter, $this->dictionary, $this->entityManager); + + $leadSegmentFilter->setQueryDescription($this->dictionary[$filter] ? $this->dictionary[$filter] : false); + $leadSegmentFilter->setQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); + $leadSegmentFilter->setDecorator($this->getDecoratorForFilter($leadSegmentFilter)); + + //@todo replaced in query builder $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilter); $this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); @@ -57,4 +68,24 @@ public function getLeadListFilters(LeadList $leadList) { return $leadSegmentFilters; } + + /** + * @param LeadSegmentFilter $filter + * + * @return BaseFilterQueryBuilder + */ + protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) + { + return new BaseFilterQueryBuilder(); + } + + /** + * @param LeadSegmentFilter $filter + * + * @return BaseDecorator + */ + protected function getDecoratorForFilter(LeadSegmentFilter $filter) + { + return new BaseDecorator(); + } } diff --git a/app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php new file mode 100644 index 00000000000..9327630acba --- /dev/null +++ b/app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php @@ -0,0 +1,13 @@ + Date: Thu, 11 Jan 2018 12:55:21 +0100 Subject: [PATCH 017/778] minor change to factory --- app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 833e6fddb2d..5e66779cc21 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -56,7 +56,9 @@ public function getLeadListFilters(LeadList $leadList) foreach ($filters as $filter) { $leadSegmentFilter = new LeadSegmentFilter($filter, $this->dictionary, $this->entityManager); - $leadSegmentFilter->setQueryDescription($this->dictionary[$filter] ? $this->dictionary[$filter] : false); + $leadSegmentFilter->setQueryDescription( + isset($this->dictionary[$leadSegmentFilter->getField()]) ? $this->dictionary[$leadSegmentFilter->getField()] : false + ); $leadSegmentFilter->setQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); $leadSegmentFilter->setDecorator($this->getDecoratorForFilter($leadSegmentFilter)); From 43d7cb07eb81be30bddfbb82fc21c32a3d974f32 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 13:02:06 +0100 Subject: [PATCH 018/778] minor change to factory #2 --- .../LeadBundle/Segment/LeadSegmentFilter.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 148330ff59c..8b34e9bf7fb 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Schema\Column; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\QueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -71,6 +72,10 @@ class LeadSegmentFilter */ private $queryBuilder; + /** + * @var BaseDecorator + */ + private $decorator; /** * @var array */ @@ -437,4 +442,24 @@ public function setQueryBuilder($queryBuilder) return $this; } + + /** + * @return BaseDecorator + */ + public function getDecorator() + { + return $this->decorator; + } + + /** + * @param BaseDecorator $decorator + * + * @return LeadSegmentFilter + */ + public function setDecorator($decorator) + { + $this->decorator = $decorator; + + return $this; + } } From 8f42338d17b397798bca96738733c894fb448ab1 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 14:27:33 +0100 Subject: [PATCH 019/778] premerge commit --- .../BaseFilterQueryBuilder.php | 19 + .../FilterQueryBuilderInterface.php | 18 + .../LeadBundle/Segment/LeadSegmentFilter.php | 10 +- .../Segment/LeadSegmentFilterFactory.php | 2 +- .../LeadBundle/Segment/LeadSegmentService.php | 8 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 1369 +++++++++++++++++ .../QueryBuilder/BaseFilterQueryBuilder.php | 13 - .../Services/LeadSegmentQueryBuilder.php | 278 ++-- 8 files changed, 1553 insertions(+), 164 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php create mode 100644 app/bundles/LeadBundle/Segment/Query/QueryBuilder.php delete mode 100644 app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php new file mode 100644 index 00000000000..d8f8c2ecfd8 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -0,0 +1,19 @@ +getDBColumn()->getType()->getName()) { diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 5e66779cc21..61675c60916 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -14,7 +14,7 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; -use Mautic\LeadBundle\Segment\QueryBuilder\BaseFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class LeadSegmentFilterFactory diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 045d9f29c91..83d4a87d2b0 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -49,17 +49,17 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); - $qb = $this->queryBuilder->addLeadListRestrictions($qb, $batchLimiters, $entity->getId(), $this->leadSegmentFilterFactory->dictionary); + //$qb = $this->queryBuilder->addLeadListRestrictions($qb, $batchLimiters, $entity->getId(), $this->leadSegmentFilterFactory->dictionary); dump($sql = $qb->getSQL()); - $parameters = $qb->getParameters(); - foreach($parameters as $parameter=>$value) { - $sql = str_replace(':' . $parameter, $value, $sql); + foreach ($parameters as $parameter=>$value) { + $sql = str_replace(':'.$parameter, $value, $sql); } var_dump($sql); // die(); return null; + return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php new file mode 100644 index 00000000000..c8329014c5d --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -0,0 +1,1369 @@ +. + */ + +namespace Mautic\LeadBundle\Segment\Query; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Query\Expression\CompositeExpression; + +/** + * QueryBuilder class is responsible to dynamically create SQL queries. + * + * Important: Verify that every feature you use will work with your database vendor. + * SQL Query Builder does not attempt to validate the generated SQL at all. + * + * The query builder does no validation whatsoever if certain features even work with the + * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements + * even if some vendors such as MySQL support it. + * + * @see www.doctrine-project.org + * @since 2.1 + * + * @author Guilherme Blanco + * @author Benjamin Eberlei + */ +class QueryBuilder +{ + /* + * The query types. + */ + const SELECT = 0; + const DELETE = 1; + const UPDATE = 2; + const INSERT = 3; + + /* + * The builder states. + */ + const STATE_DIRTY = 0; + const STATE_CLEAN = 1; + + /** + * The DBAL Connection. + * + * @var \Doctrine\DBAL\Connection + */ + private $connection; + + /** + * @var array the array of SQL parts collected + */ + private $sqlParts = [ + 'select' => [], + 'from' => [], + 'join' => [], + 'set' => [], + 'where' => null, + 'groupBy' => [], + 'having' => null, + 'orderBy' => [], + 'values' => [], + ]; + + /** + * The complete SQL string for this query. + * + * @var string + */ + private $sql; + + /** + * The query parameters. + * + * @var array + */ + private $params = []; + + /** + * The parameter type map of this query. + * + * @var array + */ + private $paramTypes = []; + + /** + * The type of query this is. Can be select, update or delete. + * + * @var int + */ + private $type = self::SELECT; + + /** + * The state of the query object. Can be dirty or clean. + * + * @var int + */ + private $state = self::STATE_CLEAN; + + /** + * The index of the first result to retrieve. + * + * @var int + */ + private $firstResult = null; + + /** + * The maximum number of results to retrieve. + * + * @var int + */ + private $maxResults = null; + + /** + * The counter of bound parameters used with {@see bindValue). + * + * @var int + */ + private $boundCounter = 0; + + /** + * Initializes a new QueryBuilder. + * + * @param \Doctrine\DBAL\Connection $connection the DBAL Connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example:. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + * + * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder + */ + public function expr() + { + return $this->connection->getExpressionBuilder(); + } + + /** + * Gets the type of the currently built query. + * + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * Gets the associated DBAL Connection for this query builder. + * + * @return \Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Gets the state of this query builder instance. + * + * @return int either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN + */ + public function getState() + { + return $this->state; + } + + /** + * Executes this query using the bound parameters and their types. + * + * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate} + * for insert, update and delete statements. + * + * @return \Doctrine\DBAL\Driver\Statement|int + */ + public function execute() + { + if ($this->type == self::SELECT) { + return $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes); + } else { + return $this->connection->executeUpdate($this->getSQL(), $this->params, $this->paramTypes); + } + } + + /** + * Gets the complete SQL string formed by the current specifications of this QueryBuilder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * echo $qb->getSQL(); // SELECT u FROM User u + * + * + * @return string the SQL query string + */ + public function getSQL() + { + if ($this->sql !== null && $this->state === self::STATE_CLEAN) { + return $this->sql; + } + + switch ($this->type) { + case self::INSERT: + $sql = $this->getSQLForInsert(); + break; + case self::DELETE: + $sql = $this->getSQLForDelete(); + break; + + case self::UPDATE: + $sql = $this->getSQLForUpdate(); + break; + + case self::SELECT: + default: + $sql = $this->getSQLForSelect(); + break; + } + + $this->state = self::STATE_CLEAN; + $this->sql = $sql; + + return $sql; + } + + /** + * Sets a query parameter for the query being constructed. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id') + * ->setParameter(':user_id', 1); + * + * + * @param string|int $key the parameter position or name + * @param mixed $value the parameter value + * @param string|null $type one of the PDO::PARAM_* constants + * + * @return $this this QueryBuilder instance + */ + public function setParameter($key, $value, $type = null) + { + if ($type !== null) { + $this->paramTypes[$key] = $type; + } + + $this->params[$key] = $value; + + return $this; + } + + /** + * Sets a collection of query parameters for the query being constructed. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(array( + * ':user_id1' => 1, + * ':user_id2' => 2 + * )); + * + * + * @param array $params the query parameters to set + * @param array $types the query parameters types to set + * + * @return $this this QueryBuilder instance + */ + public function setParameters(array $params, array $types = []) + { + $this->paramTypes = $types; + $this->params = $params; + + return $this; + } + + /** + * Gets all defined query parameters for the query being constructed indexed by parameter index or name. + * + * @return array the currently defined query parameters indexed by parameter index or name + */ + public function getParameters() + { + return $this->params; + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param mixed $key the key (index or name) of the bound parameter + * + * @return mixed the value of the bound parameter + */ + public function getParameter($key) + { + return isset($this->params[$key]) ? $this->params[$key] : null; + } + + /** + * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. + * + * @return array the currently defined query parameter types indexed by parameter index or name + */ + public function getParameterTypes() + { + return $this->paramTypes; + } + + /** + * Gets a (previously set) query parameter type of the query being constructed. + * + * @param mixed $key the key (index or name) of the bound parameter type + * + * @return mixed the value of the bound parameter type + */ + public function getParameterType($key) + { + return isset($this->paramTypes[$key]) ? $this->paramTypes[$key] : null; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param int $firstResult the first result to return + * + * @return $this this QueryBuilder instance + */ + public function setFirstResult($firstResult) + { + $this->state = self::STATE_DIRTY; + $this->firstResult = $firstResult; + + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. + * + * @return int the position of the first result + */ + public function getFirstResult() + { + return $this->firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param int $maxResults the maximum number of results to retrieve + * + * @return $this this QueryBuilder instance + */ + public function setMaxResults($maxResults) + { + $this->state = self::STATE_DIRTY; + $this->maxResults = $maxResults; + + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + * + * @return int the maximum number of results + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * Either appends to or replaces a single, generic query part. + * + * The available parts are: 'select', 'from', 'set', 'where', + * 'groupBy', 'having' and 'orderBy'. + * + * @param string $sqlPartName + * @param string $sqlPart + * @param bool $append + * + * @return $this this QueryBuilder instance + */ + public function add($sqlPartName, $sqlPart, $append = false) + { + $isArray = is_array($sqlPart); + $isMultiple = is_array($this->sqlParts[$sqlPartName]); + + if ($isMultiple && !$isArray) { + $sqlPart = [$sqlPart]; + } + + $this->state = self::STATE_DIRTY; + + if ($append) { + if ($sqlPartName == 'orderBy' || $sqlPartName == 'groupBy' || $sqlPartName == 'select' || $sqlPartName == 'set') { + foreach ($sqlPart as $part) { + $this->sqlParts[$sqlPartName][] = $part; + } + } elseif ($isArray && is_array($sqlPart[key($sqlPart)])) { + $key = key($sqlPart); + $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key]; + } elseif ($isMultiple) { + $this->sqlParts[$sqlPartName][] = $sqlPart; + } else { + $this->sqlParts[$sqlPartName] = $sqlPart; + } + + return $this; + } + + $this->sqlParts[$sqlPartName] = $sqlPart; + + return $this; + } + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id', 'p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); + * + * + * @param mixed $select the selection expressions + * + * @return $this this QueryBuilder instance + */ + public function select($select = null) + { + $this->type = self::SELECT; + + if (empty($select)) { + return $this; + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', $selects, false); + } + + /** + * Adds an item that is to be returned in the query result. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->addSelect('p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); + * + * + * @param mixed $select the selection expression + * + * @return $this this QueryBuilder instance + */ + public function addSelect($select = null) + { + $this->type = self::SELECT; + + if (empty($select)) { + return $this; + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', $selects, true); + } + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain table. + * + * + * $qb = $conn->createQueryBuilder() + * ->delete('users', 'u') + * ->where('u.id = :user_id'); + * ->setParameter(':user_id', 1); + * + * + * @param string $delete the table whose rows are subject to the deletion + * @param string $alias the table alias used in the constructed query + * + * @return $this this QueryBuilder instance + */ + public function delete($delete = null, $alias = null) + { + $this->type = self::DELETE; + + if (!$delete) { + return $this; + } + + return $this->add('from', [ + 'table' => $delete, + 'alias' => $alias, + ]); + } + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain table. + * + * + * $qb = $conn->createQueryBuilder() + * ->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * + * + * @param string $update the table whose rows are subject to the update + * @param string $alias the table alias used in the constructed query + * + * @return $this this QueryBuilder instance + */ + public function update($update = null, $alias = null) + { + $this->type = self::UPDATE; + + if (!$update) { + return $this; + } + + return $this->add('from', [ + 'table' => $update, + 'alias' => $alias, + ]); + } + + /** + * Turns the query being built into an insert query that inserts into + * a certain table. + * + * + * $qb = $conn->createQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?', + * 'password' => '?' + * ) + * ); + * + * + * @param string $insert the table into which the rows should be inserted + * + * @return $this this QueryBuilder instance + */ + public function insert($insert = null) + { + $this->type = self::INSERT; + + if (!$insert) { + return $this; + } + + return $this->add('from', [ + 'table' => $insert, + ]); + } + + /** + * Creates and adds a query root corresponding to the table identified by the + * given alias, forming a cartesian product with any existing query roots. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->from('users', 'u') + * + * + * @param string $from the table + * @param string|null $alias the alias of the table + * + * @return $this this QueryBuilder instance + */ + public function from($from, $alias = null) + { + return $this->add('from', [ + 'table' => $from, + 'alias' => $alias, + ], true); + } + + /** + * Creates and adds a join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias the alias that points to a from clause + * @param string $join the table name to join + * @param string $alias the alias of the join table + * @param string $condition the condition for the join + * + * @return $this this QueryBuilder instance + */ + public function join($fromAlias, $join, $alias, $condition = null) + { + return $this->innerJoin($fromAlias, $join, $alias, $condition); + } + + /** + * Creates and adds a join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias the alias that points to a from clause + * @param string $join the table name to join + * @param string $alias the alias of the join table + * @param string $condition the condition for the join + * + * @return $this this QueryBuilder instance + */ + public function innerJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', [ + $fromAlias => [ + 'joinType' => 'inner', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition, + ], + ], true); + } + + /** + * Creates and adds a left join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias the alias that points to a from clause + * @param string $join the table name to join + * @param string $alias the alias of the join table + * @param string $condition the condition for the join + * + * @return $this this QueryBuilder instance + */ + public function leftJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', [ + $fromAlias => [ + 'joinType' => 'left', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition, + ], + ], true); + } + + /** + * Creates and adds a right join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias the alias that points to a from clause + * @param string $join the table name to join + * @param string $alias the alias of the join table + * @param string $condition the condition for the join + * + * @return $this this QueryBuilder instance + */ + public function rightJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', [ + $fromAlias => [ + 'joinType' => 'right', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition, + ], + ], true); + } + + /** + * Sets a new value for a column in a bulk update query. + * + * + * $qb = $conn->createQueryBuilder() + * ->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * + * + * @param string $key the column to set + * @param string $value the value, expression, placeholder, etc + * + * @return $this this QueryBuilder instance + */ + public function set($key, $value) + { + return $this->add('set', $key.' = '.$value, true); + } + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programatically build and/or expressions + * $qb = $conn->createQueryBuilder(); + * + * $or = $qb->expr()->orx(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where($or); + * + * + * @param mixed $predicates the restriction predicates + * + * @return $this this QueryBuilder instance + */ + public function where($predicates) + { + if (!(func_num_args() == 1 && $predicates instanceof CompositeExpression)) { + $predicates = new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @param mixed $where the query restrictions + * + * @return $this this QueryBuilder instance + * + * @see where() + */ + public function andWhere($where) + { + $args = func_get_args(); + $where = $this->getQueryPart('where'); + + if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new CompositeExpression(CompositeExpression::TYPE_AND, $args); + } + + return $this->add('where', $where, true); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @param mixed $where the WHERE statement + * + * @return $this this QueryBuilder instance + * + * @see where() + */ + public function orWhere($where) + { + $args = func_get_args(); + $where = $this->getQueryPart('where'); + + if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new CompositeExpression(CompositeExpression::TYPE_OR, $args); + } + + return $this->add('where', $where, true); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.id'); + * + * + * @param mixed $groupBy the grouping expression + * + * @return $this this QueryBuilder instance + */ + public function groupBy($groupBy) + { + if (empty($groupBy)) { + return $this; + } + + $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + + return $this->add('groupBy', $groupBy, false); + } + + /** + * Adds a grouping expression to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.lastLogin'); + * ->addGroupBy('u.createdAt') + * + * + * @param mixed $groupBy the grouping expression + * + * @return $this this QueryBuilder instance + */ + public function addGroupBy($groupBy) + { + if (empty($groupBy)) { + return $this; + } + + $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + + return $this->add('groupBy', $groupBy, true); + } + + /** + * Sets a value for a column in an insert query. + * + * + * $qb = $conn->createQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?' + * ) + * ) + * ->setValue('password', '?'); + * + * + * @param string $column the column into which the value should be inserted + * @param string $value the value that should be inserted into the column + * + * @return $this this QueryBuilder instance + */ + public function setValue($column, $value) + { + $this->sqlParts['values'][$column] = $value; + + return $this; + } + + /** + * Specifies values for an insert query indexed by column names. + * Replaces any previous values, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?', + * 'password' => '?' + * ) + * ); + * + * + * @param array $values the values to specify for the insert query indexed by column names + * + * @return $this this QueryBuilder instance + */ + public function values(array $values) + { + return $this->add('values', $values); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having the restriction over the groups + * + * @return $this this QueryBuilder instance + */ + public function having($having) + { + if (!(func_num_args() == 1 && $having instanceof CompositeExpression)) { + $having = new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having the restriction to append + * + * @return $this this QueryBuilder instance + */ + public function andHaving($having) + { + $args = func_get_args(); + $having = $this->getQueryPart('having'); + + if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new CompositeExpression(CompositeExpression::TYPE_AND, $args); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having the restriction to add + * + * @return $this this QueryBuilder instance + */ + public function orHaving($having) + { + $args = func_get_args(); + $having = $this->getQueryPart('having'); + + if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new CompositeExpression(CompositeExpression::TYPE_OR, $args); + } + + return $this->add('having', $having); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string $sort the ordering expression + * @param string $order the ordering direction + * + * @return $this this QueryBuilder instance + */ + public function orderBy($sort, $order = null) + { + return $this->add('orderBy', $sort.' '.(!$order ? 'ASC' : $order), false); + } + + /** + * Adds an ordering to the query results. + * + * @param string $sort the ordering expression + * @param string $order the ordering direction + * + * @return $this this QueryBuilder instance + */ + public function addOrderBy($sort, $order = null) + { + return $this->add('orderBy', $sort.' '.(!$order ? 'ASC' : $order), true); + } + + /** + * Gets a query part by its name. + * + * @param string $queryPartName + * + * @return mixed + */ + public function getQueryPart($queryPartName) + { + return $this->sqlParts[$queryPartName]; + } + + /** + * Gets all query parts. + * + * @return array + */ + public function getQueryParts() + { + return $this->sqlParts; + } + + /** + * Resets SQL parts. + * + * @param array|null $queryPartNames + * + * @return $this this QueryBuilder instance + */ + public function resetQueryParts($queryPartNames = null) + { + if (is_null($queryPartNames)) { + $queryPartNames = array_keys($this->sqlParts); + } + + foreach ($queryPartNames as $queryPartName) { + $this->resetQueryPart($queryPartName); + } + + return $this; + } + + /** + * Sets SQL parts. + * + * @param array $queryPartNames + * @param array $value + * + * @return $this this QueryBuilder instance + */ + public function setQueryPart($queryPartName, $value) + { + $this->sqlParts[$queryPartName] = $value; + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets a single SQL part. + * + * @param string $queryPartName + * + * @return $this this QueryBuilder instance + */ + public function resetQueryPart($queryPartName) + { + $this->sqlParts[$queryPartName] = is_array($this->sqlParts[$queryPartName]) + ? [] : null; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * @return string + * + * @throws \Doctrine\DBAL\Query\QueryException + */ + private function getSQLForSelect() + { + $query = 'SELECT '.implode(', ', $this->sqlParts['select']); + + $query .= ($this->sqlParts['from'] ? ' FROM '.implode(', ', $this->getFromClauses()) : '') + .($this->sqlParts['where'] !== null ? ' WHERE '.((string) $this->sqlParts['where']) : '') + .($this->sqlParts['groupBy'] ? ' GROUP BY '.implode(', ', $this->sqlParts['groupBy']) : '') + .($this->sqlParts['having'] !== null ? ' HAVING '.((string) $this->sqlParts['having']) : '') + .($this->sqlParts['orderBy'] ? ' ORDER BY '.implode(', ', $this->sqlParts['orderBy']) : ''); + + if ($this->isLimitQuery()) { + return $this->connection->getDatabasePlatform()->modifyLimitQuery( + $query, + $this->maxResults, + $this->firstResult + ); + } + + return $query; + } + + /** + * @return string[] + */ + private function getFromClauses() + { + $fromClauses = []; + $knownAliases = []; + + // Loop through all FROM clauses + foreach ($this->sqlParts['from'] as $from) { + if ($from['alias'] === null) { + $tableSql = $from['table']; + $tableReference = $from['table']; + } else { + $tableSql = $from['table'].' '.$from['alias']; + $tableReference = $from['alias']; + } + + $knownAliases[$tableReference] = true; + + $fromClauses[$tableReference] = $tableSql.$this->getSQLForJoins($tableReference, $knownAliases); + } + + $this->verifyAllAliasesAreKnown($knownAliases); + + return $fromClauses; + } + + /** + * @param array $knownAliases + * + * @throws QueryException + */ + private function verifyAllAliasesAreKnown(array $knownAliases) + { + foreach ($this->sqlParts['join'] as $fromAlias => $joins) { + if (!isset($knownAliases[$fromAlias])) { + throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases)); + } + } + } + + /** + * @return bool + */ + private function isLimitQuery() + { + return $this->maxResults !== null || $this->firstResult !== null; + } + + /** + * Converts this instance into an INSERT string in SQL. + * + * @return string + */ + private function getSQLForInsert() + { + return 'INSERT INTO '.$this->sqlParts['from']['table']. + ' ('.implode(', ', array_keys($this->sqlParts['values'])).')'. + ' VALUES('.implode(', ', $this->sqlParts['values']).')'; + } + + /** + * Converts this instance into an UPDATE string in SQL. + * + * @return string + */ + private function getSQLForUpdate() + { + $table = $this->sqlParts['from']['table'].($this->sqlParts['from']['alias'] ? ' '.$this->sqlParts['from']['alias'] : ''); + $query = 'UPDATE '.$table + .' SET '.implode(', ', $this->sqlParts['set']) + .($this->sqlParts['where'] !== null ? ' WHERE '.((string) $this->sqlParts['where']) : ''); + + return $query; + } + + /** + * Converts this instance into a DELETE string in SQL. + * + * @return string + */ + private function getSQLForDelete() + { + $table = $this->sqlParts['from']['table'].($this->sqlParts['from']['alias'] ? ' '.$this->sqlParts['from']['alias'] : ''); + $query = 'DELETE FROM '.$table.($this->sqlParts['where'] !== null ? ' WHERE '.((string) $this->sqlParts['where']) : ''); + + return $query; + } + + /** + * Gets a string representation of this QueryBuilder which corresponds to + * the final SQL query being constructed. + * + * @return string the string representation of this QueryBuilder + */ + public function __toString() + { + return $this->getSQL(); + } + + /** + * Creates a new named parameter and bind the value $value to it. + * + * This method provides a shortcut for PDOStatement::bindValue + * when using prepared statements. + * + * The parameter $value specifies the value that you want to bind. If + * $placeholder is not provided bindValue() will automatically create a + * placeholder for you. An automatic placeholder will be of the name + * ':dcValue1', ':dcValue2' etc. + * + * For more information see {@link http://php.net/pdostatement-bindparam} + * + * Example: + * + * $value = 2; + * $q->eq( 'id', $q->bindValue( $value ) ); + * $stmt = $q->executeQuery(); // executed with 'id = 2' + * + * + * @license New BSD License + * + * @see http://www.zetacomponents.org + * + * @param mixed $value + * @param mixed $type + * @param string $placeHolder The name to bind with. The string must start with a colon ':'. + * + * @return string the placeholder name used + */ + public function createNamedParameter($value, $type = \PDO::PARAM_STR, $placeHolder = null) + { + if ($placeHolder === null) { + ++$this->boundCounter; + $placeHolder = ':dcValue'.$this->boundCounter; + } + $this->setParameter(substr($placeHolder, 1), $value, $type); + + return $placeHolder; + } + + /** + * Creates a new positional parameter and bind the given value to it. + * + * Attention: If you are using positional parameters with the query builder you have + * to be very careful to bind all parameters in the order they appear in the SQL + * statement , otherwise they get bound in the wrong order which can lead to serious + * bugs in your code. + * + * Example: + * + * $qb = $conn->createQueryBuilder(); + * $qb->select('u.*') + * ->from('users', 'u') + * ->where('u.username = ' . $qb->createPositionalParameter('Foo', PDO::PARAM_STR)) + * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', PDO::PARAM_STR)) + * + * + * @param mixed $value + * @param int $type + * + * @return string + */ + public function createPositionalParameter($value, $type = \PDO::PARAM_STR) + { + ++$this->boundCounter; + $this->setParameter($this->boundCounter, $value, $type); + + return '?'; + } + + /** + * @param string $fromAlias + * @param array $knownAliases + * + * @return string + */ + private function getSQLForJoins($fromAlias, array &$knownAliases) + { + $sql = ''; + + if (isset($this->sqlParts['join'][$fromAlias])) { + foreach ($this->sqlParts['join'][$fromAlias] as $join) { + if (array_key_exists($join['joinAlias'], $knownAliases)) { + throw QueryException::nonUniqueAlias($join['joinAlias'], array_keys($knownAliases)); + } + $sql .= ' '.strtoupper($join['joinType']) + .' JOIN '.$join['joinTable'].' '.$join['joinAlias'] + .' ON '.((string) $join['joinCondition']); + $knownAliases[$join['joinAlias']] = true; + } + + foreach ($this->sqlParts['join'][$fromAlias] as $join) { + $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases); + } + } + + return $sql; + } + + /** + * Deep clone of all expression objects in the SQL parts. + */ + public function __clone() + { + foreach ($this->sqlParts as $part => $elements) { + if (is_array($this->sqlParts[$part])) { + foreach ($this->sqlParts[$part] as $idx => $element) { + if (is_object($element)) { + $this->sqlParts[$part][$idx] = clone $element; + } + } + } elseif (is_object($elements)) { + $this->sqlParts[$part] = clone $elements; + } + } + + foreach ($this->params as $name => $param) { + if (is_object($param)) { + $this->params[$name] = clone $param; + } + } + } +} diff --git a/app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php deleted file mode 100644 index 9327630acba..00000000000 --- a/app/bundles/LeadBundle/Segment/QueryBuilder/BaseFilterQueryBuilder.php +++ /dev/null @@ -1,13 +0,0 @@ -entityManager = $entityManager; $this->randomParameterName = $randomParameterName; $this->schema = $this->entityManager->getConnection()->getSchemaManager(); - - //@todo Will be generate automatically, just as POC - $this->tableAliases['leads'] = 'l'; - $this->tableAliases['email_stats'] = 'es'; - $this->tableAliases['page_hits'] = 'ph'; } - - public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $leadListId, $dictionary) { + public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $leadListId, $dictionary) + { $filter_list_id = new LeadSegmentFilter([ 'glue' => 'and', 'field' => 'leadlist_id', 'object' => 'lead_lists_leads', 'type' => 'number', 'filter' => intval($leadListId), - 'operator' => "=", - 'func' => "eq" + 'operator' => '=', + 'func' => 'eq', ], $dictionary, $this->entityManager); $filter_list_added = new LeadSegmentFilter([ @@ -66,8 +60,8 @@ public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $ 'object' => 'lead_lists_leads', 'type' => 'date', 'filter' => $whatever, - 'operator' => "=", - 'func' => "lte" + 'operator' => '=', + 'func' => 'lte', ], $dictionary, $this->entityManager); $queryBuilder = $this->addForeignTableQueryWhere($queryBuilder, [$filter_list_id, $filter_list_added]); @@ -75,28 +69,28 @@ public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $ // LEFT JOIN lead_lists_leads ll ON (ll.leadlist_id = 28) AND (ll.lead_id = l.id) AND (ll.date_added <= '2018-01-09 14:48:54') // WHERE (l.propertytype = :MglShQLG) AND (ll.lead_id IS NULL) var_dump($whatever); + return $queryBuilder; die(); } - public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { + public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) + { /** @var QueryBuilder $qb */ - $qb = $this->entityManager->getConnection()->createQueryBuilder(); + $qb = new \Mautic\LeadBundle\Segment\Query\QueryBuilder($this->entityManager->getConnection()); - $qb->select('*')->from('MauticLeadBundle:Lead', 'l'); + $qb->select('*')->from('leads', 'l'); - var_dump(count($leadSegmentFilters)); + /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { - var_dump($filter->getField()); - $qb = $this->getQueryPart($filter, $qb); + var_dump('parsing filter: '.$filter->__toString()); + $qb = $filter->getQueryBuilder(); } - return $qb; echo 'SQL parameters:'; dump($q->getParameters()); - // Leads that do not have any record in the lead_lists_leads table for this lead list // For non null fields - it's apparently better to use left join over not exists due to not using nullable // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ @@ -107,7 +101,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters $listOnExpr->add($q->expr()->lte('ll.date_added', $q->expr()->literal($batchLimiters['dateTime']))); } - $q->leftJoin('l', MAUTIC_TABLE_PREFIX . 'lead_lists_leads', 'll', $listOnExpr); + $q->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll', $listOnExpr); $expr->add($q->expr()->isNull('ll.lead_id')); @@ -134,7 +128,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters $leads = []; foreach ($results as $r) { - $leads = ['count' => $r['lead_count'], 'maxId' => $r['max_id'],]; + $leads = ['count' => $r['lead_count'], 'maxId' => $r['max_id']]; if ($withMinId) { $leads['minId'] = $r['min_id']; } @@ -143,7 +137,8 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters return $leads; } - private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { + private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) + { $qb = $filter->createQuery($qb); return $qb; @@ -172,14 +167,13 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { // var_dump($func . ":" . $leadSegmentFilter->getField()); // var_dump($exprParameter); - switch ($leadSegmentFilter->getField()) { case 'hit_url': case 'referer': case 'source': case 'source_id': case 'url_title': - $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp', 'startsWith', 'endsWith', 'contains',]) ? 'EXISTS' : 'NOT EXISTS'; + $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp', 'startsWith', 'endsWith', 'contains']) ? 'EXISTS' : 'NOT EXISTS'; $ignoreAutoFilter = true; $column = $leadSegmentFilter->getField(); @@ -189,22 +183,22 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { } $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') - ->from(MAUTIC_TABLE_PREFIX . 'page_hits', $alias); + ->from(MAUTIC_TABLE_PREFIX.'page_hits', $alias); switch ($func) { case 'eq': case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); $subqb->where($q->expr()->andX($q->expr() - ->eq($alias . '.' . $column, $exprParameter), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->eq($alias.'.'.$column, $exprParameter), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); break; case 'regexp': case 'notRegexp': $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); $not = ($func === 'notRegexp') ? ' NOT' : ''; $subqb->where($q->expr()->andX($q->expr() - ->eq($alias . '.lead_id', 'l.id'), $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter)); + ->eq($alias.'.lead_id', 'l.id'), $alias.'.'.$column.$not.' REGEXP '.$exprParameter)); break; case 'like': case 'notLike': @@ -215,19 +209,19 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { case 'like': case 'notLike': case 'contains': - $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; break; case 'startsWith': - $parameters[$parameter] = $leadSegmentFilter->getFilter() . '%'; + $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; break; case 'endsWith': - $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter(); + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); break; } $subqb->where($q->expr()->andX($q->expr() - ->like($alias . '.' . $column, $exprParameter), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->like($alias.'.'.$column, $exprParameter), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); break; } @@ -239,28 +233,28 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $column = $leadSegmentFilter->getField(); $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') - ->from(MAUTIC_TABLE_PREFIX . 'lead_devices', $alias); + ->from(MAUTIC_TABLE_PREFIX.'lead_devices', $alias); switch ($func) { case 'eq': case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); $subqb->where($q->expr()->andX($q->expr() - ->eq($alias . '.' . $column, $exprParameter), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->eq($alias.'.'.$column, $exprParameter), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); break; case 'like': case '!like': - $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; $subqb->where($q->expr()->andX($q->expr() - ->like($alias . '.' . $column, $exprParameter), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->like($alias.'.'.$column, $exprParameter), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); break; case 'regexp': case 'notRegexp': $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); $not = ($func === 'notRegexp') ? ' NOT' : ''; $subqb->where($q->expr()->andX($q->expr() - ->eq($alias . '.lead_id', 'l.id'), $alias . '.' . $column . $not . ' REGEXP ' . $exprParameter)); + ->eq($alias.'.lead_id', 'l.id'), $alias.'.'.$column.$not.' REGEXP '.$exprParameter)); break; } @@ -278,14 +272,12 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $table = 'email_stats'; } - if ($filterField == 'lead_email_read_date') { var_dump($func); } $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') - ->from(MAUTIC_TABLE_PREFIX . $table, $alias); - + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); switch ($func) { case 'eq': @@ -293,8 +285,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $parameters[$parameter] = $leadSegmentFilter->getFilter(); $subqb->where($q->expr()->andX($q->expr() - ->eq($alias . '.' . $column, $exprParameter), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->eq($alias.'.'.$column, $exprParameter), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); break; case 'between': case 'notBetween': @@ -308,15 +300,14 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { if ($func === 'between') { $subqb->where($q->expr()->andX($q->expr() - ->gte($alias . '.' . $field, $exprParameter), $q->expr() - ->lt($alias . '.' . $field, $exprParameter2), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); - } - else { + ->gte($alias.'.'.$field, $exprParameter), $q->expr() + ->lt($alias.'.'.$field, $exprParameter2), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); + } else { $subqb->where($q->expr()->andX($q->expr() - ->lt($alias . '.' . $field, $exprParameter), $q->expr() - ->gte($alias . '.' . $field, $exprParameter2), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->lt($alias.'.'.$field, $exprParameter), $q->expr() + ->gte($alias.'.'.$field, $exprParameter2), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); } break; default: @@ -328,8 +319,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $parameters[$parameter2] = $leadSegmentFilter->getFilter(); $subqb->where($q->expr()->andX($q->expr() - ->$func($alias . '.' . $column, $parameter2), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + ->$func($alias.'.'.$column, $parameter2), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); break; } $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); @@ -349,15 +340,14 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { } $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) - ->from(MAUTIC_TABLE_PREFIX . $table, $alias); + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); if ($leadSegmentFilter->getFilter() == 1) { - $subqb->where($q->expr()->andX($q->expr()->isNotNull($alias . '.' . $column), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); - } - else { - $subqb->where($q->expr()->andX($q->expr()->isNull($alias . '.' . $column), $q->expr() - ->eq($alias . '.lead_id', 'l.id'))); + $subqb->where($q->expr()->andX($q->expr()->isNotNull($alias.'.'.$column), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); + } else { + $subqb->where($q->expr()->andX($q->expr()->isNull($alias.'.'.$column), $q->expr() + ->eq($alias.'.lead_id', 'l.id'))); } $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); @@ -367,21 +357,21 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $table = 'page_hits'; $select = 'COUNT(id)'; $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) - ->from(MAUTIC_TABLE_PREFIX . $table, $alias); + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); $alias2 = $this->generateRandomParameterName(); - $subqb2 = $this->entityManager->getConnection()->createQueryBuilder()->select($alias2 . '.id') - ->from(MAUTIC_TABLE_PREFIX . $table, $alias2); + $subqb2 = $this->entityManager->getConnection()->createQueryBuilder()->select($alias2.'.id') + ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); - $subqb2->where($q->expr()->andX($q->expr()->eq($alias2 . '.lead_id', 'l.id'), $q->expr() - ->gt($alias2 . '.date_hit', '(' . $alias . '.date_hit - INTERVAL 30 MINUTE)'), $q->expr() - ->lt($alias2 . '.date_hit', $alias . '.date_hit'))); + $subqb2->where($q->expr()->andX($q->expr()->eq($alias2.'.lead_id', 'l.id'), $q->expr() + ->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), $q->expr() + ->lt($alias2.'.date_hit', $alias.'.date_hit'))); $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where($q->expr()->andX($q->expr()->eq($alias . '.lead_id', 'l.id'), $q->expr() - ->isNull($alias . '.email_id'), $q->expr() - ->isNull($alias . '.redirect_id'), sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()))); + $subqb->where($q->expr()->andX($q->expr()->eq($alias.'.lead_id', 'l.id'), $q->expr() + ->isNull($alias.'.email_id'), $q->expr() + ->isNull($alias.'.redirect_id'), sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()))); $opr = ''; switch ($func) { @@ -403,7 +393,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { } if ($opr) { $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->having($select . $opr . $leadSegmentFilter->getFilter()); + $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); } $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); break; @@ -417,10 +407,10 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $select = 'COALESCE(SUM(open_count),0)'; } $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) - ->from(MAUTIC_TABLE_PREFIX . $table, $alias); + ->from(MAUTIC_TABLE_PREFIX.$table, $alias); $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where($q->expr()->andX($q->expr()->eq($alias . '.lead_id', 'l.id'))); + $subqb->where($q->expr()->andX($q->expr()->eq($alias.'.lead_id', 'l.id'))); $opr = ''; switch ($func) { @@ -443,7 +433,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { if ($opr) { $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->having($select . $opr . $leadSegmentFilter->getFilter()); + $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); } $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); @@ -465,11 +455,11 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $channelParameter = $this->generateRandomParameterName(); $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') - ->from(MAUTIC_TABLE_PREFIX . 'lead_donotcontact', $alias) + ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) ->where($q->expr()->andX($q->expr() - ->eq($alias . '.reason', $exprParameter), $q->expr() - ->eq($alias . '.lead_id', 'l.id'), $q->expr() - ->eq($alias . '.channel', ":$channelParameter"))); + ->eq($alias.'.reason', $exprParameter), $q->expr() + ->eq($alias.'.lead_id', 'l.id'), $q->expr() + ->eq($alias.'.channel', ":$channelParameter"))); $groupExpr->add(sprintf('%s (%s)', $func, $subqb->getSQL())); @@ -493,9 +483,9 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; $ignoreAutoFilter = true; - if ($filterListIds = (array)$leadSegmentFilter->getFilter()) { + if ($filterListIds = (array) $leadSegmentFilter->getFilter()) { $listQb = $this->entityManager->getConnection()->createQueryBuilder()->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX . 'lead_lists', 'l'); + ->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'l'); $listQb->where($listQb->expr()->in('l.id', $filterListIds)); $filterLists = $listQb->execute()->fetchAll(); $not = 'NOT EXISTS' === $func; @@ -505,8 +495,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { foreach ($filterLists as $list) { $alias = $this->generateRandomParameterName(); - $id = (int)$list['id']; - if ($id === (int)$listId) { + $id = (int) $list['id']; + if ($id === (int) $listId) { // Ignore as somehow self is included in the list continue; } @@ -514,9 +504,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $listFilters = unserialize($list['filters']); if (empty($listFilters)) { // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list - $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $id, $parameters, [$alias . '.manually_removed' => $falseParameter,]); - } - else { + $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $id, $parameters, [$alias.'.manually_removed' => $falseParameter]); + } else { // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet // but also leverage the current membership to take into account those manually added or removed from the segment @@ -526,7 +515,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { // Left join membership to account for manually added and removed $membershipAlias = $this->generateRandomParameterName(); - $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX . $table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") + $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX.$table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") ->where($subQb->expr()->orX($filterExpr, $subQb->expr() ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr() @@ -574,7 +563,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $column = 'email_id'; $trueParameter = $this->generateRandomParameterName(); - $subQueryFilters[$alias . '.is_read'] = $trueParameter; + $subQueryFilters[$alias.'.is_read'] = $trueParameter; $parameters[$trueParameter] = true; break; case 'lead_email_sent': @@ -606,7 +595,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { // if performance is desired. $subQb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') - ->from(MAUTIC_TABLE_PREFIX . 'stages', $alias); + ->from(MAUTIC_TABLE_PREFIX.'stages', $alias); switch ($func) { case 'empty': @@ -618,15 +607,15 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { case 'eq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subQb->where($q->expr()->andX($q->expr()->eq($alias . '.id', 'l.stage_id'), $q->expr() - ->eq($alias . '.id', ":$parameter"))); + $subQb->where($q->expr()->andX($q->expr()->eq($alias.'.id', 'l.stage_id'), $q->expr() + ->eq($alias.'.id', ":$parameter"))); $groupExpr->add(sprintf('EXISTS (%s)', $subQb->getSQL())); break; case 'neq': $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subQb->where($q->expr()->andX($q->expr()->eq($alias . '.id', 'l.stage_id'), $q->expr() - ->eq($alias . '.id', ":$parameter"))); + $subQb->where($q->expr()->andX($q->expr()->eq($alias.'.id', 'l.stage_id'), $q->expr() + ->eq($alias.'.id', ":$parameter"))); $groupExpr->add(sprintf('NOT EXISTS (%s)', $subQb->getSQL())); break; } @@ -638,14 +627,13 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $ignoreAutoFilter = true; $subQb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') - ->from(MAUTIC_TABLE_PREFIX . 'integration_entity', $alias); + ->from(MAUTIC_TABLE_PREFIX.'integration_entity', $alias); switch ($func) { case 'eq': case 'neq': if (strpos($leadSegmentFilter->getFilter(), '::') !== false) { list($integrationName, $campaignId) = explode('::', $leadSegmentFilter->getFilter()); - } - else { + } else { // Assuming this is a Salesforce integration for BC with pre 2.11.0 $integrationName = 'Salesforce'; $campaignId = $leadSegmentFilter->getFilter(); @@ -654,11 +642,11 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $parameters[$parameter] = $campaignId; $parameters[$parameter2] = $integrationName; $subQb->where($q->expr()->andX($q->expr() - ->eq($alias . '.integration', ":$parameter2"), $q->expr() - ->eq($alias . '.integration_entity', "'CampaignMember'"), $q->expr() - ->eq($alias . '.integration_entity_id', ":$parameter"), $q->expr() - ->eq($alias . '.internal_entity', "'lead'"), $q->expr() - ->eq($alias . '.internal_entity_id', 'l.id'))); + ->eq($alias.'.integration', ":$parameter2"), $q->expr() + ->eq($alias.'.integration_entity', "'CampaignMember'"), $q->expr() + ->eq($alias.'.integration_entity_id', ":$parameter"), $q->expr() + ->eq($alias.'.internal_entity', "'lead'"), $q->expr() + ->eq($alias.'.internal_entity_id', 'l.id'))); break; } @@ -684,8 +672,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { if ($func === 'between') { $groupExpr->add($q->expr()->andX($q->expr()->gte($field, $exprParameter), $q->expr() ->lt($field, $exprParameter2))); - } - else { + } else { $groupExpr->add($q->expr()->andX($q->expr()->lt($field, $exprParameter), $q->expr() ->gte($field, $exprParameter2))); } @@ -717,15 +704,13 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { if (substr($func, 0, 3) === 'not') { $operator = 'NOT REGEXP'; - } - else { + } else { $operator = 'REGEXP'; } - $groupExpr->add($field . " $operator '\\\\|?$filter\\\\|?'"); + $groupExpr->add($field." $operator '\\\\|?$filter\\\\|?'"); } - } - else { + } else { $groupExpr->add($this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null)); } $ignoreAutoFilter = true; @@ -745,19 +730,19 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { switch ($func) { case 'like': case 'notLike': - $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%' . $leadSegmentFilter->getFilter() . '%' : $leadSegmentFilter->getFilter(); + $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%'.$leadSegmentFilter->getFilter().'%' : $leadSegmentFilter->getFilter(); break; case 'startsWith': $func = 'like'; - $parameters[$parameter] = $leadSegmentFilter->getFilter() . '%'; + $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; break; case 'endsWith': $func = 'like'; - $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter(); + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); break; case 'contains': $func = 'like'; - $parameters[$parameter] = '%' . $leadSegmentFilter->getFilter() . '%'; + $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; break; } @@ -769,7 +754,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); $not = ($func === 'notRegexp') ? ' NOT' : ''; $groupExpr->add(// Escape single quotes while accounting for those that may already be escaped - $field . $not . ' REGEXP ' . $exprParameter); + $field.$not.' REGEXP '.$exprParameter); break; default: $ignoreAutoFilter = true; @@ -790,7 +775,6 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { } } - // Get the last of the filters if ($groupExpr->count()) { $groups[] = $groupExpr; @@ -798,16 +782,14 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { if (count($groups) === 1) { // Only one andX expression $expr = $groups[0]; - } - elseif (count($groups) > 1) { + } elseif (count($groups) > 1) { // Sets of expressions grouped by OR $orX = $q->expr()->orX(); $orX->addMultiple($groups); // Wrap in a andX for other functions to append $expr = $q->expr()->andX($orX); - } - else { + } else { $expr = $groupExpr; } @@ -831,7 +813,8 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) { * * @return string */ - private function generateRandomParameterName() { + private function generateRandomParameterName() + { return $this->randomParameterName->generateRandomParameterName(); } @@ -840,11 +823,12 @@ private function generateRandomParameterName() { * @param $column * @param $operator * @param $parameter - * @param $includeIsNull true/false or null to auto determine based on operator + * @param $includeIsNull true/false or null to auto determine based on operator * * @return mixed */ - public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) { + public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) + { // in/notIn for dbal will use a raw array if (!is_array($parameter) && strpos($parameter, ':') !== 0) { $parameter = ":$parameter"; @@ -857,8 +841,7 @@ public function generateFilterExpression($q, $column, $operator, $parameter, $in if ($includeIsNull) { $expr = $q->expr()->orX($q->expr()->$operator($column, $parameter), $q->expr()->isNull($column)); - } - else { + } else { $expr = $q->expr()->$operator($column, $parameter); } @@ -876,12 +859,13 @@ public function generateFilterExpression($q, $column, $operator, $parameter, $in * * @return QueryBuilder */ - protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) { + protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) + { $subQb = $this->entityManager->getConnection()->createQueryBuilder(); $subExpr = $subQb->expr()->andX(); if ('leads' !== $table) { - $subExpr->add($subQb->expr()->eq($alias . '.lead_id', 'l.id')); + $subExpr->add($subQb->expr()->eq($alias.'.lead_id', 'l.id')); } foreach ($subQueryFilters as $subColumn => $subParameter) { @@ -895,15 +879,14 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu $subFunc = 'in'; $subExpr->add($subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter")); $parameters[$subFilterParamter] = ['value' => $value, 'type' => \Doctrine\DBAL\Connection::PARAM_STR_ARRAY]; - } - else { + } else { $parameters[$subFilterParamter] = $value; } $subExpr->add($subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter")); } - $subQb->select('null')->from(MAUTIC_TABLE_PREFIX . $table, $alias)->where($subExpr); + $subQb->select('null')->from(MAUTIC_TABLE_PREFIX.$table, $alias)->where($subExpr); return $subQb; } @@ -915,18 +898,19 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu * @param QueryBuilder $q * @param LeadSegmentFilters $leadSegmentFilters */ - private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) { + private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) + { $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; // Join company tables for query optimization - $q->$joinType('l', MAUTIC_TABLE_PREFIX . 'companies_leads', 'cl', 'l.id = cl.lead_id') - ->$joinType('cl', MAUTIC_TABLE_PREFIX . 'companies', 'comp', 'cl.company_id = comp.id'); + $q->$joinType('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'l.id = cl.lead_id') + ->$joinType('cl', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'cl.company_id = comp.id'); // Return only unique contacts $q->groupBy('l.id'); } - - private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) { + private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) + { var_dump(debug_backtrace()[1]['function']); $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); @@ -940,7 +924,8 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter /** * @return LeadSegmentFilterDescriptor */ - public function getTranslator() { + public function getTranslator() + { return $this->translator; } @@ -949,15 +934,18 @@ public function getTranslator() { * * @return LeadSegmentQueryBuilder */ - public function setTranslator($translator) { + public function setTranslator($translator) + { $this->translator = $translator; + return $this; } /** * @return \Doctrine\DBAL\Schema\AbstractSchemaManager */ - public function getSchema() { + public function getSchema() + { return $this->schema; } @@ -966,10 +954,10 @@ public function getSchema() { * * @return LeadSegmentQueryBuilder */ - public function setSchema($schema) { + public function setSchema($schema) + { $this->schema = $schema; + return $this; } - - } From e3d2bccd8ee3ab5791246fb9598c13e18c295831 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 11 Jan 2018 14:29:55 +0100 Subject: [PATCH 020/778] Lead segment filter split to Crate + added base decorator --- app/bundles/LeadBundle/Config/config.php | 14 +- .../Segment/Decorator/BaseDecorator.php | 48 ++++- .../Decorator/FilterDecoratorInterface.php | 37 ++++ .../LeadBundle/Segment/LeadSegmentFilter.php | 135 +++---------- .../Segment/LeadSegmentFilterCrate.php | 186 ++++++++++++++++++ .../Segment/LeadSegmentFilterFactory.php | 54 +++-- .../Segment/LeadSegmentFilterOperator.php | 12 +- 7 files changed, 341 insertions(+), 145 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 20a3648f3b6..3d2cf8a691a 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -766,7 +766,7 @@ 'class' => \Mautic\LeadBundle\Services\LeadSegmentQueryBuilder::class, 'arguments' => [ 'doctrine.orm.entity_manager', - 'mautic.lead.model.random_parameter_name' + 'mautic.lead.model.random_parameter_name', ], ], 'mautic.lead.model.lead_segment_service' => [ @@ -774,16 +774,16 @@ 'arguments' => [ 'mautic.lead.model.lead_segment_filter_factory', 'mautic.lead.repository.lead_list_segment_repository', - 'mautic.lead.repository.lead_segment_query_builder' + 'mautic.lead.repository.lead_segment_query_builder', ], ], 'mautic.lead.model.lead_segment_filter_factory' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, 'arguments' => [ 'mautic.lead.model.lead_segment_filter_date', - 'mautic.lead.model.lead_segment_filter_operator', 'mautic.lead.repository.lead_segment_filter_descriptor', - 'doctrine.orm.entity_manager' + 'doctrine.orm.entity_manager', + 'mautic.lead.model.lead_segment_decorator_base', ], ], 'mautic.lead.model.relative_date' => [ @@ -806,6 +806,12 @@ 'mautic.lead.segment.operator_options', ], ], + 'mautic.lead.model.lead_segment_decorator_base' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment_filter_operator', + ], + ], 'mautic.lead.model.random_parameter_name' => [ 'class' => \Mautic\LeadBundle\Segment\RandomParameterName::class, ], diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index a4a54016f99..70057366fb3 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -1,13 +1,49 @@ leadSegmentFilterOperator = $leadSegmentFilterOperator; + } + + public function getField() + { + } + + public function getTable() + { + } + + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilterCrate->getOperator()); + } + + public function getParameterHolder($argument) + { + } + + public function getParameterValue() + { + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php new file mode 100644 index 00000000000..55f5fa57dbd --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -0,0 +1,37 @@ +glue = isset($filter['glue']) ? $filter['glue'] : null; - $this->field = isset($filter['field']) ? $filter['field'] : null; - $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; - $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->display = isset($filter['display']) ? $filter['display'] : null; - $this->func = isset($filter['func']) ? $filter['func'] : null; - $operatorValue = isset($filter['operator']) ? $filter['operator'] : null; - $this->setOperator($operatorValue); - - $filterValue = isset($filter['filter']) ? $filter['filter'] : null; - $this->setFilter($filterValue); - $this->em = $em; + public function __construct( + LeadSegmentFilterCrate $leadSegmentFilterCrate, + FilterDecoratorInterface $filterDecorator, + \ArrayIterator $dictionary = null, + EntityManager $em = null + ) { + $this->leadSegmentFilterCrate = $leadSegmentFilterCrate; + $this->filterDecorator = $filterDecorator; + $this->em = $em; if (!is_null($dictionary)) { $this->translateQueryDescription($dictionary); } @@ -240,7 +203,7 @@ public function getDBColumn() */ public function getGlue() { - return $this->glue; + return $this->leadSegmentFilterCrate->getGlue(); } /** @@ -248,7 +211,7 @@ public function getGlue() */ public function getField() { - return $this->field; + return $this->leadSegmentFilterCrate->getField(); } /** @@ -256,7 +219,7 @@ public function getField() */ public function getObject() { - return $this->object; + return $this->leadSegmentFilterCrate->getObject(); } /** @@ -264,7 +227,7 @@ public function getObject() */ public function isLeadType() { - return $this->object === self::LEAD_OBJECT; + return $this->leadSegmentFilterCrate->isLeadType(); } /** @@ -272,7 +235,7 @@ public function isLeadType() */ public function isCompanyType() { - return $this->object === self::COMPANY_OBJECT; + return $this->leadSegmentFilterCrate->isCompanyType(); } /** @@ -280,7 +243,7 @@ public function isCompanyType() */ public function getType() { - return $this->type; + return $this->leadSegmentFilterCrate->getType(); } /** @@ -288,7 +251,7 @@ public function getType() */ public function getFilter() { - return $this->filter; + return $this->leadSegmentFilterCrate->getFilter(); } /** @@ -296,7 +259,7 @@ public function getFilter() */ public function getDisplay() { - return $this->display; + return $this->leadSegmentFilterCrate->getDisplay(); } /** @@ -304,25 +267,7 @@ public function getDisplay() */ public function getOperator() { - return $this->operator; - } - - /** - * @param string|null $operator - */ - public function setOperator($operator) - { - $this->operator = $operator; - } - - /** - * @param string|array|bool|float|null $filter - */ - public function setFilter($filter) - { - $filter = $this->sanitizeFilter($filter); - - $this->filter = $filter; + return $this->filterDecorator->getOperator($this->leadSegmentFilterCrate); } /** @@ -330,15 +275,7 @@ public function setFilter($filter) */ public function getFunc() { - return $this->func; - } - - /** - * @param string $func - */ - public function setFunc($func) - { - $this->func = $func; + return $this->leadSegmentFilterCrate->getFunc(); } /** @@ -358,30 +295,6 @@ public function toArray() ]; } - /** - * @param string|array|bool|float|null $filter - * - * @return string|array|bool|float|null - */ - private function sanitizeFilter($filter) - { - if ($filter === null || is_array($filter) || !$this->getType()) { - return $filter; - } - - switch ($this->getType()) { - case 'number': - $filter = (float) $filter; - break; - - case 'boolean': - $filter = (bool) $filter; - break; - } - - return $filter; - } - /** * @return array */ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php new file mode 100644 index 00000000000..425aa447dd1 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php @@ -0,0 +1,186 @@ +glue = isset($filter['glue']) ? $filter['glue'] : null; + $this->field = isset($filter['field']) ? $filter['field'] : null; + $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; + $this->type = isset($filter['type']) ? $filter['type'] : null; + $this->display = isset($filter['display']) ? $filter['display'] : null; + $this->func = isset($filter['func']) ? $filter['func'] : null; + $this->operator = isset($filter['operator']) ? $filter['operator'] : null; + + $filterValue = isset($filter['filter']) ? $filter['filter'] : null; + $this->setFilter($filterValue); + } + + /** + * @return string|null + */ + public function getGlue() + { + return $this->glue; + } + + /** + * @return string|null + */ + public function getField() + { + return $this->field; + } + + /** + * @return string|null + */ + public function getObject() + { + return $this->object; + } + + /** + * @return bool + */ + public function isLeadType() + { + return $this->object === self::LEAD_OBJECT; + } + + /** + * @return bool + */ + public function isCompanyType() + { + return $this->object === self::COMPANY_OBJECT; + } + + /** + * @return string|null + */ + public function getType() + { + return $this->type; + } + + /** + * @return string|array|null + */ + public function getFilter() + { + return $this->filter; + } + + /** + * @return string|null + */ + public function getDisplay() + { + return $this->display; + } + + /** + * @return string|null + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @param string|array|bool|float|null $filter + */ + public function setFilter($filter) + { + $filter = $this->sanitizeFilter($filter); + + $this->filter = $filter; + } + + /** + * @return string + */ + public function getFunc() + { + return $this->func; + } + + /** + * @param string|array|bool|float|null $filter + * + * @return string|array|bool|float|null + */ + private function sanitizeFilter($filter) + { + if ($filter === null || is_array($filter) || !$this->getType()) { + return $filter; + } + + switch ($this->getType()) { + case 'number': + $filter = (float) $filter; + break; + + case 'boolean': + $filter = (bool) $filter; + break; + } + + return $filter; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 5e66779cc21..ffc94307753 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; +use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\QueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; @@ -25,22 +26,30 @@ class LeadSegmentFilterFactory private $leadSegmentFilterDate; /** - * @var LeadSegmentFilterOperator + * @var LeadSegmentFilterDescriptor */ - private $leadSegmentFilterOperator; - - /** @var LeadSegmentFilterDescriptor */ public $dictionary; - /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ + /** + * @var \Doctrine\DBAL\Schema\AbstractSchemaManager + */ private $entityManager; - public function __construct(LeadSegmentFilterDate $leadSegmentFilterDate, LeadSegmentFilterOperator $leadSegmentFilterOperator, LeadSegmentFilterDescriptor $dictionary, EntityManager $entityManager) - { - $this->leadSegmentFilterDate = $leadSegmentFilterDate; - $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; - $this->dictionary = $dictionary; - $this->entityManager = $entityManager; + /** + * @var BaseDecorator + */ + private $baseDecorator; + + public function __construct( + LeadSegmentFilterDate $leadSegmentFilterDate, + LeadSegmentFilterDescriptor $dictionary, + EntityManager $entityManager, + BaseDecorator $baseDecorator + ) { + $this->leadSegmentFilterDate = $leadSegmentFilterDate; + $this->dictionary = $dictionary; + $this->entityManager = $entityManager; + $this->baseDecorator = $baseDecorator; } /** @@ -54,20 +63,25 @@ public function getLeadListFilters(LeadList $leadList) $filters = $leadList->getFilters(); foreach ($filters as $filter) { - $leadSegmentFilter = new LeadSegmentFilter($filter, $this->dictionary, $this->entityManager); + // LeadSegmentFilterCrate is for accessing $filter as an object + $leadSegmentFilterCrate = new LeadSegmentFilterCrate($filter); + + $decorator = $this->getDecoratorForFilter($leadSegmentFilterCrate); + $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->dictionary, $this->entityManager); + //$this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); $leadSegmentFilter->setQueryDescription( isset($this->dictionary[$leadSegmentFilter->getField()]) ? $this->dictionary[$leadSegmentFilter->getField()] : false ); $leadSegmentFilter->setQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); - $leadSegmentFilter->setDecorator($this->getDecoratorForFilter($leadSegmentFilter)); + //dump($leadSegmentFilter); + //dump($leadSegmentFilter->getOperator()); + //continue; //@todo replaced in query builder - $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilter); - $this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); } - + //die(); return $leadSegmentFilters; } @@ -82,12 +96,12 @@ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) } /** - * @param LeadSegmentFilter $filter + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate * - * @return BaseDecorator + * @return FilterDecoratorInterface */ - protected function getDecoratorForFilter(LeadSegmentFilter $filter) + protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return new BaseDecorator(); + return $this->baseDecorator; } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php index 751c9db2578..8b25b1aec45 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php @@ -43,7 +43,12 @@ public function __construct( $this->operatorOptions = $operatorOptions; } - public function fixOperator(LeadSegmentFilter $leadSegmentFilter) + /** + * @param string $operator + * + * @return string + */ + public function fixOperator($operator) { $options = $this->operatorOptions->getFilterExpressionFunctionsNonStatic(); @@ -52,9 +57,8 @@ public function fixOperator(LeadSegmentFilter $leadSegmentFilter) $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_OPERATORS_ON_GENERATE, $event); $options = $event->getOperators(); - $operatorDetails = $options[$leadSegmentFilter->getOperator()]; - $func = $operatorDetails['expr']; + $operatorDetails = $options[$operator]; - $leadSegmentFilter->setFunc($func); + return $operatorDetails['expr']; } } From eb89d44d5edeedcbee1879cc95258b79f5acb8b7 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 14:52:16 +0100 Subject: [PATCH 021/778] unifying --- .../LeadBundle/Segment/LeadSegmentFilter.php | 106 ++++++------------ .../Segment/LeadSegmentFilterFactory.php | 2 +- 2 files changed, 33 insertions(+), 75 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 880ed14f7f8..8d761ba796d 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -51,8 +51,30 @@ class LeadSegmentFilter /** @var Column */ private $dbColumn; - /** @var EntityManager */ - private $em; + public function getField() + { + throw new \Exception('Not implemented'); + } + + public function getTable() + { + throw new \Exception('Not implemented'); + } + + public function getOperator() + { + throw new \Exception('Not implemented'); + } + + public function getParameterHolder() + { + throw new \Exception('Not implemented'); + } + + public function getParameterValue() + { + throw new \Exception('Not implemented'); + } public function __construct( LeadSegmentFilterCrate $leadSegmentFilterCrate, @@ -209,70 +231,6 @@ public function getDBColumn() return $this->dbColumn; } - /** - * @return string|null - */ - public function getGlue() - { - return $this->leadSegmentFilterCrate->getGlue(); - } - - /** - * @return string|null - */ - public function getField() - { - return $this->leadSegmentFilterCrate->getField(); - } - - /** - * @return string|null - */ - public function getObject() - { - return $this->leadSegmentFilterCrate->getObject(); - } - - /** - * @return bool - */ - public function isLeadType() - { - return $this->leadSegmentFilterCrate->isLeadType(); - } - - /** - * @return bool - */ - public function isCompanyType() - { - return $this->leadSegmentFilterCrate->isCompanyType(); - } - - /** - * @return string|null - */ - public function getType() - { - return $this->leadSegmentFilterCrate->getType(); - } - - /** - * @return string|array|null - */ - public function getFilter() - { - return $this->leadSegmentFilterCrate->getFilter(); - } - - /** - * @return string|null - */ - public function getDisplay() - { - return $this->leadSegmentFilterCrate->getDisplay(); - } - /** * @return string|null */ @@ -281,14 +239,6 @@ public function getOperator() return $this->filterDecorator->getOperator($this->leadSegmentFilterCrate); } - /** - * @return string - */ - public function getFunc() - { - return $this->leadSegmentFilterCrate->getFunc(); - } - /** * @return array */ @@ -361,4 +311,12 @@ public function setQueryBuilder($queryBuilder) return $this; } + + /** + * @return string + */ + public function __toString() + { + return sprintf('%s %s = %s', $this->getObject(), $this->getField(), $this->getField()); + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index ffc94307753..d2bf31f0d8e 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -15,7 +15,7 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\QueryBuilder\BaseFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class LeadSegmentFilterFactory From 6eb683b429335da59728029a33cb28410973a839 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 11 Jan 2018 15:02:18 +0100 Subject: [PATCH 022/778] Move filter descriptor to decorator --- app/bundles/LeadBundle/Config/config.php | 2 +- .../Segment/Decorator/BaseDecorator.php | 15 ++- .../Decorator/FilterDecoratorInterface.php | 2 - .../LeadBundle/Segment/LeadSegmentFilter.php | 96 ++++--------------- .../Segment/LeadSegmentFilterFactory.php | 22 ++--- 5 files changed, 40 insertions(+), 97 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 3d2cf8a691a..12f61a9bce4 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -781,7 +781,6 @@ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, 'arguments' => [ 'mautic.lead.model.lead_segment_filter_date', - 'mautic.lead.repository.lead_segment_filter_descriptor', 'doctrine.orm.entity_manager', 'mautic.lead.model.lead_segment_decorator_base', ], @@ -810,6 +809,7 @@ 'class' => \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::class, 'arguments' => [ 'mautic.lead.model.lead_segment_filter_operator', + 'mautic.lead.repository.lead_segment_filter_descriptor', ], ], 'mautic.lead.model.random_parameter_name' => [ diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 70057366fb3..78283d78b76 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -13,6 +13,7 @@ use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\LeadSegmentFilterOperator; +use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class BaseDecorator implements FilterDecoratorInterface { @@ -21,9 +22,17 @@ class BaseDecorator implements FilterDecoratorInterface */ private $leadSegmentFilterOperator; - public function __construct(LeadSegmentFilterOperator $leadSegmentFilterOperator) - { - $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; + /** + * @var LeadSegmentFilterDescriptor + */ + private $leadSegmentFilterDescriptor; + + public function __construct( + LeadSegmentFilterOperator $leadSegmentFilterOperator, + LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor + ) { + $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; + $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; } public function getField() diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index 55f5fa57dbd..c09ba35cea3 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -25,8 +25,6 @@ public function getTable(); public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate); - // To, co vrati LeadSegmentFilterOperator - public function getParameterHolder($argument); // vrati ":$argument", pripadne pro like "%:$argument%", date between vrati pole: [$startWith, $endWith] diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 8d761ba796d..71da515700b 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -16,7 +16,7 @@ use Doctrine\DBAL\Schema\Column; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\QueryBuilder\BaseFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -39,55 +39,17 @@ class LeadSegmentFilter */ private $queryBuilder; - /** - * @var BaseDecorator - */ - private $decorator; - /** - * @var array - */ - private $queryDescription = null; - /** @var Column */ private $dbColumn; - public function getField() - { - throw new \Exception('Not implemented'); - } - - public function getTable() - { - throw new \Exception('Not implemented'); - } - - public function getOperator() - { - throw new \Exception('Not implemented'); - } - - public function getParameterHolder() - { - throw new \Exception('Not implemented'); - } - - public function getParameterValue() - { - throw new \Exception('Not implemented'); - } - public function __construct( LeadSegmentFilterCrate $leadSegmentFilterCrate, FilterDecoratorInterface $filterDecorator, - \ArrayIterator $dictionary = null, EntityManager $em = null ) { $this->leadSegmentFilterCrate = $leadSegmentFilterCrate; $this->filterDecorator = $filterDecorator; $this->em = $em; - if (!is_null($dictionary)) { - $this->translateQueryDescription($dictionary); - } } /** @@ -239,6 +201,26 @@ public function getOperator() return $this->filterDecorator->getOperator($this->leadSegmentFilterCrate); } + public function getField() + { + throw new \Exception('Not implemented'); + } + + public function getTable() + { + throw new \Exception('Not implemented'); + } + + public function getParameterHolder() + { + throw new \Exception('Not implemented'); + } + + public function getParameterValue() + { + throw new \Exception('Not implemented'); + } + /** * @return array */ @@ -256,42 +238,6 @@ public function toArray() ]; } - /** - * @return array - */ - public function getQueryDescription($dictionary = null) - { - if (is_null($this->queryDescription)) { - $this->translateQueryDescription($dictionary); - } - - return $this->queryDescription; - } - - /** - * @param array $queryDescription - * - * @return LeadSegmentFilter - */ - public function setQueryDescription($queryDescription) - { - $this->queryDescription = $queryDescription; - - return $this; - } - - /** - * @return $this - */ - public function translateQueryDescription(\ArrayIterator $dictionary = null) - { - $this->queryDescription = isset($dictionary[$this->getField()]) - ? $dictionary[$this->getField()] - : false; - - return $this; - } - /** * @return BaseFilterQueryBuilder */ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index d2bf31f0d8e..58e9d5c8af0 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -16,7 +16,6 @@ use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; -use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class LeadSegmentFilterFactory { @@ -25,11 +24,6 @@ class LeadSegmentFilterFactory */ private $leadSegmentFilterDate; - /** - * @var LeadSegmentFilterDescriptor - */ - public $dictionary; - /** * @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ @@ -42,12 +36,10 @@ class LeadSegmentFilterFactory public function __construct( LeadSegmentFilterDate $leadSegmentFilterDate, - LeadSegmentFilterDescriptor $dictionary, EntityManager $entityManager, BaseDecorator $baseDecorator ) { $this->leadSegmentFilterDate = $leadSegmentFilterDate; - $this->dictionary = $dictionary; $this->entityManager = $entityManager; $this->baseDecorator = $baseDecorator; } @@ -68,20 +60,18 @@ public function getLeadListFilters(LeadList $leadList) $decorator = $this->getDecoratorForFilter($leadSegmentFilterCrate); - $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->dictionary, $this->entityManager); + $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->entityManager); //$this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); - $leadSegmentFilter->setQueryDescription( - isset($this->dictionary[$leadSegmentFilter->getField()]) ? $this->dictionary[$leadSegmentFilter->getField()] : false - ); $leadSegmentFilter->setQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); - //dump($leadSegmentFilter); - //dump($leadSegmentFilter->getOperator()); - //continue; + dump($leadSegmentFilter); + dump($leadSegmentFilter->getOperator()); + continue; //@todo replaced in query builder $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); } - //die(); + die(); + return $leadSegmentFilters; } From 109f07a0a741ab6cbf12c71a26c868825521da6b Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 15:09:19 +0100 Subject: [PATCH 023/778] filter cleanup --- .../LeadBundle/Segment/LeadSegmentFilter.php | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 71da515700b..93ffb34e784 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment; -use Doctrine\Common\Persistence\Mapping\MappingException; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Schema\Column; use Doctrine\ORM\EntityManager; @@ -59,7 +58,7 @@ public function __construct( * * @throws \Exception */ - public function getFilterConditionValue($argument = null) + public function XXgetFilterConditionValue($argument = null) { switch ($this->getDBColumn()->getType()->getName()) { case 'number': @@ -88,7 +87,7 @@ public function getFilterConditionValue($argument = null) throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); } - public function createQuery(QueryBuilder $queryBuilder, $alias = false) + public function XXcreateQuery(QueryBuilder $queryBuilder, $alias = false) { dump('creating query:'.$this->getObject()); $glueFunc = $this->getGlue().'Where'; @@ -104,7 +103,7 @@ public function createQuery(QueryBuilder $queryBuilder, $alias = false) return $queryBuilder; } - public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) + public function XXcreateExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) { dump('creating query:'.$this->getField()); $func = is_null($func) ? $this->getFunc() : $func; @@ -141,19 +140,7 @@ public function createExpression(QueryBuilder $queryBuilder, $parameterName, $fu return $queryBuilder; } - public function getDBTable() - { - //@todo cache metadata - try { - $tableName = $this->em->getClassMetadata($this->getEntityName())->getTableName(); - } catch (MappingException $e) { - return $this->getObject(); - } - - return $tableName; - } - - public function getEntityName() + public function getEntity() { $converter = new CamelCaseToSnakeCaseNameConverter(); if ($this->getQueryDescription()) { @@ -172,7 +159,7 @@ public function getEntityName() * * @throws \Exception */ - public function getDBColumn() + public function XXgetDBColumn() { if (is_null($this->dbColumn)) { if ($descr = $this->getQueryDescription()) { From 756502a15010632db73a99f2504a3a9ef45166ba Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 11 Jan 2018 15:09:51 +0100 Subject: [PATCH 024/778] getField implemented --- .../LeadBundle/Segment/Decorator/BaseDecorator.php | 9 ++++++++- .../Segment/Decorator/FilterDecoratorInterface.php | 2 +- app/bundles/LeadBundle/Segment/LeadSegmentFilter.php | 2 +- .../LeadBundle/Segment/LeadSegmentFilterFactory.php | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 78283d78b76..830577a7eba 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -35,8 +35,15 @@ public function __construct( $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; } - public function getField() + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) { + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + return $originalField; + } + + return $this->leadSegmentFilterDescriptor[$originalField]['field']; } public function getTable() diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index c09ba35cea3..05eef016535 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -15,7 +15,7 @@ interface FilterDecoratorInterface { - public function getField(); + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate); // $field, respektive preklad z queryDescription - $field diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 71da515700b..ac305e1eaa6 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -203,7 +203,7 @@ public function getOperator() public function getField() { - throw new \Exception('Not implemented'); + return $this->filterDecorator->getField($this->leadSegmentFilterCrate); } public function getTable() diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 58e9d5c8af0..111e83ac9fd 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -65,6 +65,7 @@ public function getLeadListFilters(LeadList $leadList) $leadSegmentFilter->setQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); dump($leadSegmentFilter); dump($leadSegmentFilter->getOperator()); + dump($leadSegmentFilter->getField()); continue; //@todo replaced in query builder From db0513795455669c8364ec3dc18708834e1474d6 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 11 Jan 2018 15:34:47 +0100 Subject: [PATCH 025/778] rename query builder, minor changes --- .../LeadBundle/Segment/LeadSegmentFilter.php | 14 +++++++------- .../Segment/LeadSegmentFilterFactory.php | 7 +------ .../LeadBundle/Segment/LeadSegmentFilters.php | 14 +++++++------- .../Services/LeadSegmentQueryBuilder.php | 12 ++++++------ 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index d99a9325840..09b31bfd265 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -36,7 +36,7 @@ class LeadSegmentFilter /** * @var BaseFilterQueryBuilder */ - private $queryBuilder; + private $filterQueryBuilder; /** @var Column */ private $dbColumn; @@ -228,19 +228,19 @@ public function toArray() /** * @return BaseFilterQueryBuilder */ - public function getQueryBuilder() + public function getFilterQueryBuilder() { - return $this->queryBuilder; + return $this->filterQueryBuilder; } /** - * @param BaseFilterQueryBuilder $queryBuilder + * @param BaseFilterQueryBuilder $filterQueryBuilder * * @return LeadSegmentFilter */ - public function setQueryBuilder($queryBuilder) + public function setFilterQueryBuilder($filterQueryBuilder) { - $this->queryBuilder = $queryBuilder; + $this->filterQueryBuilder = $filterQueryBuilder; return $this; } @@ -250,6 +250,6 @@ public function setQueryBuilder($queryBuilder) */ public function __toString() { - return sprintf('%s %s = %s', $this->getObject(), $this->getField(), $this->getField()); + return sprintf('%s %s = %s', $this->getTable(), $this->getField(), $this->getParameterHolder()); } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 111e83ac9fd..c7ebf0027a7 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -62,16 +62,11 @@ public function getLeadListFilters(LeadList $leadList) $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->entityManager); //$this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); - $leadSegmentFilter->setQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); - dump($leadSegmentFilter); - dump($leadSegmentFilter->getOperator()); - dump($leadSegmentFilter->getField()); - continue; + $leadSegmentFilter->setFilterQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); //@todo replaced in query builder $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); } - die(); return $leadSegmentFilters; } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php index 9d7aa217be3..522fff32eb8 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilters.php @@ -36,13 +36,13 @@ class LeadSegmentFilters implements \Iterator, \Countable public function addLeadSegmentFilter(LeadSegmentFilter $leadSegmentFilter) { $this->leadSegmentFilters[] = $leadSegmentFilter; - if ($leadSegmentFilter->isCompanyType()) { - $this->hasCompanyFilter = true; - // Must tell getLeadsByList how to best handle the relationship with the companies table - if (!in_array($leadSegmentFilter->getFunc(), ['empty', 'neq', 'notIn', 'notLike'], true)) { - $this->listFiltersInnerJoinCompany = true; - } - } +// if ($leadSegmentFilter->isCompanyType()) { +// $this->hasCompanyFilter = true; +// // Must tell getLeadsByList how to best handle the relationship with the companies table +// if (!in_array($leadSegmentFilter->getFunc(), ['empty', 'neq', 'notIn', 'notLike'], true)) { +// $this->listFiltersInnerJoinCompany = true; +// } +// } } /** diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 6770f35baef..e365acc4157 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -76,18 +76,18 @@ public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { - /** @var QueryBuilder $qb */ - $qb = new \Mautic\LeadBundle\Segment\Query\QueryBuilder($this->entityManager->getConnection()); + /** @var QueryBuilder $queryBuilder */ + $queryBuilder = new \Mautic\LeadBundle\Segment\Query\QueryBuilder($this->entityManager->getConnection()); - $qb->select('*')->from('leads', 'l'); + $queryBuilder->select('*')->from('leads', 'l'); /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { var_dump('parsing filter: '.$filter->__toString()); - $qb = $filter->getQueryBuilder(); + $queryBuilder = $filter->getFilterQueryBuilder()->applyQuery($queryBuilder); } - return $qb; + return $queryBuilder; echo 'SQL parameters:'; dump($q->getParameters()); @@ -563,7 +563,7 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $column = 'email_id'; $trueParameter = $this->generateRandomParameterName(); - $subQueryFilters[$alias.'.is_read'] = $trueParameter; + $subQueryFilters[$alias.'.is_read'] = $trueParameter; $parameters[$trueParameter] = true; break; case 'lead_email_sent': From 1bf808bee088a03b37dd86c1bea4068a06f06f61 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 11 Jan 2018 15:35:14 +0100 Subject: [PATCH 026/778] Implemented base decorator --- .../Segment/Decorator/BaseDecorator.php | 39 +++++++++++++++++-- .../Decorator/FilterDecoratorInterface.php | 14 ++----- .../LeadBundle/Segment/LeadSegmentFilter.php | 24 ++++++------ .../Segment/LeadSegmentFilterFactory.php | 3 ++ 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 830577a7eba..97ac5112d9a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -46,8 +46,19 @@ public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) return $this->leadSegmentFilterDescriptor[$originalField]['field']; } - public function getTable() + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) { + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + if ($leadSegmentFilterCrate->isLeadType()) { + return 'leads'; + } + + return 'companies'; + } + + return $this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; } public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) @@ -55,11 +66,33 @@ public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) return $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilterCrate->getOperator()); } - public function getParameterHolder($argument) + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) { + if (is_array($argument)) { + $result = []; + foreach ($argument as $arg) { + $result[] = $this->getParameterHolder($leadSegmentFilterCrate, $arg); + } + + return $result; + } + + switch ($this->getOperator($leadSegmentFilterCrate)) { + case 'like': + case 'notLike': + case 'contains': + return '%:'.$argument.'%'; + case 'startsWith': + return ':'.$argument.'%'; + case 'endsWith': + return '%:'.$argument; + } + + return ':'.$argument; } - public function getParameterValue() + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { + return $leadSegmentFilterCrate->getFilter(); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index 05eef016535..bc3989b92f6 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -17,19 +17,11 @@ interface FilterDecoratorInterface { public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate); - // $field, respektive preklad z queryDescription - $field - - public function getTable(); - - // leads nebo company - na zaklade podminky isLeadType() nebo z prekladu foreign_table + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate); public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate); - public function getParameterHolder($argument); - - // vrati ":$argument", pripadne pro like "%:$argument%", date between vrati pole: [$startWith, $endWith] - - public function getParameterValue(); + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument); - // aktualne $filter + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index ac305e1eaa6..4a3b13ee46b 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -208,17 +208,17 @@ public function getField() public function getTable() { - throw new \Exception('Not implemented'); + return $this->filterDecorator->getTable($this->leadSegmentFilterCrate); } - public function getParameterHolder() + public function getParameterHolder($argument) { - throw new \Exception('Not implemented'); + return $this->filterDecorator->getParameterHolder($this->leadSegmentFilterCrate, $argument); } public function getParameterValue() { - throw new \Exception('Not implemented'); + return $this->filterDecorator->getParameterValue($this->leadSegmentFilterCrate); } /** @@ -227,14 +227,14 @@ public function getParameterValue() public function toArray() { return [ - 'glue' => $this->getGlue(), - 'field' => $this->getField(), - 'object' => $this->getObject(), - 'type' => $this->getType(), - 'filter' => $this->getFilter(), - 'display' => $this->getDisplay(), - 'operator' => $this->getOperator(), - 'func' => $this->getFunc(), + 'glue' => $this->leadSegmentFilterCrate->getGlue(), + 'field' => $this->leadSegmentFilterCrate->getField(), + 'object' => $this->leadSegmentFilterCrate->getObject(), + 'type' => $this->leadSegmentFilterCrate->getType(), + 'filter' => $this->leadSegmentFilterCrate->getFilter(), + 'display' => $this->leadSegmentFilterCrate->getDisplay(), + 'operator' => $this->leadSegmentFilterCrate->getOperator(), + 'func' => $this->leadSegmentFilterCrate->getFunc(), ]; } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 111e83ac9fd..a864f33cdcc 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -66,6 +66,9 @@ public function getLeadListFilters(LeadList $leadList) dump($leadSegmentFilter); dump($leadSegmentFilter->getOperator()); dump($leadSegmentFilter->getField()); + dump($leadSegmentFilter->getTable()); + dump($leadSegmentFilter->getParameterHolder('xxx')); + dump($leadSegmentFilter->getParameterValue()); continue; //@todo replaced in query builder From 63c97d328f922be8d92f762ac570fab2ba05cf5a Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 11 Jan 2018 15:50:31 +0100 Subject: [PATCH 027/778] Move parameter type cast to decorator --- .../Segment/Decorator/BaseDecorator.php | 11 +++++- .../Segment/LeadSegmentFilterCrate.php | 38 +------------------ 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 97ac5112d9a..fb648ddd198 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -93,6 +93,15 @@ public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrat public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return $leadSegmentFilterCrate->getFilter(); + $filter = $leadSegmentFilterCrate->getFilter(); + + switch ($leadSegmentFilterCrate->getType()) { + case 'number': + return (float) $filter; + case 'boolean': + return (bool) $filter; + } + + return $filter; } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php index 425aa447dd1..4643b1f40a6 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php @@ -65,9 +65,7 @@ public function __construct(array $filter) $this->display = isset($filter['display']) ? $filter['display'] : null; $this->func = isset($filter['func']) ? $filter['func'] : null; $this->operator = isset($filter['operator']) ? $filter['operator'] : null; - - $filterValue = isset($filter['filter']) ? $filter['filter'] : null; - $this->setFilter($filterValue); + $this->filter = isset($filter['filter']) ? $filter['filter'] : null; } /** @@ -142,16 +140,6 @@ public function getOperator() return $this->operator; } - /** - * @param string|array|bool|float|null $filter - */ - public function setFilter($filter) - { - $filter = $this->sanitizeFilter($filter); - - $this->filter = $filter; - } - /** * @return string */ @@ -159,28 +147,4 @@ public function getFunc() { return $this->func; } - - /** - * @param string|array|bool|float|null $filter - * - * @return string|array|bool|float|null - */ - private function sanitizeFilter($filter) - { - if ($filter === null || is_array($filter) || !$this->getType()) { - return $filter; - } - - switch ($this->getType()) { - case 'number': - $filter = (float) $filter; - break; - - case 'boolean': - $filter = (bool) $filter; - break; - } - - return $filter; - } } From f79b6139bff08c238c677a7a0443f0a801b6b081 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 11 Jan 2018 16:23:58 +0100 Subject: [PATCH 028/778] Add glue getter to filter --- app/bundles/LeadBundle/Segment/LeadSegmentFilter.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 25fd5b8c86a..15177b08986 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -208,6 +208,11 @@ public function getParameterValue() return $this->filterDecorator->getParameterValue($this->leadSegmentFilterCrate); } + public function getGlue() + { + return $this->leadSegmentFilterCrate->getGlue(); + } + /** * @return array */ From e2f8ca39fc4bc21bea8ab3cc522645a196adf49f Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 12 Jan 2018 11:10:22 +0100 Subject: [PATCH 029/778] segments query builder refactoring New Query Builder for segment filter object Extending default doctrine's query builder --- .../Segment/Decorator/BaseDecorator.php | 22 +-- .../BaseFilterQueryBuilder.php | 70 +++++++++- .../ForeignFuncFilterQueryBuilder.php | 13 ++ .../ForeignValueFilterQueryBuilder.php | 35 +++++ .../Segment/LeadSegmentFilterCrate.php | 1 + .../LeadBundle/Segment/Query/QueryBuilder.php | 80 +++++++++++ .../LeadSegmentFilterQueryBuilderTrait.php | 126 ++++++++---------- .../Services/LeadSegmentQueryBuilder.php | 1 - 8 files changed, 264 insertions(+), 84 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index fb648ddd198..88da0e1a26e 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -77,17 +77,6 @@ public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrat return $result; } - switch ($this->getOperator($leadSegmentFilterCrate)) { - case 'like': - case 'notLike': - case 'contains': - return '%:'.$argument.'%'; - case 'startsWith': - return ':'.$argument.'%'; - case 'endsWith': - return '%:'.$argument; - } - return ':'.$argument; } @@ -102,6 +91,17 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate return (bool) $filter; } + switch ($this->getOperator($leadSegmentFilterCrate)) { + case 'like': + case 'notLike': + case 'contains': + return '%'.$filter.'%'; + case 'startsWith': + return $filter.'%'; + case 'endsWith': + return '%'.filter; + } + return $filter; } } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index ab816719b8e..50f2711315b 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -10,14 +10,80 @@ use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; +use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; class BaseFilterQueryBuilder implements FilterQueryBuilderInterface { + use LeadSegmentFilterQueryBuilderTrait; + public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { - // TODO: Implement applyQuery() method. + $filterOperator = $filter->getOperator(); + $filterGlue = $filter->getGlue(); + + $filterParameters = $filter->getParameterValue(); + + if (is_array($filterParameters)) { + $parameters = []; + foreach ($filterParameters as $filterParameter) { + $parameters[] = $this->generateRandomParameterName(); + } + } else { + $parameters = $this->generateRandomParameterName(); + } + + $filterParametersHolder = $filter->getParameterHolder($parameters); + + dump(sprintf('START filter query for %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($parameters, true))); + + $filterGlueFunc = $filterGlue.'Where'; + + $tableAlias = $this->getTableAlias($filter->getTable(), $queryBuilder); + + if (!$tableAlias) { + throw new \Exception('This QB is not intended for foreign queries, add entity "'.$filter->getTable().'"" first.'); + } + + switch ($filterOperator) { + case 'empty': + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) + ); + $queryBuilder->setParameter($emptyParameter, ''); + break; + case 'gt': + case 'eq': + case 'neq': + case 'gte': + case 'like': + case 'notLike': + case 'lt': + case 'lte': + case 'notIn': + case 'in': + case 'startsWith': + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + break; + default: + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + } + + if ($this->isJoinTable($filter->getTable(), $queryBuilder)) { + $queryBuilder->addJoinCondition($tableAlias, $expression); + } else { + $queryBuilder->$filterGlueFunc($expression); + } + //$queryBuilder->$filterGlueFunc() + + $queryBuilder->setParametersPairs($parameters, $filterParameters); - dump('Not aplying any query for me: '.$filter->__toString()); + dump('DONE aplying query for me: '.$filter->__toString()); + dump($queryBuilder->getQueryParts()); + dump($queryBuilder->getParameters()); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php new file mode 100644 index 00000000000..38e890ce43f --- /dev/null +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php @@ -0,0 +1,13 @@ +__toString()); + + $glueFunc = $filter->get Glue().'Where'; + + $parameterName = $this->generateRandomParameterName(); + + $queryBuilder = $this->createExpression($queryBuilder, $parameterName, $this->getFunc()); + + $queryBuilder->setParameter($parameterName, $this->getFilter()); + + dump($queryBuilder->getSQL()); + + + return $queryBuilder; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php index 4643b1f40a6..9752a198651 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php @@ -58,6 +58,7 @@ class LeadSegmentFilterCrate public function __construct(array $filter) { + var_dump($filter); $this->glue = isset($filter['glue']) ? $filter['glue'] : null; $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index c8329014c5d..983344ebe8e 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1366,4 +1366,84 @@ public function __clone() } } } + + /** + * @param $alias + * + * @return bool + */ + public function getJoinCondition($alias) + { + $parts = $this->getQueryParts(); + foreach ($parts['join']['l'] as $joinedTable) { + if ($joinedTable['joinAlias'] == $alias) { + return $joinedTable['joinCondition']; + } + } + + return false; + } + + /** + * @todo I need to rewrite it, it's no longer necessary like this, we have direct access to query parts + * + * @param $alias + * @param $expr + * + * @return $this + */ + public function addJoinCondition($alias, $expr) + { + $parts = $this->getQueryPart('join'); + + foreach ($parts['l'] as $key=>$part) { + if ($part['joinAlias'] == $alias) { + $result['l'][$key]['joinCondition'] = $part['joinCondition'].' and '.$expr; + } + } + + $this->setQueryPart('join', $result); + + return $this; + } + + /** + * @param $alias + * @param $expr + * + * @return $this + */ + public function replaceJoinCondition($alias, $expr) + { + $parts = $this->getQueryPart('join'); + foreach ($parts['l'] as $key=>$part) { + if ($part['joinAlias'] == $alias) { + $parts['l'][$key]['joinCondition'] = $expr; + } + } + + $this->setQueryPart('join', $parts); + + return $this; + } + + /** + * @param $parameters + * @param $filterParameters + * + * @return QueryBuilder + */ + public function setParametersPairs($parameters, $filterParameters) + { + if (!is_array($parameters)) { + return $this->setParameter($parameters, $filterParameters); + } + + foreach ($parameters as $parameter) { + $parameterValue = array_shift($filterParameters); + $return = $this->setParameter($parameter, $parameterValue); + } + + return $return; + } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index 23c787660ef..90f29136478 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -3,46 +3,72 @@ * Created by PhpStorm. * User: jan * Date: 1/9/18 - * Time: 1:54 PM + * Time: 1:54 PM. */ namespace Mautic\LeadBundle\Services; - -use Doctrine\DBAL\Query\QueryBuilder; -use Doctrine\DBAL\Schema\Column; use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\Query\QueryBuilder; -trait LeadSegmentFilterQueryBuilderTrait { +trait LeadSegmentFilterQueryBuilderTrait +{ protected $parameterAliases = []; - public function getTableAlias($tableEntity, QueryBuilder $queryBuilder) { - + /** + * @todo move to query builder + * + * @param $table + * @param QueryBuilder $queryBuilder + * + * @return bool + */ + public function getTableAlias($table, QueryBuilder $queryBuilder) + { $tables = $this->getTableAliases($queryBuilder); - if (!in_array($tableEntity, $tables)) { - //var_dump(sprintf('table entity ' . $tableEntity . ' not found in "%s"', join(', ', array_keys($tables)))); - } - - return isset($tables[$tableEntity]) ? $tables[$tableEntity] : false; + return isset($tables[$table]) ? $tables[$table] : false; } - public function getTableAliases(QueryBuilder $queryBuilder) { + public function getTableAliases(QueryBuilder $queryBuilder) + { $queryParts = $queryBuilder->getQueryParts(); - $tables = array_reduce($queryParts['from'], function ($result, $item) { + $tables = array_reduce($queryParts['from'], function ($result, $item) { $result[$item['table']] = $item['alias']; + return $result; - }, array()); + }, []); foreach ($queryParts['join'] as $join) { - foreach($join as $joinPart) { + foreach ($join as $joinPart) { $tables[$joinPart['joinTable']] = $joinPart['joinAlias']; } } - + return $tables; } + /** + * @param $table + * @param QueryBuilder $queryBuilder + * + * @return bool + */ + public function isJoinTable($table, QueryBuilder $queryBuilder) + { + $queryParts = $queryBuilder->getQueryParts(); + + foreach ($queryParts['join'] as $join) { + foreach ($join as $joinPart) { + if ($joinPart['joinTable'] == $table) { + return true; + } + } + } + + return false; + } + /** * Generate a unique parameter name. * @@ -54,7 +80,7 @@ protected function generateRandomParameterName() $paramName = substr(str_shuffle($alpha_numeric), 0, 8); - if (!in_array($paramName, $this->parameterAliases )) { + if (!in_array($paramName, $this->parameterAliases)) { $this->parameterAliases[] = $paramName; return $paramName; @@ -64,9 +90,10 @@ protected function generateRandomParameterName() } // should be used by filter - protected function createJoin(QueryBuilder $queryBuilder, $target, $alias, $joinOn = '', $from = 'MauticLeadBundle:Lead') { + protected function createJoin(QueryBuilder $queryBuilder, $target, $alias, $joinOn = '', $from = 'MauticLeadBundle:Lead') + { $queryBuilder = $queryBuilder->leftJoin($this->getTableAlias($from, $queryBuilder), $target, $alias, sprintf( - '%s.id = %s.lead_id' . ( $joinOn ? " and $joinOn" : ""), + '%s.id = %s.lead_id'.($joinOn ? " and $joinOn" : ''), $this->getTableAlias($from, $queryBuilder), $alias )); @@ -74,7 +101,8 @@ protected function createJoin(QueryBuilder $queryBuilder, $target, $alias, $join return $queryBuilder; } - protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) { + protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) + { $filter->createJoin($qb, $alias); if (isset($translated) && $translated) { if (isset($translated['func'])) { @@ -83,49 +111,45 @@ protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $fil //@todo rewrite with getFullQualifiedName $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); - - } - else { + } else { //@todo rewrite with getFullQualifiedName $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); } - $qb->setParameter($parameterHolder, $filter->getFilter()); $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); } else { // Default behaviour, translation not necessary - } } /** * @param QueryBuilder $qb * @param $filter - * @param null $alias use alias to extend current query + * @param null $alias use alias to extend current query * * @throws \Exception */ - private function addForeignTableQueryWhere(QueryBuilder $qb, $filter, $alias = null) { + private function addForeignTableQueryWhere(QueryBuilder $qb, $filter, $alias = null) + { dump($filter); if (is_array($filter)) { $alias = is_null($alias) ? $this->generateRandomParameterName() : $alias; foreach ($filter as $singleFilter) { $qb = $this->addForeignTableQueryWhere($qb, $singleFilter, $alias); } + return $qb; } $parameterHolder = $this->generateRandomParameterName(); - $qb = $filter->createExpression($qb, $parameterHolder); + $qb = $filter->createExpression($qb, $parameterHolder); return $qb; dump($expr); die(); - - //$qb = $qb->andWhere($expr); $qb->setParameter($parameterHolder, $filter->getFilter()); @@ -153,42 +177,4 @@ private function addForeignTableQueryWhere(QueryBuilder $qb, $filter, $alias = n // $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); // } } - - protected function getJoinCondition(QueryBuilder $qb, $alias) { - $parts = $qb->getQueryParts(); - foreach ($parts['join']['l'] as $joinedTable) { - if ($joinedTable['joinAlias']==$alias) { - return $joinedTable['joinCondition']; - } - } - throw new \Exception(sprintf('Join alias "%s" doesn\'t exist',$alias)); - } - - protected function addJoinCondition(QueryBuilder $qb, $alias, $expr) { - $result = $parts = $qb->getQueryPart('join'); - - - foreach ($parts['l'] as $key=>$part) { - if ($part['joinAlias'] == $alias) { - $result['l'][$key]['joinCondition'] = $part['joinCondition'] . " and " . $expr; - } - } - - $qb->setQueryPart('join', $result); - - return $qb; - } - - protected function replaceJoinCondition(QueryBuilder $qb, $alias, $expr) { - $parts = $qb->getQueryPart('join'); - foreach ($parts['l'] as $key=>$part) { - if ($part['joinAlias']==$alias) { - $parts['l'][$key]['joinCondition'] = $expr; - } - } - - $qb->setQueryPart('join', $parts); - return $qb; - } - -} \ No newline at end of file +} diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 4859500ad0d..85902dfe81f 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -83,7 +83,6 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { - var_dump('parsing filter: '.$filter->__toString()); $queryBuilder = $filter->applyQuery($queryBuilder); } From aa5b29768049a62d0ea9b79a2a64fff3117c2d44 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 12 Jan 2018 12:13:13 +0100 Subject: [PATCH 030/778] query builder is now doing correctly: * simple queries * simple queries on foreign objects * empty operations * aggregate functions on foreign tables * extending correctly join conditions in order to speedup query * create correctly formatted parameters --- .../Segment/Decorator/BaseDecorator.php | 8 +++ .../BaseFilterQueryBuilder.php | 70 ++++++++++++++++--- .../LeadBundle/Segment/LeadSegmentFilter.php | 27 +++++-- .../LeadBundle/Segment/Query/QueryBuilder.php | 2 +- 4 files changed, 91 insertions(+), 16 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 88da0e1a26e..2737f876ad2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -104,4 +104,12 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate return $filter; } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? + $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; + } } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 50f2711315b..50367b5ac1d 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -20,6 +20,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter { $filterOperator = $filter->getOperator(); $filterGlue = $filter->getGlue(); + $filterAggr = $filter->getAggregateFunction(); $filterParameters = $filter->getParameterValue(); @@ -40,8 +41,49 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = $this->getTableAlias($filter->getTable(), $queryBuilder); + // for aggregate function we need to create new alias and not reuse the old one + if ($filterAggr) { + $tableAlias = false; + } + if (!$tableAlias) { - throw new \Exception('This QB is not intended for foreign queries, add entity "'.$filter->getTable().'"" first.'); + $tableAlias = $this->generateRandomParameterName(); + + switch ($filterOperator) { + case 'notLike': + case 'notIn': + + case 'empty': + case 'startsWith': + case 'gt': + case 'eq': + case 'neq': + case 'gte': + case 'like': + case 'lt': + case 'lte': + case 'in': + if ($filterAggr) { + $queryBuilder = $queryBuilder->leftJoin( + $this->getTableAlias('leads', $queryBuilder), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $this->getTableAlias('leads', $queryBuilder), $tableAlias) + ); + } else { + $queryBuilder = $queryBuilder->innerJoin( + $this->getTableAlias('leads', $queryBuilder), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $this->getTableAlias('leads', $queryBuilder), $tableAlias) + ); + } + break; + default: + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + } + + var_dump('This QB is not intended for foreign queries, add entity "'.$filter->getTable().'"" first.'); } switch ($filterOperator) { @@ -52,6 +94,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter ); $queryBuilder->setParameter($emptyParameter, ''); break; + case 'startsWith': + $filterOperator = 'like'; case 'gt': case 'eq': case 'neq': @@ -62,22 +106,32 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'lte': case 'notIn': case 'in': - case 'startsWith': - $expression = $queryBuilder->expr()->$filterOperator( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ); + if ($filterAggr) { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), + $filterParametersHolder + ); + } else { + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + } break; default: + var_dump($filter->toArray()); throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } if ($this->isJoinTable($filter->getTable(), $queryBuilder)) { - $queryBuilder->addJoinCondition($tableAlias, $expression); + if ($filterAggr) { + $queryBuilder->andHaving($expression); + } else { + $queryBuilder->addJoinCondition($tableAlias, $expression); + } } else { $queryBuilder->$filterGlueFunc($expression); } - //$queryBuilder->$filterGlueFunc() $queryBuilder->setParametersPairs($parameters, $filterParameters); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 15177b08986..30059b75041 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -13,6 +13,7 @@ use Doctrine\DBAL\Schema\Column; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; @@ -29,7 +30,7 @@ class LeadSegmentFilter private $leadSegmentFilterCrate; /** - * @var FilterDecoratorInterface + * @var FilterDecoratorInterface|BaseDecorator */ private $filterDecorator; @@ -112,16 +113,19 @@ public function XXcreateExpression(QueryBuilder $queryBuilder, $parameterName, $ if (!$alias) { if ($desc['func']) { $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($desc['func'].'('.$alias.'.'.$this->getDBColumn()->getName().')', $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($desc['func'].'('.$alias.'.'.$this->getDBColumn() + ->getName().')', $this->getFilterConditionValue($parameterName)); $queryBuilder = $queryBuilder->andHaving($expr); } else { if ($alias != 'l') { $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() + ->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); } else { dump('lead restriction'); - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() + ->getName(), $this->getFilterConditionValue($parameterName)); var_dump($expr); die(); $queryBuilder = $queryBuilder->andWhere($expr); @@ -129,10 +133,12 @@ public function XXcreateExpression(QueryBuilder $queryBuilder, $parameterName, $ } } else { if ($alias != 'l') { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() + ->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); } else { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() + ->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $queryBuilder->andWhere($expr); } } @@ -163,7 +169,8 @@ public function XXgetDBColumn() { if (is_null($this->dbColumn)) { if ($descr = $this->getQueryDescription()) { - $this->dbColumn = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; + $this->dbColumn = $this->em->getConnection()->getSchemaManager() + ->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; } else { $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); if (!$dbTableColumns) { @@ -213,6 +220,11 @@ public function getGlue() return $this->leadSegmentFilterCrate->getGlue(); } + public function getAggregateFunction() + { + return $this->filterDecorator->getAggregateFunc($this->leadSegmentFilterCrate); + } + /** * @return array */ @@ -227,6 +239,7 @@ public function toArray() 'display' => $this->leadSegmentFilterCrate->getDisplay(), 'operator' => $this->leadSegmentFilterCrate->getOperator(), 'func' => $this->leadSegmentFilterCrate->getFunc(), + 'aggr' => $this->getAggregateFunction(), ]; } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 983344ebe8e..abbaae1cae2 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1394,7 +1394,7 @@ public function getJoinCondition($alias) */ public function addJoinCondition($alias, $expr) { - $parts = $this->getQueryPart('join'); + $result = $parts = $this->getQueryPart('join'); foreach ($parts['l'] as $key=>$part) { if ($part['joinAlias'] == $alias) { From 0dfcb2ac0c905427d6a98075b46d934ad1398638 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 12 Jan 2018 16:14:53 +0100 Subject: [PATCH 031/778] Get operator for filter - handel contains, startsWith and endsWith --- .../LeadBundle/Segment/Decorator/BaseDecorator.php | 12 +++++++++++- .../FilterQueryBuilder/BaseFilterQueryBuilder.php | 2 -- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 2737f876ad2..ed4f8a44a42 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -63,7 +63,17 @@ public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilterCrate->getOperator()); + $operator = $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilterCrate->getOperator()); + + switch ($operator) { + case 'startsWith': + case 'endsWith': + case 'contains': + return 'like'; + break; + } + + return $operator; } public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 50367b5ac1d..2b7ac6b9879 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -94,8 +94,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter ); $queryBuilder->setParameter($emptyParameter, ''); break; - case 'startsWith': - $filterOperator = 'like'; case 'gt': case 'eq': case 'neq': From 479e3a793cc526bc60bcf3583540a0ecdcebab38 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 12 Jan 2018 16:28:35 +0100 Subject: [PATCH 032/778] add qb services * dnc qb templates * basic * foreign + foreign func cleanup obsolete methods from filter extend filter with queryType method make qb services aware of its service id - will be refactored to automatic, using container move functions from trait to our QB cleanup trait --- app/bundles/LeadBundle/Config/config.php | 18 +++ .../Segment/Decorator/BaseDecorator.php | 14 +- .../BaseFilterQueryBuilder.php | 38 +++-- .../DncFilterQueryBuilder.php | 147 ++++++++++++++++++ .../FilterQueryBuilderInterface.php | 2 + .../ForeignFuncFilterQueryBuilder.php | 2 +- .../ForeignValueFilterQueryBuilder.php | 24 +-- .../LeadBundle/Segment/LeadSegmentFilter.php | 119 +------------- .../Segment/LeadSegmentFilterFactory.php | 3 + .../LeadBundle/Segment/LeadSegmentService.php | 12 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 95 +++++++++-- .../Services/LeadSegmentFilterDescriptor.php | 62 +++++++- .../LeadSegmentFilterQueryBuilderTrait.php | 54 ------- 13 files changed, 355 insertions(+), 235 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 12f61a9bce4..413097643b5 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -716,6 +716,24 @@ 'event_dispatcher', ], ], + // Segment Filter Query builders + + 'mautic.lead.query.builder.basic' => [ + 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder::class, + 'arguments' => [], + ], + 'mautic.lead.query.builder.foreign.value' => [ + 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder::class, + 'arguments' => [], + ], + 'mautic.lead.query.builder.foreign.func' => [ + 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder::class, + 'arguments' => [], + ], + 'mautic.lead.query.builder.dnc' => [ + 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder::class, + 'arguments' => [], + ], ], 'helpers' => [ 'mautic.helper.template.avatar' => [ diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 2737f876ad2..5f9dc20238c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\LeadSegmentFilterOperator; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; @@ -66,6 +67,17 @@ public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) return $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilterCrate->getOperator()); } + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (!isset($this->leadSegmentFilterDescriptor[$originalField]['type'])) { + return BaseFilterQueryBuilder::getServiceId(); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['type']; + } + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) { if (is_array($argument)) { @@ -99,7 +111,7 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate case 'startsWith': return $filter.'%'; case 'endsWith': - return '%'.filter; + return '%'.$filter; } return $filter; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 50367b5ac1d..ec22330f83a 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -16,6 +16,11 @@ class BaseFilterQueryBuilder implements FilterQueryBuilderInterface { use LeadSegmentFilterQueryBuilderTrait; + public static function getServiceId() + { + return 'mautic.lead.query.builder.basic'; + } + public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { $filterOperator = $filter->getOperator(); @@ -35,11 +40,11 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterParametersHolder = $filter->getParameterHolder($parameters); - dump(sprintf('START filter query for %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($parameters, true))); + dump(sprintf('START filter query for %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); $filterGlueFunc = $filterGlue.'Where'; - $tableAlias = $this->getTableAlias($filter->getTable(), $queryBuilder); + $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); // for aggregate function we need to create new alias and not reuse the old one if ($filterAggr) { @@ -63,27 +68,27 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'lt': case 'lte': case 'in': + //@todo this logic needs to if ($filterAggr) { $queryBuilder = $queryBuilder->leftJoin( - $this->getTableAlias('leads', $queryBuilder), + $queryBuilder->getTableAlias('leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $this->getTableAlias('leads', $queryBuilder), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); } else { $queryBuilder = $queryBuilder->innerJoin( - $this->getTableAlias('leads', $queryBuilder), + $queryBuilder->getTableAlias('leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $this->getTableAlias('leads', $queryBuilder), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); } break; default: - throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + dump('Dunno how to handle operator "'.$filterOperator.'"'); } - - var_dump('This QB is not intended for foreign queries, add entity "'.$filter->getTable().'"" first.'); } switch ($filterOperator) { @@ -95,6 +100,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->setParameter($emptyParameter, ''); break; case 'startsWith': + case 'endsWith': $filterOperator = 'like'; case 'gt': case 'eq': @@ -119,15 +125,17 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } break; default: - var_dump($filter->toArray()); - throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + dump(' * IGNORED * - Dunno how to handle operator "'.$filterOperator.'"'); + //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + $expression = '1=1'; } - if ($this->isJoinTable($filter->getTable(), $queryBuilder)) { + if ($queryBuilder->isJoinTable($filter->getTable())) { if ($filterAggr) { $queryBuilder->andHaving($expression); } else { - $queryBuilder->addJoinCondition($tableAlias, $expression); + dump($filter->getGlue()); + $queryBuilder->addJoinCondition($tableAlias, 'and ('.$expression.')'); } } else { $queryBuilder->$filterGlueFunc($expression); @@ -135,10 +143,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->setParametersPairs($parameters, $filterParameters); - dump('DONE aplying query for me: '.$filter->__toString()); - dump($queryBuilder->getQueryParts()); - dump($queryBuilder->getParameters()); - return $queryBuilder; } } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php new file mode 100644 index 00000000000..50c85c106e6 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php @@ -0,0 +1,147 @@ +getOperator(); + $filterGlue = $filter->getGlue(); + $filterAggr = $filter->getAggregateFunction(); + + $filterParameters = $filter->getParameterValue(); + + if (is_array($filterParameters)) { + $parameters = []; + foreach ($filterParameters as $filterParameter) { + $parameters[] = $this->generateRandomParameterName(); + } + } else { + $parameters = $this->generateRandomParameterName(); + } + + $filterParametersHolder = $filter->getParameterHolder($parameters); + + dump(sprintf('START filter query for %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + + $filterGlueFunc = $filterGlue.'Where'; + + $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + + // for aggregate function we need to create new alias and not reuse the old one + if ($filterAggr) { + $tableAlias = false; + } + + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + + switch ($filterOperator) { + case 'notLike': + case 'notIn': + + case 'empty': + case 'startsWith': + case 'gt': + case 'eq': + case 'neq': + case 'gte': + case 'like': + case 'lt': + case 'lte': + case 'in': + //@todo this logic needs to + if ($filterAggr) { + $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + ); + } else { + $queryBuilder = $queryBuilder->innerJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + ); + } + break; + default: + //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + dump('Dunno how to handle operator "'.$filterOperator.'"'); + } + } + + switch ($filterOperator) { + case 'empty': + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) + ); + $queryBuilder->setParameter($emptyParameter, ''); + break; + case 'startsWith': + case 'endsWith': + $filterOperator = 'like'; + case 'gt': + case 'eq': + case 'neq': + case 'gte': + case 'like': + case 'notLike': + case 'lt': + case 'lte': + case 'notIn': + case 'in': + if ($filterAggr) { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), + $filterParametersHolder + ); + } else { + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + } + break; + default: + dump(' * IGNORED * - Dunno how to handle operator "'.$filterOperator.'"'); + //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + $expression = '1=1'; + } + + if ($queryBuilder->isJoinTable($filter->getTable())) { + if ($filterAggr) { + $queryBuilder->andHaving($expression); + } else { + dump($filter->getGlue()); + $queryBuilder->addJoinCondition($tableAlias, 'and ('.$expression.')'); + } + } else { + $queryBuilder->$filterGlueFunc($expression); + } + + $queryBuilder->setParametersPairs($parameters, $filterParameters); + + return $queryBuilder; + } +} diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php index aeac02d35c3..810c83afef9 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php @@ -22,4 +22,6 @@ interface FilterQueryBuilderInterface * @return QueryBuilder */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter); + + public static function getServiceId(); } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php index 38e890ce43f..c4382090c52 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php @@ -8,6 +8,6 @@ namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; -class ForeignFilterQueryBuilder extends BaseFilterQueryBuilder +class ForeignFuncFilterQueryBuilder extends BaseFilterQueryBuilder { } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php index 632aa31e483..d110103c548 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php @@ -8,28 +8,6 @@ namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; -use Mautic\LeadBundle\Segment\Query\QueryBuilder; - -class ForeignFilterQueryBuilder implements FilterQueryBuilderInterface +class ForeignValueFilterQueryBuilder extends BaseFilterQueryBuilder { - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) - { - // TODO: Implement applyQuery() method. - - dump('Aplying any query for me: '.$filter->__toString()); - - $glueFunc = $filter->get Glue().'Where'; - - $parameterName = $this->generateRandomParameterName(); - - $queryBuilder = $this->createExpression($queryBuilder, $parameterName, $this->getFunc()); - - $queryBuilder->setParameter($parameterName, $this->getFilter()); - - dump($queryBuilder->getSQL()); - - - return $queryBuilder; - } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 30059b75041..50e79c66867 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -52,100 +52,6 @@ public function __construct( $this->em = $em; } - /** - * @param null $argument - * - * @return string - * - * @throws \Exception - */ - public function XXgetFilterConditionValue($argument = null) - { - switch ($this->getDBColumn()->getType()->getName()) { - case 'number': - case 'integer': - case 'float': - return ':'.$argument; - case 'datetime': - case 'date': - return sprintf('":%s"', $argument); - case 'text': - case 'string': - switch ($this->getFunc()) { - case 'eq': - case 'ne': - case 'neq': - return sprintf("':%s'", $argument); - default: - throw new \Exception('Unknown operator '.$this->getFunc()); - } - default: - var_dump($this->getDBColumn()->getType()->getName()); - var_dump($this); - die(); - } - var_dump($filter); - throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); - } - - public function XXcreateQuery(QueryBuilder $queryBuilder, $alias = false) - { - dump('creating query:'.$this->getObject()); - $glueFunc = $this->getGlue().'Where'; - - $parameterName = $this->generateRandomParameterName(); - - $queryBuilder = $this->createExpression($queryBuilder, $parameterName, $this->getFunc()); - - $queryBuilder->setParameter($parameterName, $this->getFilter()); - - dump($queryBuilder->getSQL()); - - return $queryBuilder; - } - - public function XXcreateExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) - { - dump('creating query:'.$this->getField()); - $func = is_null($func) ? $this->getFunc() : $func; - $alias = $this->getTableAlias($this->getEntityName(), $queryBuilder); - $desc = $this->getQueryDescription(); - if (!$alias) { - if ($desc['func']) { - $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($desc['func'].'('.$alias.'.'.$this->getDBColumn() - ->getName().')', $this->getFilterConditionValue($parameterName)); - $queryBuilder = $queryBuilder->andHaving($expr); - } else { - if ($alias != 'l') { - $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() - ->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); - } else { - dump('lead restriction'); - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() - ->getName(), $this->getFilterConditionValue($parameterName)); - var_dump($expr); - die(); - $queryBuilder = $queryBuilder->andWhere($expr); - } - } - } else { - if ($alias != 'l') { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() - ->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); - } else { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn() - ->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $queryBuilder->andWhere($expr); - } - } - - return $queryBuilder; - } - public function getEntity() { $converter = new CamelCaseToSnakeCaseNameConverter(); @@ -161,30 +67,11 @@ public function getEntity() } /** - * @return Column - * - * @throws \Exception + * @return string */ - public function XXgetDBColumn() + public function getQueryType() { - if (is_null($this->dbColumn)) { - if ($descr = $this->getQueryDescription()) { - $this->dbColumn = $this->em->getConnection()->getSchemaManager() - ->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; - } else { - $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); - if (!$dbTableColumns) { - var_dump($this); - throw new \Exception('Unknown database table and no translation provided for type "'.$this->getType().'"'); - } - if (!isset($dbTableColumns[$this->getField()])) { - throw new \Exception('Unknown database column and no translation provided for type "'.$this->getType().'"'); - } - $this->dbColumn = $dbTableColumns[$this->getField()]; - } - } - - return $this->dbColumn; + return $this->filterDecorator->getQueryType($this->leadSegmentFilterCrate); } /** diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index c7ebf0027a7..e8b7e1af0cd 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -78,6 +78,9 @@ public function getLeadListFilters(LeadList $leadList) */ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) { + dump($filter->getQueryType()); + die(); + return new BaseFilterQueryBuilder(); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 83d4a87d2b0..6550c0a9350 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -49,16 +49,10 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); - //$qb = $this->queryBuilder->addLeadListRestrictions($qb, $batchLimiters, $entity->getId(), $this->leadSegmentFilterFactory->dictionary); - dump($sql = $qb->getSQL()); - $parameters = $qb->getParameters(); - foreach ($parameters as $parameter=>$value) { - $sql = str_replace(':'.$parameter, $value, $sql); - } - var_dump($sql); -// die(); - return null; + dump($qb->getQueryParts()); + dump($qb->getParameters()); + dump($qb->execute()); return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index abbaae1cae2..ee360eb2c67 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -32,7 +32,7 @@ * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements * even if some vendors such as MySQL support it. * - * @see www.doctrine-project.org + * @see www.doctrine-project.org * @since 2.1 * * @author Guilherme Blanco @@ -1200,8 +1200,8 @@ private function isLimitQuery() private function getSQLForInsert() { return 'INSERT INTO '.$this->sqlParts['from']['table']. - ' ('.implode(', ', array_keys($this->sqlParts['values'])).')'. - ' VALUES('.implode(', ', $this->sqlParts['values']).')'; + ' ('.implode(', ', array_keys($this->sqlParts['values'])).')'. + ' VALUES('.implode(', ', $this->sqlParts['values']).')'; } /** @@ -1265,7 +1265,7 @@ public function __toString() * * @license New BSD License * - * @see http://www.zetacomponents.org + * @see http://www.zetacomponents.org * * @param mixed $value * @param mixed $type @@ -1396,9 +1396,9 @@ public function addJoinCondition($alias, $expr) { $result = $parts = $this->getQueryPart('join'); - foreach ($parts['l'] as $key=>$part) { + foreach ($parts['l'] as $key => $part) { if ($part['joinAlias'] == $alias) { - $result['l'][$key]['joinCondition'] = $part['joinCondition'].' and '.$expr; + $result['l'][$key]['joinCondition'] = $part['joinCondition'].' '.$expr.''; } } @@ -1416,7 +1416,7 @@ public function addJoinCondition($alias, $expr) public function replaceJoinCondition($alias, $expr) { $parts = $this->getQueryPart('join'); - foreach ($parts['l'] as $key=>$part) { + foreach ($parts['l'] as $key => $part) { if ($part['joinAlias'] == $alias) { $parts['l'][$key]['joinCondition'] = $expr; } @@ -1441,9 +1441,86 @@ public function setParametersPairs($parameters, $filterParameters) foreach ($parameters as $parameter) { $parameterValue = array_shift($filterParameters); - $return = $this->setParameter($parameter, $parameterValue); + $this->setParameter($parameter, $parameterValue); } - return $return; + return $this; + } + + public function getTableAlias($table, $joinType = null) + { + if (is_null($joinType)) { + $tables = $this->getTableAliases(); + + return isset($tables[$table]) ? $tables[$table] : false; + } + + $tableJoins = $this->getTableJoins($table); + + if (!$tableJoins) { + return false; + } + + $result = []; + + foreach ($tableJoins as $tableJoin) { + if ($tableJoin['joinType'] == $joinType) { + $result[] = $tableJoin['joinAlias']; + } + } + + return !count($result) ? false : count($result) == 1 ? array_shift($result) : $result; + } + + public function getTableJoins($tableName) + { + foreach ($this->getQueryParts()['join'] as $join) { + foreach ($join as $joinPart) { + if ($tableName == $joinPart['joinAlias']) { + return $joinPart; + } + } + } + + return false; + } + + public function getTableAliases() + { + $queryParts = $this->getQueryParts(); + $tables = array_reduce($queryParts['from'], function ($result, $item) { + $result[$item['table']] = $item['alias']; + + return $result; + }, []); + + foreach ($queryParts['join'] as $join) { + foreach ($join as $joinPart) { + $tables[$joinPart['joinTable']] = $joinPart['joinAlias']; + } + } + + return $tables; + } + + /** + * @param $table + * @param QueryBuilder $queryBuilder + * + * @return bool + */ + public function isJoinTable($table) + { + $queryParts = $this->getQueryParts(); + + foreach ($queryParts['join'] as $join) { + foreach ($join as $joinPart) { + if ($joinPart['joinTable'] == $table) { + return true; + } + } + } + + return false; } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index bcce61e1c71..c2e8d02167f 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -11,6 +11,10 @@ namespace Mautic\LeadBundle\Services; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder; + class LeadSegmentFilterDescriptor extends \ArrayIterator { private $translations; @@ -18,25 +22,73 @@ class LeadSegmentFilterDescriptor extends \ArrayIterator public function __construct() { $this->translations['lead_email_read_count'] = [ - 'type' => 'foreign_aggr', + 'type' => ForeignFuncFilterQueryBuilder::getServiceId(), 'foreign_table' => 'email_stats', 'foreign_table_field' => 'lead_id', 'table' => 'leads', 'table_field' => 'id', 'func' => 'sum', - 'field' => 'open_count' + 'field' => 'open_count', ]; $this->translations['lead_email_read_date'] = [ - 'type' => 'foreign', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), 'foreign_table' => 'page_hits', 'foreign_table_field' => 'lead_id', 'table' => 'leads', 'table_field' => 'id', - 'field' => 'date_hit' + 'field' => 'date_hit', + ]; + + $this->translations['dnc_bounced'] = [ + 'type' => DncFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_table_field' => 'lead_id', + 'table' => 'leads', + 'table_field' => 'id', + 'field' => 'date_hit', ]; parent::__construct($this->translations); } - } + +//case 'dnc_bounced': +// case 'dnc_unsubscribed': +// case 'dnc_bounced_sms': +// case 'dnc_unsubscribed_sms': +// // Special handling of do not contact +// $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; +// +// $parts = explode('_', $leadSegmentFilter->getField()); +// $channel = 'email'; +// +// if (count($parts) === 3) { +// $channel = $parts[2]; +// } +// +// $channelParameter = $this->generateRandomParameterName(); +// $subqb = $this->entityManager->getConnection()->createQueryBuilder() +// ->select('null') +// ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) +// ->where( +// $q->expr()->andX( +// $q->expr()->eq($alias.'.reason', $exprParameter), +// $q->expr()->eq($alias.'.lead_id', 'l.id'), +// $q->expr()->eq($alias.'.channel', ":$channelParameter") +// ) +// ); +// +// $groupExpr->add( +// sprintf('%s (%s)', $func, $subqb->getSQL()) +// ); +// +// // Filter will always be true and differentiated via EXISTS/NOT EXISTS +// $leadSegmentFilter->setFilter(true); +// +// $ignoreAutoFilter = true; +// +// $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; +// $parameters[$channelParameter] = $channel; +// +// break; diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index 90f29136478..fc213353e9a 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -15,60 +15,6 @@ trait LeadSegmentFilterQueryBuilderTrait { protected $parameterAliases = []; - /** - * @todo move to query builder - * - * @param $table - * @param QueryBuilder $queryBuilder - * - * @return bool - */ - public function getTableAlias($table, QueryBuilder $queryBuilder) - { - $tables = $this->getTableAliases($queryBuilder); - - return isset($tables[$table]) ? $tables[$table] : false; - } - - public function getTableAliases(QueryBuilder $queryBuilder) - { - $queryParts = $queryBuilder->getQueryParts(); - $tables = array_reduce($queryParts['from'], function ($result, $item) { - $result[$item['table']] = $item['alias']; - - return $result; - }, []); - - foreach ($queryParts['join'] as $join) { - foreach ($join as $joinPart) { - $tables[$joinPart['joinTable']] = $joinPart['joinAlias']; - } - } - - return $tables; - } - - /** - * @param $table - * @param QueryBuilder $queryBuilder - * - * @return bool - */ - public function isJoinTable($table, QueryBuilder $queryBuilder) - { - $queryParts = $queryBuilder->getQueryParts(); - - foreach ($queryParts['join'] as $join) { - foreach ($join as $joinPart) { - if ($joinPart['joinTable'] == $table) { - return true; - } - } - } - - return false; - } - /** * Generate a unique parameter name. * From 991d2e424a43db9cafb7818b1d53f7d427d25fd6 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 12 Jan 2018 16:42:45 +0100 Subject: [PATCH 033/778] inject correct query builder to filter based on service id provided by qb itself --- app/bundles/LeadBundle/Config/config.php | 1 + .../Segment/LeadSegmentFilterFactory.php | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 413097643b5..f1a03b05ec4 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -801,6 +801,7 @@ 'mautic.lead.model.lead_segment_filter_date', 'doctrine.orm.entity_manager', 'mautic.lead.model.lead_segment_decorator_base', + '@service_container', ], ], 'mautic.lead.model.relative_date' => [ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index e8b7e1af0cd..7d443566a32 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -16,6 +16,7 @@ use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; +use Symfony\Component\DependencyInjection\Container; class LeadSegmentFilterFactory { @@ -34,14 +35,21 @@ class LeadSegmentFilterFactory */ private $baseDecorator; + /** + * @var Container + */ + private $container; + public function __construct( LeadSegmentFilterDate $leadSegmentFilterDate, EntityManager $entityManager, - BaseDecorator $baseDecorator + BaseDecorator $baseDecorator, + Container $container ) { $this->leadSegmentFilterDate = $leadSegmentFilterDate; $this->entityManager = $entityManager; $this->baseDecorator = $baseDecorator; + $this->container = $container; } /** @@ -78,10 +86,9 @@ public function getLeadListFilters(LeadList $leadList) */ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) { - dump($filter->getQueryType()); - die(); + $qbServiceId = $filter->getQueryType(); - return new BaseFilterQueryBuilder(); + return $this->container->get($qbServiceId); } /** From 1b2fb02054f03ac0b32c441abcf335e1c093c61d Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 15 Jan 2018 09:52:15 +0100 Subject: [PATCH 034/778] add old version of filter to make the old sql generation work, make the old version work, alter DI qb relations --- app/bundles/LeadBundle/Config/config.php | 2 +- .../Entity/LeadListSegmentRepository.php | 18 +- .../DncFilterQueryBuilder.php | 180 +++---- .../LeadBundle/Segment/LeadSegmentFilter.php | 2 +- .../Segment/LeadSegmentFilterCrate.php | 1 - .../Segment/LeadSegmentFilterOld.php | 438 ++++++++++++++++++ 6 files changed, 521 insertions(+), 120 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index f1a03b05ec4..bd65e9d0b0c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -730,7 +730,7 @@ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder::class, 'arguments' => [], ], - 'mautic.lead.query.builder.dnc' => [ + 'mautic.lead.query.builder.special.dnc' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder::class, 'arguments' => [], ], diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 10b9c8a399c..bdfcf909d9f 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -16,6 +16,7 @@ use Mautic\CoreBundle\Helper\InputHelper; use Mautic\LeadBundle\Event\LeadListFilteringEvent; use Mautic\LeadBundle\LeadEvents; +use Mautic\LeadBundle\Segment\LeadSegmentFilterOld; use Mautic\LeadBundle\Segment\LeadSegmentFilters; use Mautic\LeadBundle\Segment\RandomParameterName; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -86,7 +87,6 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte echo 'SQL parameters:'; dump($q->getParameters()); - // Leads that do not have any record in the lead_lists_leads table for this lead list // For non null fields - it's apparently better to use left join over not exists due to not using nullable // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ @@ -179,15 +179,15 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $groupExpr = $q->expr()->andX(); foreach ($leadSegmentFilters as $k => $leadSegmentFilter) { - $object = $leadSegmentFilter->getObject(); + $leadSegmentFilter = new LeadSegmentFilterOld((array) $leadSegmentFilter->leadSegmentFilterCrate); + //$object = $leadSegmentFilter->getObject(); $column = false; $field = false; $filterField = $leadSegmentFilter->getField(); - if($filterField=='lead_email_read_date') { - + if ($filterField == 'lead_email_read_date') { } if ($leadSegmentFilter->isLeadType()) { @@ -213,13 +213,14 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $func = $leadSegmentFilter->getFunc(); + dump($func); + dump($leadSegmentFilter->getField()); // Generate a unique alias $alias = $this->generateRandomParameterName(); // var_dump($func.":".$leadSegmentFilter->getField()); // var_dump($exprParameter); - switch ($leadSegmentFilter->getField()) { case 'hit_url': case 'referer': @@ -360,8 +361,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $table = 'email_stats'; } - - if($filterField=='lead_email_read_date') { + if ($filterField == 'lead_email_read_date') { var_dump($func); } @@ -370,8 +370,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query ->select('id') ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - switch ($func) { case 'eq': case 'neq': @@ -420,7 +418,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query default: $parameter2 = $this->generateRandomParameterName(); - if($filterField=='lead_email_read_date') { + if ($filterField == 'lead_email_read_date') { var_dump($exprParameter); } $parameters[$parameter2] = $leadSegmentFilter->getFilter(); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php index 50c85c106e6..ee83dea3316 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php @@ -18,128 +18,94 @@ class DncFilterQueryBuilder implements FilterQueryBuilderInterface public static function getServiceId() { + return 'mautic.lead.query.builder.special.dnc'; } - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function aaa() { - $filterOperator = $filter->getOperator(); - $filterGlue = $filter->getGlue(); - $filterAggr = $filter->getAggregateFunction(); - - $filterParameters = $filter->getParameterValue(); - - if (is_array($filterParameters)) { - $parameters = []; - foreach ($filterParameters as $filterParameter) { - $parameters[] = $this->generateRandomParameterName(); - } - } else { - $parameters = $this->generateRandomParameterName(); - } + switch (false) { + case + 'dnc_bounced': + case 'dnc_unsubscribed': + case 'dnc_bounced_sms': + case 'dnc_unsubscribed_sms': + // Special handling of do not contact - $filterParametersHolder = $filter->getParameterHolder($parameters); + $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; - dump(sprintf('START filter query for %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + $parts = explode('_', $leadSegmentFilter->getField()); + $channel = 'email'; - $filterGlueFunc = $filterGlue.'Where'; + if (count($parts) === 3) { + $channel = $parts[2]; + } - $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + $channelParameter = $this->generateRandomParameterName(); + $subqb = $this->entityManager->getConnection()->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) + ->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.reason', $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id'), + $q->expr() + ->eq($alias.'.channel', ":$channelParameter") + ) + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subqb->getSQL()) + ); - // for aggregate function we need to create new alias and not reuse the old one - if ($filterAggr) { - $tableAlias = false; - } + // Filter will always be true and differentiated via EXISTS/NOT EXISTS + $leadSegmentFilter->setFilter(true); - if (!$tableAlias) { - $tableAlias = $this->generateRandomParameterName(); - - switch ($filterOperator) { - case 'notLike': - case 'notIn': - - case 'empty': - case 'startsWith': - case 'gt': - case 'eq': - case 'neq': - case 'gte': - case 'like': - case 'lt': - case 'lte': - case 'in': - //@todo this logic needs to - if ($filterAggr) { - $queryBuilder = $queryBuilder->leftJoin( - $queryBuilder->getTableAlias('leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) - ); - } else { - $queryBuilder = $queryBuilder->innerJoin( - $queryBuilder->getTableAlias('leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) - ); - } - break; - default: - //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); - dump('Dunno how to handle operator "'.$filterOperator.'"'); - } - } + $ignoreAutoFilter = true; + + $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; + $parameters[$channelParameter] = $channel; - switch ($filterOperator) { - case 'empty': - $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) - ); - $queryBuilder->setParameter($emptyParameter, ''); - break; - case 'startsWith': - case 'endsWith': - $filterOperator = 'like'; - case 'gt': - case 'eq': - case 'neq': - case 'gte': - case 'like': - case 'notLike': - case 'lt': - case 'lte': - case 'notIn': - case 'in': - if ($filterAggr) { - $expression = $queryBuilder->expr()->$filterOperator( - sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), - $filterParametersHolder - ); - } else { - $expression = $queryBuilder->expr()->$filterOperator( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ); - } break; - default: - dump(' * IGNORED * - Dunno how to handle operator "'.$filterOperator.'"'); - //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); - $expression = '1=1'; } + } - if ($queryBuilder->isJoinTable($filter->getTable())) { - if ($filterAggr) { - $queryBuilder->andHaving($expression); - } else { - dump($filter->getGlue()); - $queryBuilder->addJoinCondition($tableAlias, 'and ('.$expression.')'); - } - } else { - $queryBuilder->$filterGlueFunc($expression); + public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + { + dump('dnc apply query:'); + var_dump($filter); + die(); + $parts = explode('_', $filter->getField()); + $channel = 'email'; + + if (count($parts) === 3) { + $channel = $parts[2]; } + $channelParameter = $this->generateRandomParameterName(); + $subqb = $this->entityManager->getConnection()->createQueryBuilder() + ->select('null') + ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) + ->where( + $q->expr()->andX( + $q->expr()->eq($alias.'.reason', $exprParameter), + $q->expr()->eq($alias.'.lead_id', 'l.id'), + $q->expr() + ->eq($alias.'.channel', ":$channelParameter") + ) + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subqb->getSQL()) + ); + + // Filter will always be true and differentiated via EXISTS/NOT EXISTS + $leadSegmentFilter->setFilter(true); + + $ignoreAutoFilter = true; + + $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; + $parameters[$channelParameter] = $channel; + $queryBuilder->setParametersPairs($parameters, $filterParameters); return $queryBuilder; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 50e79c66867..c92b5835170 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -27,7 +27,7 @@ class LeadSegmentFilter /** * @var LeadSegmentFilterCrate */ - private $leadSegmentFilterCrate; + public $leadSegmentFilterCrate; /** * @var FilterDecoratorInterface|BaseDecorator diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php index 9752a198651..4643b1f40a6 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php @@ -58,7 +58,6 @@ class LeadSegmentFilterCrate public function __construct(array $filter) { - var_dump($filter); $this->glue = isset($filter['glue']) ? $filter['glue'] : null; $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php new file mode 100644 index 00000000000..8d41efd937d --- /dev/null +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php @@ -0,0 +1,438 @@ +glue = isset($filter['glue']) ? $filter['glue'] : null; + $this->field = isset($filter['field']) ? $filter['field'] : null; + $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; + $this->type = isset($filter['type']) ? $filter['type'] : null; + $this->display = isset($filter['display']) ? $filter['display'] : null; + $this->func = isset($filter['func']) ? $filter['func'] : null; + $operatorValue = isset($filter['operator']) ? $filter['operator'] : null; + $this->setOperator($operatorValue); + + $filterValue = isset($filter['filter']) ? $filter['filter'] : null; + $this->setFilter($filterValue); + $this->em = $em; + if (!is_null($dictionary)) { + $this->translateQueryDescription($dictionary); + } + } + + /** + * @return string + * + * @throws \Exception + */ + public function getSQLOperator() + { + switch ($this->getOperator()) { + case 'gt': + return '>'; + case 'eq': + return '='; + case 'gt': + return '>'; + case 'gte': + return '>='; + case 'lt': + return '<'; + case 'lte': + return '<='; + } + throw new \Exception(sprintf('Unknown operator \'%s\'.', $this->getOperator())); + } + + public function getFilterConditionValue($argument = null) + { + switch ($this->getDBColumn()->getType()->getName()) { + case 'number': + case 'integer': + case 'float': + return ':'.$argument; + case 'datetime': + case 'date': + return sprintf('":%s"', $argument); + case 'text': + case 'string': + switch ($this->getFunc()) { + case 'eq': + case 'ne': + case 'neq': + return sprintf("':%s'", $argument); + default: + throw new \Exception('Unknown operator '.$this->getFunc()); + } + default: + var_dump($this->getDBColumn()->getType()->getName()); + var_dump($this); + die(); + } + var_dump($filter); + throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); + } + + public function createQuery(QueryBuilder $queryBuilder, $alias = false) + { + dump('creating query:'.$this->getObject()); + $glueFunc = $this->getGlue().'Where'; + + $parameterName = $this->generateRandomParameterName(); + + $queryBuilder = $this->createExpression($queryBuilder, $parameterName, $this->getFunc()); + + $queryBuilder->setParameter($parameterName, $this->getFilter()); + + dump($queryBuilder->getSQL()); + + return $queryBuilder; + } + + public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) + { + dump('creating query:'.$this->getField()); + $func = is_null($func) ? $this->getFunc() : $func; + $alias = $this->getTableAlias($this->getEntityName(), $queryBuilder); + $desc = $this->getQueryDescription(); + if (!$alias) { + if ($desc['func']) { + $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); + $expr = $queryBuilder->expr()->$func($desc['func'].'('.$alias.'.'.$this->getDBColumn()->getName().')', $this->getFilterConditionValue($parameterName)); + $queryBuilder = $queryBuilder->andHaving($expr); + } else { + if ($alias != 'l') { + $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); + } else { + dump('lead restriction'); + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + var_dump($expr); + die(); + $queryBuilder = $queryBuilder->andWhere($expr); + } + } + } else { + if ($alias != 'l') { + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); + } else { + $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); + $queryBuilder = $queryBuilder->andWhere($expr); + } + } + + return $queryBuilder; + } + + public function getDBTable() + { + //@todo cache metadata + try { + $tableName = $this->em->getClassMetadata($this->getEntityName())->getTableName(); + } catch (MappingException $e) { + return $this->getObject(); + } + + return $tableName; + } + + public function getEntityName() + { + $converter = new CamelCaseToSnakeCaseNameConverter(); + if ($this->getQueryDescription()) { + $table = $this->queryDescription['foreign_table']; + } else { + $table = $this->getObject(); + } + + $entity = sprintf('MauticLeadBundle:%s', ucfirst($converter->denormalize($table))); + + return $entity; + } + + /** + * @return Column + * + * @throws \Exception + */ + public function getDBColumn() + { + if (is_null($this->dbColumn)) { + if ($descr = $this->getQueryDescription()) { + $this->dbColumn = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; + } else { + $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); + if (!$dbTableColumns) { + var_dump($this); + throw new \Exception('Unknown database table and no translation provided for type "'.$this->getType().'"'); + } + if (!isset($dbTableColumns[$this->getField()])) { + throw new \Exception('Unknown database column and no translation provided for type "'.$this->getType().'"'); + } + $this->dbColumn = $dbTableColumns[$this->getField()]; + } + } + + return $this->dbColumn; + } + + /** + * @return string|null + */ + public function getGlue() + { + return $this->glue; + } + + /** + * @return string|null + */ + public function getField() + { + return $this->field; + } + + /** + * @return string|null + */ + public function getObject() + { + return $this->object; + } + + /** + * @return bool + */ + public function isLeadType() + { + return $this->object === self::LEAD_OBJECT; + } + + /** + * @return bool + */ + public function isCompanyType() + { + return $this->object === self::COMPANY_OBJECT; + } + + /** + * @return string|null + */ + public function getType() + { + return $this->type; + } + + /** + * @return string|array|null + */ + public function getFilter() + { + return $this->filter; + } + + /** + * @return string|null + */ + public function getDisplay() + { + return $this->display; + } + + /** + * @return string|null + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @param string|null $operator + */ + public function setOperator($operator) + { + $this->operator = $operator; + } + + /** + * @param string|array|bool|float|null $filter + */ + public function setFilter($filter) + { + $filter = $this->sanitizeFilter($filter); + + $this->filter = $filter; + } + + /** + * @return string + */ + public function getFunc() + { + return $this->func; + } + + /** + * @param string $func + */ + public function setFunc($func) + { + $this->func = $func; + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'glue' => $this->getGlue(), + 'field' => $this->getField(), + 'object' => $this->getObject(), + 'type' => $this->getType(), + 'filter' => $this->getFilter(), + 'display' => $this->getDisplay(), + 'operator' => $this->getOperator(), + 'func' => $this->getFunc(), + ]; + } + + /** + * @param string|array|bool|float|null $filter + * + * @return string|array|bool|float|null + */ + private function sanitizeFilter($filter) + { + if ($filter === null || is_array($filter) || !$this->getType()) { + return $filter; + } + + switch ($this->getType()) { + case 'number': + $filter = (float) $filter; + break; + + case 'boolean': + $filter = (bool) $filter; + break; + } + + return $filter; + } + + /** + * @return array + */ + public function getQueryDescription($dictionary = null) + { + if (is_null($this->queryDescription)) { + $this->translateQueryDescription($dictionary); + } + + return $this->queryDescription; + } + + /** + * @param array $queryDescription + * + * @return LeadSegmentFilter + */ + public function setQueryDescription($queryDescription) + { + $this->queryDescription = $queryDescription; + + return $this; + } + + /** + * @return $this + */ + public function translateQueryDescription(\ArrayIterator $dictionary = null) + { + $this->queryDescription = isset($dictionary[$this->getField()]) + ? $dictionary[$this->getField()] + : false; + + return $this; + } +} From 9bf4d7db7f69e3281e9b0ad58339419c2b21b765 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 15 Jan 2018 13:16:57 +0100 Subject: [PATCH 035/778] minor fixes and mods --- .../BaseFilterQueryBuilder.php | 2 +- .../DncFilterQueryBuilder.php | 90 ++++--------------- .../LeadBundle/Segment/LeadSegmentFilter.php | 15 ++++ .../LeadBundle/Segment/Query/QueryBuilder.php | 2 +- .../Services/LeadSegmentFilterDescriptor.php | 9 +- 5 files changed, 40 insertions(+), 78 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 44b7fa86ef9..1776ec268ab 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -134,7 +134,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->andHaving($expression); } else { dump($filter->getGlue()); - $queryBuilder->addJoinCondition($tableAlias, 'and ('.$expression.')'); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } } else { $queryBuilder->$filterGlueFunc($expression); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php index ee83dea3316..c81e4965c0e 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php @@ -8,6 +8,7 @@ namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; @@ -21,92 +22,39 @@ public static function getServiceId() return 'mautic.lead.query.builder.special.dnc'; } - public function aaa() - { - switch (false) { - case - 'dnc_bounced': - case 'dnc_unsubscribed': - case 'dnc_bounced_sms': - case 'dnc_unsubscribed_sms': - // Special handling of do not contact - - $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; - - $parts = explode('_', $leadSegmentFilter->getField()); - $channel = 'email'; - - if (count($parts) === 3) { - $channel = $parts[2]; - } - - $channelParameter = $this->generateRandomParameterName(); - $subqb = $this->entityManager->getConnection()->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) - ->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.reason', $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id'), - $q->expr() - ->eq($alias.'.channel', ":$channelParameter") - ) - ); - - $groupExpr->add( - sprintf('%s (%s)', $func, $subqb->getSQL()) - ); - - // Filter will always be true and differentiated via EXISTS/NOT EXISTS - $leadSegmentFilter->setFilter(true); - - $ignoreAutoFilter = true; - - $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; - $parameters[$channelParameter] = $channel; - - break; - } - } - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { - dump('dnc apply query:'); - var_dump($filter); - die(); - $parts = explode('_', $filter->getField()); + $parts = explode('_', $filter->getCrate('field')); $channel = 'email'; if (count($parts) === 3) { $channel = $parts[2]; } + $tableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'left'); + + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact = l.id'); + } + + $exprParameter = $this->generateRandomParameterName(); $channelParameter = $this->generateRandomParameterName(); - $subqb = $this->entityManager->getConnection()->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) - ->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.reason', $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id'), - $q->expr() - ->eq($alias.'.channel', ":$channelParameter") - ) - ); - $groupExpr->add( - sprintf('%s (%s)', $func, $subqb->getSQL()) + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($tableAlias.'.reason', ":$exprParameter"), + $queryBuilder->expr() + ->eq($tableAlias.'.channel', ":$channelParameter") ); - // Filter will always be true and differentiated via EXISTS/NOT EXISTS - $leadSegmentFilter->setFilter(true); + $queryBuilder->addJoinCondition($tableAlias, $expression); - $ignoreAutoFilter = true; + $queryType = $filter->getOperator() === 'eq' ? 'isNull' : 'isNotNull'; - $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; - $parameters[$channelParameter] = $channel; + $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); - $queryBuilder->setParametersPairs($parameters, $filterParameters); + $queryBuilder->setParameter($exprParameter, ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED); + $queryBuilder->setParameter($channelParameter, $channel); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index c92b5835170..f2a19d957de 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -112,6 +112,21 @@ public function getAggregateFunction() return $this->filterDecorator->getAggregateFunc($this->leadSegmentFilterCrate); } + public function getCrate($field = null) + { + $fields = (array) $this->toArray(); + + if (is_null($field)) { + return $fields; + } + + if (isset($fields[$field])) { + return $fields[$field]; + } + + throw new \Exception('Unknown crate field "'.$field."'"); + } + /** * @return array */ diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index ee360eb2c67..5ddacbee53f 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1398,7 +1398,7 @@ public function addJoinCondition($alias, $expr) foreach ($parts['l'] as $key => $part) { if ($part['joinAlias'] == $alias) { - $result['l'][$key]['joinCondition'] = $part['joinCondition'].' '.$expr.''; + $result['l'][$key]['joinCondition'] = $part['joinCondition'].' and '.$expr.''; } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index c2e8d02167f..1b650a58773 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -42,11 +42,10 @@ public function __construct() $this->translations['dnc_bounced'] = [ 'type' => DncFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'foreign_table_field' => 'lead_id', - 'table' => 'leads', - 'table_field' => 'id', - 'field' => 'date_hit', + ]; + + $this->translations['dnc_bounced_sms'] = [ + 'type' => DncFilterQueryBuilder::getServiceId(), ]; parent::__construct($this->translations); From fcab4afe0534a3ad1ed5f9fb38192882e372c9dc Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 15 Jan 2018 15:09:37 +0100 Subject: [PATCH 036/778] remove var_dumps --- app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php | 1 - app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php | 6 ------ app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php | 3 --- 3 files changed, 10 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index bdfcf909d9f..d18dffd6b49 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -167,7 +167,6 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter */ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId) { - var_dump(debug_backtrace()[1]['function']); $parameters = []; $schema = $this->entityManager->getConnection()->getSchemaManager(); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php index 8d41efd937d..2b2a8180abc 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php @@ -141,10 +141,7 @@ public function getFilterConditionValue($argument = null) } default: var_dump($this->getDBColumn()->getType()->getName()); - var_dump($this); - die(); } - var_dump($filter); throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); } @@ -181,9 +178,7 @@ public function createExpression(QueryBuilder $queryBuilder, $parameterName, $fu $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); } else { - dump('lead restriction'); $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); - var_dump($expr); die(); $queryBuilder = $queryBuilder->andWhere($expr); } @@ -240,7 +235,6 @@ public function getDBColumn() } else { $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); if (!$dbTableColumns) { - var_dump($this); throw new \Exception('Unknown database table and no translation provided for type "'.$this->getType().'"'); } if (!isset($dbTableColumns[$this->getField()])) { diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 85902dfe81f..08cc22e9b19 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -68,7 +68,6 @@ public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $ // SELECT count(l.id) as lead_count, max(l.id) as max_id FROM leads l // LEFT JOIN lead_lists_leads ll ON (ll.leadlist_id = 28) AND (ll.lead_id = l.id) AND (ll.date_added <= '2018-01-09 14:48:54') // WHERE (l.propertytype = :MglShQLG) AND (ll.lead_id IS NULL) - var_dump($whatever); return $queryBuilder; die(); @@ -802,8 +801,6 @@ private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) $q->setParameter($k, $v, $paramType); } - var_dump($parameters); - return $expr; } From f5d7af2b4e970c79428faad40ca8a0c73c169d84 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 10:28:27 +0100 Subject: [PATCH 037/778] add more query builder, now we need original sql to compare --- .../Entity/LeadListSegmentRepository.php | 3 - .../BaseFilterQueryBuilder.php | 67 ++++++++++- .../ForeignValueFilterQueryBuilder.php | 47 ++++++++ .../LeadBundle/Segment/LeadSegmentFilter.php | 17 ++- .../LeadBundle/Segment/LeadSegmentService.php | 9 +- .../Services/LeadSegmentFilterDescriptor.php | 42 +++++++ .../LeadSegmentFilterQueryBuilderTrait.php | 105 ++++-------------- .../Services/LeadSegmentQueryBuilder.php | 31 ------ 8 files changed, 198 insertions(+), 123 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index d18dffd6b49..005577effc1 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -84,9 +84,6 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte $expr = $this->generateSegmentExpression($leadSegmentFilters, $q, $id); - echo 'SQL parameters:'; - dump($q->getParameters()); - // Leads that do not have any record in the lead_lists_leads table for this lead list // For non null fields - it's apparently better to use left join over not exists due to not using nullable // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 1776ec268ab..9bec9be2da7 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -40,7 +40,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterParametersHolder = $filter->getParameterHolder($parameters); - dump(sprintf('START filter query for %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + // @debug we do not need this, it's just to verify we reference an existing database column + try { + $filter->getColumn(); + } catch (\Exception $e) { + dump(' * IGNORED * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + } $filterGlueFunc = $filterGlue.'Where'; @@ -133,7 +138,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($filterAggr) { $queryBuilder->andHaving($expression); } else { - dump($filter->getGlue()); $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } } else { @@ -144,4 +148,63 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter return $queryBuilder; } + + public function aaa() + { + switch (true) { + case 'tags': + case 'globalcategory': + case 'lead_email_received': + case 'lead_email_sent': + case 'device_type': + case 'device_brand': + case 'device_os': + // Special handling of lead lists and tags + $func = in_array($func, ['eq', 'in'], true) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + + // Collect these and apply after building the query because we'll want to apply the lead first for each of the subqueries + $subQueryFilters = []; + switch ($leadSegmentFilter->getField()) { + case 'tags': + $table = 'lead_tags_xref'; + $column = 'tag_id'; + break; + case 'globalcategory': + $table = 'lead_categories'; + $column = 'category_id'; + break; + case 'lead_email_received': + $table = 'email_stats'; + $column = 'email_id'; + + $trueParameter = $this->generateRandomParameterName(); + $subQueryFilters[$alias.'.is_read'] = $trueParameter; + $parameters[$trueParameter] = true; + break; + case 'lead_email_sent': + $table = 'email_stats'; + $column = 'email_id'; + break; + case 'device_type': + $table = 'lead_devices'; + $column = 'device'; + break; + case 'device_brand': + $table = 'lead_devices'; + $column = 'device_brand'; + break; + case 'device_os': + $table = 'lead_devices'; + $column = 'device_os_name'; + break; + } + + $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $leadSegmentFilter->getFilter(), $parameters, $subQueryFilters); + + $groupExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); + break; + } + } } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php index d110103c548..1e5f37c0fd9 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php @@ -8,6 +8,53 @@ namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\Query\QueryBuilder; + class ForeignValueFilterQueryBuilder extends BaseFilterQueryBuilder { + public static function getServiceId() + { + return 'mautic.lead.query.builder.foreign.value'; + } + + public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + { + $filterOperator = $filter->getOperator(); + + $filterParameters = $filter->getParameterValue(); + + if (is_array($filterParameters)) { + $parameters = []; + foreach ($filterParameters as $filterParameter) { + $parameters[] = $this->generateRandomParameterName(); + } + } else { + $parameters = $this->generateRandomParameterName(); + } + + $filterParametersHolder = $filter->getParameterHolder($parameters); + + $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + + if (!$tableAlias) { + $queryBuilder = $queryBuilder->innerJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + $expression + ); + } else { + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + } + + $queryBuilder->setParametersPairs($parameters, $filterParameters); + + return $queryBuilder; + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index f2a19d957de..3e9b5fb0497 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment; -use Doctrine\DBAL\Schema\Column; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; @@ -39,8 +38,10 @@ class LeadSegmentFilter */ private $filterQueryBuilder; - /** @var Column */ - private $dbColumn; + /** + * @var EntityManager + */ + private $em; public function __construct( LeadSegmentFilterCrate $leadSegmentFilterCrate, @@ -52,6 +53,16 @@ public function __construct( $this->em = $em; } + public function getColumn() + { + $columns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getTable()); + if (!isset($columns[$this->getField()])) { + throw new \Exception(sprintf('Database schema does not contain field %s.%s', $this->getTable(), $this->getField())); + } + + return $columns[$this->getField()]; + } + public function getEntity() { $converter = new CamelCaseToSnakeCaseNameConverter(); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 6550c0a9350..b92fda8bb46 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -14,10 +14,13 @@ use Doctrine\DBAL\Query\QueryBuilder; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListSegmentRepository; +use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; use Mautic\LeadBundle\Services\LeadSegmentQueryBuilder; class LeadSegmentService { + use LeadSegmentFilterQueryBuilderTrait; + /** * @var LeadListSegmentRepository */ @@ -50,9 +53,11 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); - dump($qb->getQueryParts()); + $qb = $this->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); + + dump($qb->getSQL()); dump($qb->getParameters()); - dump($qb->execute()); + dump($qb->getFirstResult()); return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index 1b650a58773..e3867a10ca5 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -48,6 +48,48 @@ public function __construct() 'type' => DncFilterQueryBuilder::getServiceId(), ]; + $this->translations['globalcategory'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_categories', + 'field' => 'category_id', + ]; + + $this->translations['tags'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_tags_xref', + 'field' => 'tag_id', + ]; + + $this->translations['lead_email_sent'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'email_stats', + 'field' => 'email_id', + ]; + + $this->translations['device_type'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device', + ]; + + $this->translations['device_brand'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device_brand', + ]; + + $this->translations['device_os'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device_os_name', + ]; + + $this->translations['device_model'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device_model', + ]; + parent::__construct($this->translations); } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index fc213353e9a..e0fe3042357 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -8,11 +8,11 @@ namespace Mautic\LeadBundle\Services; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; trait LeadSegmentFilterQueryBuilderTrait { + // @todo make static to asure single instance protected $parameterAliases = []; /** @@ -35,92 +35,33 @@ protected function generateRandomParameterName() return $this->generateRandomParameterName(); } - // should be used by filter - protected function createJoin(QueryBuilder $queryBuilder, $target, $alias, $joinOn = '', $from = 'MauticLeadBundle:Lead') + public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) { - $queryBuilder = $queryBuilder->leftJoin($this->getTableAlias($from, $queryBuilder), $target, $alias, sprintf( - '%s.id = %s.lead_id'.($joinOn ? " and $joinOn" : ''), - $this->getTableAlias($from, $queryBuilder), - $alias - )); + $queryBuilder->select('max(l.id) maxId, count(l.id) as leadCount'); + $queryBuilder->addGroupBy('l.id'); - return $queryBuilder; - } + $parts = $queryBuilder->getQueryParts(); + $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); - protected function addForeignTableQuery(QueryBuilder $qb, LeadSegmentFilter $filter) - { - $filter->createJoin($qb, $alias); - if (isset($translated) && $translated) { - if (isset($translated['func'])) { - //@todo rewrite with getFullQualifiedName - $qb->leftJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'])); - - //@todo rewrite with getFullQualifiedName - $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); - } else { - //@todo rewrite with getFullQualifiedName - $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); - } - - $qb->setParameter($parameterHolder, $filter->getFilter()); - - $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); - } else { - // Default behaviour, translation not necessary - } - } + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id'); + $queryBuilder->addSelect($tableAlias.'.lead_id'); - /** - * @param QueryBuilder $qb - * @param $filter - * @param null $alias use alias to extend current query - * - * @throws \Exception - */ - private function addForeignTableQueryWhere(QueryBuilder $qb, $filter, $alias = null) - { - dump($filter); - if (is_array($filter)) { - $alias = is_null($alias) ? $this->generateRandomParameterName() : $alias; - foreach ($filter as $singleFilter) { - $qb = $this->addForeignTableQueryWhere($qb, $singleFilter, $alias); - } - - return $qb; + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $leadListId), + $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$whatever['dateTime']."'") + ); + + $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'.lead_id'); + + $queryBuilder->addJoinCondition($tableAlias, $expression); + + if ($setHaving) { + $queryBuilder->andHaving($restrictionExpression); + } else { + $queryBuilder->andWhere($restrictionExpression); } - $parameterHolder = $this->generateRandomParameterName(); - $qb = $filter->createExpression($qb, $parameterHolder); - - return $qb; - dump($expr); - die(); - - //$qb = $qb->andWhere($expr); - $qb->setParameter($parameterHolder, $filter->getFilter()); - - //var_dump($qb->getSQL()); die(); - -// die(); -// -// if (isset($translated) && $translated) { -// if (isset($translated['func'])) { -// //@todo rewrite with getFullQualifiedName -// $qb->leftJoin($this->getTableAlias($filter->get), $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'])); -// -// //@todo rewrite with getFullQualifiedName -// $qb->andHaving(isset($translated['func']) ? sprintf('%s(%s.%s) %s %s', $translated['func'], $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)) : sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $this->getFilterOperator($filter), $this->getFilterValue($filter, $parameterHolder, $dbColumn))); -// -// } -// else { -// //@todo rewrite with getFullQualifiedName -// $qb->innerJoin($this->tableAliases[$translated['table']], $translated['foreign_table'], $this->tableAliases[$translated['foreign_table']], sprintf('%s.%s = %s.%s and %s', $this->tableAliases[$translated['table']], $translated['table_field'], $this->tableAliases[$translated['foreign_table']], $translated['foreign_table_field'], sprintf('%s.%s %s %s', $this->tableAliases[$translated['foreign_table']], $translated['field'], $filter->getSQLOperator(), $filter->getFilterConditionValue($parameterHolder)))); -// } -// -// -// $qb->setParameter($parameterHolder, $filter->getFilter()); -// -// $qb->groupBy(sprintf('%s.%s', $this->tableAliases[$translated['table']], $translated['table_field'])); -// } + return $queryBuilder; } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 08cc22e9b19..e4a46ce587d 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -42,37 +42,6 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r $this->schema = $this->entityManager->getConnection()->getSchemaManager(); } - public function addLeadListRestrictions(QueryBuilder $queryBuilder, $whatever, $leadListId, $dictionary) - { - $filter_list_id = new LeadSegmentFilter([ - 'glue' => 'and', - 'field' => 'leadlist_id', - 'object' => 'lead_lists_leads', - 'type' => 'number', - 'filter' => intval($leadListId), - 'operator' => '=', - 'func' => 'eq', - ], $dictionary, $this->entityManager); - - $filter_list_added = new LeadSegmentFilter([ - 'glue' => 'and', - 'field' => 'date_added', - 'object' => 'lead_lists_leads', - 'type' => 'date', - 'filter' => $whatever, - 'operator' => '=', - 'func' => 'lte', - ], $dictionary, $this->entityManager); - - $queryBuilder = $this->addForeignTableQueryWhere($queryBuilder, [$filter_list_id, $filter_list_added]); - // SELECT count(l.id) as lead_count, max(l.id) as max_id FROM leads l - // LEFT JOIN lead_lists_leads ll ON (ll.leadlist_id = 28) AND (ll.lead_id = l.id) AND (ll.date_added <= '2018-01-09 14:48:54') - // WHERE (l.propertytype = :MglShQLG) AND (ll.lead_id IS NULL) - - return $queryBuilder; - die(); - } - public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { /** @var QueryBuilder $queryBuilder */ From 53218d831b66efa257789157dc43ada31f345b53 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 11:07:58 +0100 Subject: [PATCH 038/778] fix foreign value joins, improve debug output, fix foreign value joins --- .../Segment/Decorator/BaseDecorator.php | 4 +- .../BaseFilterQueryBuilder.php | 61 +--- .../DncFilterQueryBuilder.php | 2 +- .../ForeignValueFilterQueryBuilder.php | 17 +- .../LeadBundle/Segment/LeadSegmentService.php | 12 +- .../Query/Expression/CompositeExpression.php | 135 ++++++++ .../Query/Expression/ExpressionBuilder.php | 314 ++++++++++++++++++ .../Segment/Query/QueryException.php | 54 +++ .../Services/LeadSegmentQueryBuilder.php | 2 +- 9 files changed, 528 insertions(+), 73 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php create mode 100644 app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php create mode 100644 app/bundles/LeadBundle/Segment/Query/QueryException.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 4cf82a1546d..bc68d4e2b43 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -40,7 +40,7 @@ public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) { $originalField = $leadSegmentFilterCrate->getField(); - if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + if (empty($this->leadSegmentFilterDescriptor[$originalField]['field'])) { return $originalField; } @@ -51,7 +51,7 @@ public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) { $originalField = $leadSegmentFilterCrate->getField(); - if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + if (empty($this->leadSegmentFilterDescriptor[$originalField]['foreign_table'])) { if ($leadSegmentFilterCrate->isLeadType()) { return 'leads'; } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 9bec9be2da7..5d01afc2a0f 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -44,7 +44,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter try { $filter->getColumn(); } catch (\Exception $e) { - dump(' * IGNORED * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + dump(' * ERROR * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); } $filterGlueFunc = $filterGlue.'Where'; @@ -148,63 +148,4 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter return $queryBuilder; } - - public function aaa() - { - switch (true) { - case 'tags': - case 'globalcategory': - case 'lead_email_received': - case 'lead_email_sent': - case 'device_type': - case 'device_brand': - case 'device_os': - // Special handling of lead lists and tags - $func = in_array($func, ['eq', 'in'], true) ? 'EXISTS' : 'NOT EXISTS'; - - $ignoreAutoFilter = true; - - // Collect these and apply after building the query because we'll want to apply the lead first for each of the subqueries - $subQueryFilters = []; - switch ($leadSegmentFilter->getField()) { - case 'tags': - $table = 'lead_tags_xref'; - $column = 'tag_id'; - break; - case 'globalcategory': - $table = 'lead_categories'; - $column = 'category_id'; - break; - case 'lead_email_received': - $table = 'email_stats'; - $column = 'email_id'; - - $trueParameter = $this->generateRandomParameterName(); - $subQueryFilters[$alias.'.is_read'] = $trueParameter; - $parameters[$trueParameter] = true; - break; - case 'lead_email_sent': - $table = 'email_stats'; - $column = 'email_id'; - break; - case 'device_type': - $table = 'lead_devices'; - $column = 'device'; - break; - case 'device_brand': - $table = 'lead_devices'; - $column = 'device_brand'; - break; - case 'device_os': - $table = 'lead_devices'; - $column = 'device_os_name'; - break; - } - - $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $leadSegmentFilter->getFilter(), $parameters, $subQueryFilters); - - $groupExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); - break; - } - } } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php index c81e4965c0e..2a74751ae17 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php @@ -35,7 +35,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact = l.id'); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact.lead_id = l.id'); } $exprParameter = $this->generateRandomParameterName(); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php index 1e5f37c0fd9..74694108413 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php @@ -37,22 +37,23 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); - $expression = $queryBuilder->expr()->$filterOperator( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ); - if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder = $queryBuilder->innerJoin( $queryBuilder->getTableAlias('leads'), $filter->getTable(), $tableAlias, - $expression + $tableAlias.'.lead_id = l.id' ); - } else { - $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + $queryBuilder->setParametersPairs($parameters, $filterParameters); return $queryBuilder; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index b92fda8bb46..b21576ba859 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -55,9 +55,19 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) $qb = $this->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); + dump($qb->getQueryParts()); dump($qb->getSQL()); + dump($qb->getParameters()); - dump($qb->getFirstResult()); + + try { + $results = $qb->execute()->fetchAll(); + foreach ($results as $result) { + var_dump($result); + } + } catch (\Exception $e) { + dump('Query exception: '.$e->getMessage()); + } return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php b/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php new file mode 100644 index 00000000000..334bfb9ee24 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php @@ -0,0 +1,135 @@ +. + */ + +namespace Doctrine\DBAL\Query\Expression; + +/** + * Composite expression is responsible to build a group of similar expression. + * + * @see www.doctrine-project.org + * @since 2.1 + * + * @author Guilherme Blanco + * @author Benjamin Eberlei + */ +class CompositeExpression implements \Countable +{ + /** + * Constant that represents an AND composite expression. + */ + const TYPE_AND = 'AND'; + + /** + * Constant that represents an OR composite expression. + */ + const TYPE_OR = 'OR'; + + /** + * The instance type of composite expression. + * + * @var string + */ + private $type; + + /** + * Each expression part of the composite expression. + * + * @var array + */ + private $parts = []; + + /** + * Constructor. + * + * @param string $type instance type of composite expression + * @param array $parts composition of expressions to be joined on composite expression + */ + public function __construct($type, array $parts = []) + { + $this->type = $type; + + $this->addMultiple($parts); + } + + /** + * Adds multiple parts to composite expression. + * + * @param array $parts + * + * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + */ + public function addMultiple(array $parts = []) + { + foreach ((array) $parts as $part) { + $this->add($part); + } + + return $this; + } + + /** + * Adds an expression to composite expression. + * + * @param mixed $part + * + * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + */ + public function add($part) + { + if (!empty($part) || ($part instanceof self && $part->count() > 0)) { + $this->parts[] = $part; + } + + return $this; + } + + /** + * Retrieves the amount of expressions on composite expression. + * + * @return int + */ + public function count() + { + return count($this->parts); + } + + /** + * Retrieves the string representation of this composite expression. + * + * @return string + */ + public function __toString() + { + if (count($this->parts) === 1) { + return (string) $this->parts[0]; + } + + return '('.implode(') '.$this->type.' (', $this->parts).')'; + } + + /** + * Returns the type of this composite expression (AND/OR). + * + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php new file mode 100644 index 00000000000..72d5791607c --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -0,0 +1,314 @@ +. + */ + +namespace Doctrine\DBAL\Query\Expression; + +use Doctrine\DBAL\Connection; + +/** + * ExpressionBuilder class is responsible to dynamically create SQL query parts. + * + * @see www.doctrine-project.org + * @since 2.1 + * + * @author Guilherme Blanco + * @author Benjamin Eberlei + */ +class ExpressionBuilder +{ + const EQ = '='; + const NEQ = '<>'; + const LT = '<'; + const LTE = '<='; + const GT = '>'; + const GTE = '>='; + + /** + * The DBAL Connection. + * + * @var \Doctrine\DBAL\Connection + */ + private $connection; + + /** + * Initializes a new ExpressionBuilder. + * + * @param \Doctrine\DBAL\Connection $connection the DBAL Connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Creates a conjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?) AND (u.role = ?) + * $expr->andX('u.type = ?', 'u.role = ?')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * + * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + */ + public function andX($x = null) + { + return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + /** + * Creates a disjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?) OR (u.role = ?) + * $qb->where($qb->expr()->orX('u.type = ?', 'u.role = ?')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * + * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + */ + public function orX($x = null) + { + return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args()); + } + + /** + * Creates a comparison expression. + * + * @param mixed $x the left expression + * @param string $operator one of the ExpressionBuilder::* constants + * @param mixed $y the right expression + * + * @return string + */ + public function comparison($x, $operator, $y) + { + return $x.' '.$operator.' '.$y; + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function eq($x, $y) + { + return $this->comparison($x, self::EQ, $y); + } + + /** + * Creates a non equality comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <> . Example:. + * + * [php] + * // u.id <> 1 + * $q->where($q->expr()->neq('u.id', '1')); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function neq($x, $y) + { + return $this->comparison($x, self::NEQ, $y); + } + + /** + * Creates a lower-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a < . Example:. + * + * [php] + * // u.id < ? + * $q->where($q->expr()->lt('u.id', '?')); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function lt($x, $y) + { + return $this->comparison($x, self::LT, $y); + } + + /** + * Creates a lower-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <= . Example:. + * + * [php] + * // u.id <= ? + * $q->where($q->expr()->lte('u.id', '?')); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function lte($x, $y) + { + return $this->comparison($x, self::LTE, $y); + } + + /** + * Creates a greater-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a > . Example:. + * + * [php] + * // u.id > ? + * $q->where($q->expr()->gt('u.id', '?')); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function gt($x, $y) + { + return $this->comparison($x, self::GT, $y); + } + + /** + * Creates a greater-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a >= . Example:. + * + * [php] + * // u.id >= ? + * $q->where($q->expr()->gte('u.id', '?')); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function gte($x, $y) + { + return $this->comparison($x, self::GTE, $y); + } + + /** + * Creates an IS NULL expression with the given arguments. + * + * @param string $x the field in string format to be restricted by IS NULL + * + * @return string + */ + public function isNull($x) + { + return $x.' IS NULL'; + } + + /** + * Creates an IS NOT NULL expression with the given arguments. + * + * @param string $x the field in string format to be restricted by IS NOT NULL + * + * @return string + */ + public function isNotNull($x) + { + return $x.' IS NOT NULL'; + } + + /** + * Creates a LIKE() comparison expression with the given arguments. + * + * @param string $x field in string format to be inspected by LIKE() comparison + * @param mixed $y argument to be used in LIKE() comparison + * + * @return string + */ + public function like($x, $y) + { + return $this->comparison($x, 'LIKE', $y); + } + + /** + * Creates a NOT LIKE() comparison expression with the given arguments. + * + * @param string $x field in string format to be inspected by NOT LIKE() comparison + * @param mixed $y argument to be used in NOT LIKE() comparison + * + * @return string + */ + public function notLike($x, $y) + { + return $this->comparison($x, 'NOT LIKE', $y); + } + + /** + * Creates a IN () comparison expression with the given arguments. + * + * @param string $x the field in string format to be inspected by IN() comparison + * @param string|array $y the placeholder or the array of values to be used by IN() comparison + * + * @return string + */ + public function in($x, $y) + { + return $this->comparison($x, 'IN', '('.implode(', ', (array) $y).')'); + } + + /** + * Creates a NOT IN () comparison expression with the given arguments. + * + * @param string $x the field in string format to be inspected by NOT IN() comparison + * @param string|array $y the placeholder or the array of values to be used by NOT IN() comparison + * + * @return string + */ + public function notIn($x, $y) + { + return $this->comparison($x, 'NOT IN', '('.implode(', ', (array) $y).')'); + } + + /** + * Quotes a given input parameter. + * + * @param mixed $input the parameter to be quoted + * @param string|null $type the type of the parameter + * + * @return string + */ + public function literal($input, $type = null) + { + return $this->connection->quote($input, $type); + } +} diff --git a/app/bundles/LeadBundle/Segment/Query/QueryException.php b/app/bundles/LeadBundle/Segment/Query/QueryException.php new file mode 100644 index 00000000000..9063c6d0ad3 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Query/QueryException.php @@ -0,0 +1,54 @@ +. + */ + +namespace Mautic\LeadBundle\Segment\Query; + +use Doctrine\DBAL\DBALException; + +/** + * @since 2.1.4 + */ +class QueryException extends DBALException +{ + /** + * @param string $alias + * @param array $registeredAliases + * + * @return \Doctrine\DBAL\Query\QueryException + */ + public static function unknownAlias($alias, $registeredAliases) + { + return new self("The given alias '".$alias."' is not part of ". + 'any FROM or JOIN clause table. The currently registered '. + 'aliases are: '.implode(', ', $registeredAliases).'.'); + } + + /** + * @param string $alias + * @param array $registeredAliases + * + * @return \Doctrine\DBAL\Query\QueryException + */ + public static function nonUniqueAlias($alias, $registeredAliases) + { + return new self("The given alias '".$alias."' is not unique ". + 'in FROM and JOIN clause table. The currently registered '. + 'aliases are: '.implode(', ', $registeredAliases).'.'); + } +} diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index e4a46ce587d..c56344c1d8a 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -87,7 +87,7 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters // remove any possible group by $q->resetQueryPart('groupBy'); - dump($q->getSQL()); + var_dump($q->getSQL()); echo 'SQL parameters:'; dump($q->getParameters()); From 5c023035cd57c3599b3465e88b74477477cc2379 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 16 Jan 2018 12:31:56 +0100 Subject: [PATCH 039/778] Handle like and regex values --- app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index bc68d4e2b43..3d83cd5b049 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator; +use Mautic\LeadBundle\Entity\RegexTrait; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\LeadSegmentFilterOperator; @@ -18,6 +19,8 @@ class BaseDecorator implements FilterDecoratorInterface { + use RegexTrait; + /** * @var LeadSegmentFilterOperator */ @@ -116,12 +119,16 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate switch ($this->getOperator($leadSegmentFilterCrate)) { case 'like': case 'notLike': + return strpos($filter, '%') === false ? '%'.$filter.'%' : $filter; case 'contains': return '%'.$filter.'%'; case 'startsWith': return $filter.'%'; case 'endsWith': return '%'.$filter; + case 'regexp': + case 'notRegexp': + return $this->prepareRegex($filter); } return $filter; From efb783e67d2afb55efcc0d0133c61574571dc84a Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 16 Jan 2018 12:39:37 +0100 Subject: [PATCH 040/778] Remove dumps from Unnused repository --- .../LeadBundle/Entity/LeadListSegmentRepository.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 005577effc1..3ec8c18de24 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -124,10 +124,6 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte // remove any possible group by $q->resetQueryPart('groupBy'); - dump($q->getSQL()); - echo 'SQL parameters:'; - dump($q->getParameters()); - $results = $q->execute()->fetchAll(); $leads = []; @@ -208,9 +204,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $ignoreAutoFilter = false; $func = $leadSegmentFilter->getFunc(); - - dump($func); - dump($leadSegmentFilter->getField()); // Generate a unique alias $alias = $this->generateRandomParameterName(); From 972465ba197d6cf15d1a8ac3775c1e45c904a5dc Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 12:54:32 +0100 Subject: [PATCH 041/778] multiple changes, won't describe --- .../BaseFilterQueryBuilder.php | 41 +++++++----- .../DncFilterQueryBuilder.php | 2 +- .../ForeignValueFilterQueryBuilder.php | 2 +- .../LeadListFilterQueryBuilder.php | 63 +++++++++++++++++++ .../LeadBundle/Segment/LeadSegmentService.php | 25 ++++++-- .../LeadBundle/Segment/Query/QueryBuilder.php | 8 ++- .../Services/LeadSegmentFilterDescriptor.php | 45 ++----------- .../LeadSegmentFilterQueryBuilderTrait.php | 1 - 8 files changed, 122 insertions(+), 65 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 5d01afc2a0f..2a948ab8d46 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -27,6 +27,15 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterGlue = $filter->getGlue(); $filterAggr = $filter->getAggregateFunction(); + // @debug we do not need this, it's just to verify we reference an existing database column + try { + $filter->getColumn(); + } catch (\Exception $e) { + dump(' * ERROR * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + + return $queryBuilder; + } + $filterParameters = $filter->getParameterValue(); if (is_array($filterParameters)) { @@ -40,13 +49,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterParametersHolder = $filter->getParameterHolder($parameters); - // @debug we do not need this, it's just to verify we reference an existing database column - try { - $filter->getColumn(); - } catch (\Exception $e) { - dump(' * ERROR * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); - } - $filterGlueFunc = $filterGlue.'Where'; $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); @@ -56,13 +58,16 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = false; } +// dump($filter->getTable()); if ($filter->getTable()=='companies') { +// dump('companies'); +// } + if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); switch ($filterOperator) { case 'notLike': case 'notIn': - case 'empty': case 'startsWith': case 'gt': @@ -75,19 +80,25 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'in': //@todo this logic needs to if ($filterAggr) { - $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->leftJoin( $queryBuilder->getTableAlias('leads'), $filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); } else { - $queryBuilder = $queryBuilder->innerJoin( - $queryBuilder->getTableAlias('leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) - ); + if ($filter->getTable() == 'companies') { + $relTable = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); + $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + } else { + $queryBuilder->leftJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + ); + } } break; default: diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php index 2a74751ae17..5d8f4066727 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php @@ -35,7 +35,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact.lead_id = l.id'); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, $tableAlias.'.lead_id = l.id'); } $exprParameter = $this->generateRandomParameterName(); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php index 74694108413..b7855c00f10 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php @@ -40,7 +40,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); - $queryBuilder = $queryBuilder->innerJoin( + $queryBuilder = $queryBuilder->leftJoin( $queryBuilder->getTableAlias('leads'), $filter->getTable(), $tableAlias, diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php new file mode 100644 index 00000000000..e2d3b9742ab --- /dev/null +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php @@ -0,0 +1,63 @@ +getCrate('field')); + $channel = 'email'; + + if (count($parts) === 3) { + $channel = $parts[2]; + } + + $tableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'left'); + + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact.lead_id = l.id'); + } + + $exprParameter = $this->generateRandomParameterName(); + $channelParameter = $this->generateRandomParameterName(); + + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($tableAlias.'.reason', ":$exprParameter"), + $queryBuilder->expr() + ->eq($tableAlias.'.channel', ":$channelParameter") + ); + + $queryBuilder->addJoinCondition($tableAlias, $expression); + + $queryType = $filter->getOperator() === 'eq' ? 'isNull' : 'isNotNull'; + + $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); + + $queryBuilder->setParameter($exprParameter, ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED); + $queryBuilder->setParameter($channelParameter, $channel); + + return $queryBuilder; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index b21576ba859..c583d7a0725 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -46,6 +46,15 @@ public function __construct( $this->queryBuilder = $queryBuilder; } + public function getDqlWithParams(Doctrine_Query $query) + { + $vals = $query->getFlattenedParams(); + $sql = $query->getDql(); + $sql = str_replace('?', '%s', $sql); + + return vsprintf($sql, $vals); + } + public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) { $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($entity); @@ -55,19 +64,27 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) $qb = $this->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); +// $qb->andWhere('l.sssss=1'); dump($qb->getQueryParts()); - dump($qb->getSQL()); + $sql = $qb->getSQL(); - dump($qb->getParameters()); + foreach ($qb->getParameters() as $k=>$v) { + $sql = str_replace(":$k", "'$v'", $sql); + } + echo '
'; + echo $sql; try { - $results = $qb->execute()->fetchAll(); + $stmt = $qb->execute(); + $results = $stmt->fetchAll(); + dump($results); foreach ($results as $result) { - var_dump($result); + dump($result); } } catch (\Exception $e) { dump('Query exception: '.$e->getMessage()); } + echo '
'; return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 5ddacbee53f..513d5b3a7fc 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1396,9 +1396,11 @@ public function addJoinCondition($alias, $expr) { $result = $parts = $this->getQueryPart('join'); - foreach ($parts['l'] as $key => $part) { - if ($part['joinAlias'] == $alias) { - $result['l'][$key]['joinCondition'] = $part['joinCondition'].' and '.$expr.''; + foreach ($parts as $tbl=>$joins) { + foreach ($joins as $key=>$join) { + if ($join['joinAlias'] == $alias) { + $result[$tbl][$key]['joinCondition'] = $join['joinCondition'].' and '.$expr; + } } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index e3867a10ca5..3a27cd6e023 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -14,6 +14,7 @@ use Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder; class LeadSegmentFilterDescriptor extends \ArrayIterator { @@ -48,6 +49,10 @@ public function __construct() 'type' => DncFilterQueryBuilder::getServiceId(), ]; + $this->translations['leadlist'] = [ + 'type' => LeadListFilterQueryBuilder::getServiceId(), + ]; + $this->translations['globalcategory'] = [ 'type' => ForeignValueFilterQueryBuilder::getServiceId(), 'foreign_table' => 'lead_categories', @@ -93,43 +98,3 @@ public function __construct() parent::__construct($this->translations); } } - -//case 'dnc_bounced': -// case 'dnc_unsubscribed': -// case 'dnc_bounced_sms': -// case 'dnc_unsubscribed_sms': -// // Special handling of do not contact -// $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; -// -// $parts = explode('_', $leadSegmentFilter->getField()); -// $channel = 'email'; -// -// if (count($parts) === 3) { -// $channel = $parts[2]; -// } -// -// $channelParameter = $this->generateRandomParameterName(); -// $subqb = $this->entityManager->getConnection()->createQueryBuilder() -// ->select('null') -// ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) -// ->where( -// $q->expr()->andX( -// $q->expr()->eq($alias.'.reason', $exprParameter), -// $q->expr()->eq($alias.'.lead_id', 'l.id'), -// $q->expr()->eq($alias.'.channel', ":$channelParameter") -// ) -// ); -// -// $groupExpr->add( -// sprintf('%s (%s)', $func, $subqb->getSQL()) -// ); -// -// // Filter will always be true and differentiated via EXISTS/NOT EXISTS -// $leadSegmentFilter->setFilter(true); -// -// $ignoreAutoFilter = true; -// -// $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; -// $parameters[$channelParameter] = $channel; -// -// break; diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index e0fe3042357..7d269dc7fdc 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -38,7 +38,6 @@ protected function generateRandomParameterName() public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) { $queryBuilder->select('max(l.id) maxId, count(l.id) as leadCount'); - $queryBuilder->addGroupBy('l.id'); $parts = $queryBuilder->getQueryParts(); $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); From 4a537d7c42fd2d32745ee629278cee97c88b621a Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 13:16:09 +0100 Subject: [PATCH 042/778] remove debug die's, add output of original method --- app/bundles/LeadBundle/Entity/LeadListRepository.php | 3 --- app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php | 2 -- app/bundles/LeadBundle/Model/ListModel.php | 3 ++- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index b1aebff0b76..22526d67bd2 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -389,8 +389,6 @@ public function getLeadsByList($lists, $args = []) if ($newOnly || !$nonMembersOnly) { // !$nonMembersOnly is mainly used for tests as we just want a live count $expr = $this->generateSegmentExpression($filters, $parameters, $q, null, $id); - dump($expr); - dump($expr->count()); if (!$this->hasCompanyFilter && !$expr->count()) { // Treat this as if it has no filters since all the filters are now invalid (fields were deleted) $return[$id] = []; @@ -511,7 +509,6 @@ public function getLeadsByList($lists, $args = []) dump($q->getSQL()); dump($q->getParameters()); - die(); $results = $q->execute()->fetchAll(); foreach ($results as $r) { diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 3ec8c18de24..e1c09aa7b7c 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -1033,8 +1033,6 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $q->setParameter($k, $v, $paramType); } - var_dump($parameters); - return $expr; } diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 342b3934b6c..c18ebad5bd0 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -30,7 +30,6 @@ use Mautic\LeadBundle\Helper\FormFieldHelper; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\LeadSegmentService; -use Mautic\LeadBundle\Services\LeadSegmentQueryBuilder; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -791,6 +790,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa // Get a count of leads to add $newLeadsCount = $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); + echo "
Petr's version result:"; dump($newLeadsCount); // Get a count of leads to add $newLeadsCount = $this->getLeadsByList( @@ -802,6 +802,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa 'batchLimiters' => $batchLimiters, ] ); + echo '
Original result:'; dump($newLeadsCount); exit; // Ensure the same list is used each batch From fd4ee6e981101bfe09a54db5e13cd9385877b256 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 14:46:18 +0100 Subject: [PATCH 043/778] show 3 query versions on debug output --- .../LeadBundle/Entity/LeadListRepository.php | 3 +++ .../Entity/LeadListSegmentRepository.php | 5 +++++ app/bundles/LeadBundle/Model/ListModel.php | 11 ++++++++-- .../LeadBundle/Segment/LeadSegmentService.php | 22 +++++++++++++++++-- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 22526d67bd2..83aa8eb4893 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -509,7 +509,10 @@ public function getLeadsByList($lists, $args = []) dump($q->getSQL()); dump($q->getParameters()); + $start = microtime(true); $results = $q->execute()->fetchAll(); + $end = microtime(true) - $start; + dump('Query took '.$end.'ms'); foreach ($results as $r) { if ($countOnly) { diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index e1c09aa7b7c..f46ddae4e1d 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -124,7 +124,12 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte // remove any possible group by $q->resetQueryPart('groupBy'); + dump($q->getSQL()); + + $start = microtime(true); $results = $q->execute()->fetchAll(); + $end = microtime(true) - $start; + dump('Query took '.$end.'ms'); $leads = []; foreach ($results as $r) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index c18ebad5bd0..1fa824bad8c 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -776,6 +776,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa $id = $entity->getId(); $list = ['id' => $id, 'filters' => $entity->getFilters()]; $dtHelper = new DateTimeHelper(); + $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); $batchLimiters = [ 'dateTime' => $dtHelper->toUtcString(), @@ -790,8 +791,12 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa // Get a count of leads to add $newLeadsCount = $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); - echo "
Petr's version result:"; + dump($newLeadsCount); + echo '
Original result:'; + + $versionStart = microtime(true); + // Get a count of leads to add $newLeadsCount = $this->getLeadsByList( $list, @@ -802,7 +807,9 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa 'batchLimiters' => $batchLimiters, ] ); - echo '
Original result:'; + $versionEnd = microtime(true) - $versionStart; + dump('Total query assembly took:'.$versionEnd.'ms'); + dump($newLeadsCount); exit; // Ensure the same list is used each batch diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index c583d7a0725..0876618abc9 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -59,11 +59,13 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) { $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($entity); + echo '
New version result:'; + $versionStart = microtime(true); + /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); $qb = $this->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); - // $qb->andWhere('l.sssss=1'); dump($qb->getQueryParts()); $sql = $qb->getSQL(); @@ -75,8 +77,17 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) echo '
'; echo $sql; try { + $start = microtime(true); + $stmt = $qb->execute(); $results = $stmt->fetchAll(); + + $end = microtime(true) - $start; + dump('Query took '.$end.'ms'); + + $versionEnd = microtime(true) - $versionStart; + dump('Total query assembly took:'.$versionEnd.'ms'); + dump($results); foreach ($results as $result) { dump($result); @@ -86,6 +97,13 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) } echo '
'; - return $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); + echo "
Petr's version result:"; + $versionStart = microtime(true); + + $result = $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); + $versionEnd = microtime(true) - $versionStart; + dump('Total query assembly took:'.$versionEnd.'ms'); + + return $result; } } From 71e37637a584d699c82cc3f9095044db8d6199e7 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 15:11:11 +0100 Subject: [PATCH 044/778] add empty and nonEmpty handling, add stages description --- .../BaseFilterQueryBuilder.php | 7 +++++ .../ForeignValueFilterQueryBuilder.php | 28 ++++++++++++++----- .../Services/LeadSegmentFilterDescriptor.php | 7 +++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 2a948ab8d46..c62f177797d 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -115,6 +115,13 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter ); $queryBuilder->setParameter($emptyParameter, ''); break; + case 'notEmpty': + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) + ); + $queryBuilder->setParameter($emptyParameter, ''); + break; case 'startsWith': case 'endsWith': case 'gt': diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php index b7855c00f10..2f8727e7b55 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php @@ -48,13 +48,27 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter ); } - $expression = $queryBuilder->expr()->$filterOperator( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ); - $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); - - $queryBuilder->setParametersPairs($parameters, $filterParameters); + switch ($filterOperator) { + case 'empty': + $queryBuilder->addSelect($tableAlias.'.lead_id'); + $expression = $queryBuilder->expr()->isNull( + $tableAlias.'.lead_id'); + $queryBuilder->andWhere($expression); + break; + case 'notEmpty': + $queryBuilder->addSelect($tableAlias.'.lead_id'); + $expression = $queryBuilder->expr()->isNull( + $tableAlias.'.lead_id'); + $queryBuilder->andWhere($expression); + break; + default: + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + $queryBuilder->setParametersPairs($parameters, $filterParameters); + } return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index 3a27cd6e023..eabfcef6678 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Services; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder; @@ -95,6 +96,12 @@ public function __construct() 'field' => 'device_model', ]; + $this->translations['stage'] = [ + 'type' => BaseFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'leads', + 'field' => 'stage_id', + ]; + parent::__construct($this->translations); } } From ecaca745e4a61ddbad1df7cb00a5ec1fa0959bc6 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 16:15:19 +0100 Subject: [PATCH 045/778] add sessions query builder, add multiple filter fields translation --- app/bundles/LeadBundle/Config/config.php | 4 + .../LeadBundle/Entity/LeadListRepository.php | 2 - app/bundles/LeadBundle/Model/ListModel.php | 2 +- .../SessionsFilterQueryBuilder.php | 107 ++++++++++++++++++ .../LeadBundle/Segment/LeadSegmentService.php | 3 +- .../Services/LeadSegmentFilterDescriptor.php | 56 +++++++++ 6 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index bd65e9d0b0c..c6fd70b0324 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -734,6 +734,10 @@ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder::class, 'arguments' => [], ], + 'mautic.lead.query.builder.special.sessions' => [ + 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\SessionsFilterQueryBuilder::class, + 'arguments' => [], + ], ], 'helpers' => [ 'mautic.helper.template.avatar' => [ diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 83aa8eb4893..ae3ceb6ed4e 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -507,7 +507,6 @@ public function getLeadsByList($lists, $args = []) $q->resetQueryPart('groupBy'); } dump($q->getSQL()); - dump($q->getParameters()); $start = microtime(true); $results = $q->execute()->fetchAll(); @@ -660,7 +659,6 @@ protected function generateSegmentExpression(array $filters, array &$parameters, */ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNot = false, $leadId = null, $object = 'lead', $listId = null) { - dump($filters); if (!count($filters)) { return $q->expr()->andX(); } diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 1fa824bad8c..0748c6cb3e9 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -810,7 +810,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa $versionEnd = microtime(true) - $versionStart; dump('Total query assembly took:'.$versionEnd.'ms'); - dump($newLeadsCount); + dump(array_shift($newLeadsCount)); exit; // Ensure the same list is used each batch $batchLimiters['maxId'] = (int) $newLeadsCount[$id]['maxId']; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php new file mode 100644 index 00000000000..c914252a268 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php @@ -0,0 +1,107 @@ +getOperator(); + + $filterParameters = $filter->getParameterValue(); + + if (is_array($filterParameters)) { + $parameters = []; + foreach ($filterParameters as $filterParameter) { + $parameters[] = $this->generateRandomParameterName(); + } + } else { + $parameters = $this->generateRandomParameterName(); + } + + $filterParametersHolder = $filter->getParameterHolder($parameters); + + $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + + $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + $tableAlias.'.lead_id = l.id' + ); + } + + $expression = $queryBuilder->expr()->$filterOperator( + 'count('.$tableAlias.'.id)', + $filterParametersHolder + ); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + $queryBuilder->setParametersPairs($parameters, $filterParameters); + + $queryBuilder->andHaving($expression); + + return $queryBuilder; + } +} + +//$operand = 'EXISTS'; +//$table = 'page_hits'; +//$select = 'COUNT(id)'; +//$subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) +// ->from(MAUTIC_TABLE_PREFIX.$table, $alias); +// +//$alias2 = $this->generateRandomParameterName(); +//$subqb2 = $this->entityManager->getConnection()->createQueryBuilder()->select($alias2.'.id') +// ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); +// +//$subqb2->where($q->expr()->andX($q->expr()->eq($alias2.'.lead_id', 'l.id'), $q->expr() +// ->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), $q->expr() +// ->lt($alias2.'.date_hit', $alias.'.date_hit'))); +// +//$parameters[$parameter] = $leadSegmentFilter->getFilter(); +// +//$subqb->where($q->expr()->andX($q->expr()->eq($alias.'.lead_id', 'l.id'), $q->expr() +// ->isNull($alias.'.email_id'), $q->expr() +// ->isNull($alias.'.redirect_id'), sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()))); +// +//$opr = ''; +//switch ($func) { +// case 'eq': +// $opr = '='; +// break; +// case 'gt': +// $opr = '>'; +// break; +// case 'gte': +// $opr = '>='; +// break; +// case 'lt': +// $opr = '<'; +// break; +// case 'lte': +// $opr = '<='; +// break; +//} +//if ($opr) { +// $parameters[$parameter] = $leadSegmentFilter->getFilter(); +// $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); +//} +//$groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); +//break; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 0876618abc9..9fa91aaa762 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -66,7 +66,6 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); $qb = $this->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); -// $qb->andWhere('l.sssss=1'); dump($qb->getQueryParts()); $sql = $qb->getSQL(); @@ -75,7 +74,7 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) } echo '
'; - echo $sql; + dump($sql); try { $start = microtime(true); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index eabfcef6678..4eba405f80f 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -16,6 +16,7 @@ use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder; use Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder; +use Mautic\LeadBundle\Segment\FilterQueryBuilder\SessionsFilterQueryBuilder; class LeadSegmentFilterDescriptor extends \ArrayIterator { @@ -102,6 +103,61 @@ public function __construct() 'field' => 'stage_id', ]; + $this->translations['notification'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'push_ids', + 'field' => 'id', + ]; + + $this->translations['page_id'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'page_id', + ]; + + $this->translations['email_id'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'email_id', + ]; + + $this->translations['redirect_id'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'redirect_id', + ]; + + $this->translations['source'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'source', + ]; + + $this->translations['hit_url'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'field' => 'url', + ]; + + $this->translations['referer'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + ]; + + $this->translations['source_id'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + ]; + + $this->translations['url_title'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + ]; + + $this->translations['sessions'] = [ + 'type' => SessionsFilterQueryBuilder::getServiceId(), + ]; + parent::__construct($this->translations); } } From 109305e91e3c64ca548612a78e0ed3d48168dec4 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 16 Jan 2018 16:20:09 +0100 Subject: [PATCH 046/778] add missing reference to unfinished leadlist builder --- app/bundles/LeadBundle/Config/config.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index c6fd70b0324..201b8b25e9c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -738,6 +738,10 @@ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\SessionsFilterQueryBuilder::class, 'arguments' => [], ], + 'mautic.lead.query.builder.special.leadlist' => [ + 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder::class, + 'arguments' => [], + ], ], 'helpers' => [ 'mautic.helper.template.avatar' => [ From b840997e38e753ab67d77e5a6c548cd23f8b2ce7 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 17 Jan 2018 11:41:27 +0100 Subject: [PATCH 047/778] cleanup, fix of foreign func, count wrapper format query to ms add foreign func QB body add function to wrap querybuilder's query into count for leadcount queries document new query builder methods\ remove old draft of processing its output move method for leads restriction from trait to SQB --- .../LeadBundle/Entity/LeadListRepository.php | 2 +- .../Entity/LeadListSegmentRepository.php | 2 +- app/bundles/LeadBundle/Model/ListModel.php | 2 +- .../ForeignFuncFilterQueryBuilder.php | 160 +++- .../LeadBundle/Segment/LeadSegmentService.php | 29 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 20 +- .../LeadSegmentFilterQueryBuilderTrait.php | 31 - .../Services/LeadSegmentQueryBuilder.php | 842 +----------------- 8 files changed, 219 insertions(+), 869 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index ae3ceb6ed4e..4718b52ad05 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -511,7 +511,7 @@ public function getLeadsByList($lists, $args = []) $start = microtime(true); $results = $q->execute()->fetchAll(); $end = microtime(true) - $start; - dump('Query took '.$end.'ms'); + dump('Query took '.(1000 * $end).'ms'); foreach ($results as $r) { if ($countOnly) { diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index f46ddae4e1d..3cb9195950d 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -129,7 +129,7 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte $start = microtime(true); $results = $q->execute()->fetchAll(); $end = microtime(true) - $start; - dump('Query took '.$end.'ms'); + dump('Query took '.(1000 * $end).'ms'); $leads = []; foreach ($results as $r) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 0748c6cb3e9..d6e24660e3f 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -808,7 +808,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa ] ); $versionEnd = microtime(true) - $versionStart; - dump('Total query assembly took:'.$versionEnd.'ms'); + dump('Total query assembly took:'.(1000 * $versionEnd).'ms'); dump(array_shift($newLeadsCount)); exit; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php index c4382090c52..37f9affb8ff 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php @@ -8,6 +8,164 @@ namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; -class ForeignFuncFilterQueryBuilder extends BaseFilterQueryBuilder +use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\Query\QueryBuilder; +use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; + +class ForeignFuncFilterQueryBuilder implements FilterQueryBuilderInterface { + use LeadSegmentFilterQueryBuilderTrait; + + public static function getServiceId() + { + return 'mautic.lead.query.builder.foreign.func'; + } + + public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + { + $filterOperator = $filter->getOperator(); + $filterGlue = $filter->getGlue(); + $filterAggr = $filter->getAggregateFunction(); + + // @debug we do not need this, it's just to verify we reference an existing database column + try { + $filter->getColumn(); + } catch (\Exception $e) { + dump(' * ERROR * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); + + return $queryBuilder; + } + + $filterParameters = $filter->getParameterValue(); + + if (is_array($filterParameters)) { + $parameters = []; + foreach ($filterParameters as $filterParameter) { + $parameters[] = $this->generateRandomParameterName(); + } + } else { + $parameters = $this->generateRandomParameterName(); + } + + $filterParametersHolder = $filter->getParameterHolder($parameters); + + $filterGlueFunc = $filterGlue.'Where'; + + $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + + // for aggregate function we need to create new alias and not reuse the old one + if ($filterAggr) { + $tableAlias = false; + } + + // dump($filter->getTable()); if ($filter->getTable()=='companies') { + // dump('companies'); + // } + + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + + switch ($filterOperator) { + case 'notLike': + case 'notIn': + case 'empty': + case 'startsWith': + case 'gt': + case 'eq': + case 'neq': + case 'gte': + case 'like': + case 'lt': + case 'lte': + case 'in': + //@todo this logic needs to + if ($filterAggr) { + $queryBuilder->innerJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + ); + } else { + if ($filter->getTable() == 'companies') { + $relTable = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); + $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + } else { + $queryBuilder->leftJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + ); + } + } + break; + default: + //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + dump('Dunno how to handle operator "'.$filterOperator.'"'); + } + } + + switch ($filterOperator) { + case 'empty': + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) + ); + $queryBuilder->setParameter($emptyParameter, ''); + break; + case 'notEmpty': + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) + ); + $queryBuilder->setParameter($emptyParameter, ''); + break; + case 'startsWith': + case 'endsWith': + case 'gt': + case 'eq': + case 'neq': + case 'gte': + case 'like': + case 'notLike': + case 'lt': + case 'lte': + case 'notIn': + case 'in': + if ($filterAggr) { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), + $filterParametersHolder + ); + } else { + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + } + break; + default: + dump(' * IGNORED * - Dunno how to handle operator "'.$filterOperator.'"'); + //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + $expression = '1=1'; + } + + if ($queryBuilder->isJoinTable($filter->getTable())) { + if ($filterAggr) { + $queryBuilder->andHaving($expression); + } else { + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + } + } else { + $queryBuilder->$filterGlueFunc($expression); + } + + $queryBuilder->addGroupBy('l.id'); + + $queryBuilder->setParametersPairs($parameters, $filterParameters); + + return $queryBuilder; + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 9fa91aaa762..2292d8c296f 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -63,12 +63,12 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) $versionStart = microtime(true); /** @var QueryBuilder $qb */ - $qb = $this->queryBuilder->getLeadsQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); + $qb = $this->queryBuilder->getLeadsSegmentQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); + $qb = $this->queryBuilder->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); + $qb = $this->queryBuilder->wrapInCount($qb); - $qb = $this->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); - dump($qb->getQueryParts()); + // Debug output $sql = $qb->getSQL(); - foreach ($qb->getParameters() as $k=>$v) { $sql = str_replace(":$k", "'$v'", $sql); } @@ -82,26 +82,19 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) $results = $stmt->fetchAll(); $end = microtime(true) - $start; - dump('Query took '.$end.'ms'); + dump('Query took '.(1000 * $end).'ms'); + + $start = microtime(true); + + $result = $qb->execute()->fetch(); $versionEnd = microtime(true) - $versionStart; - dump('Total query assembly took:'.$versionEnd.'ms'); + dump('Total query assembly took:'.(1000 * $versionEnd).'ms'); - dump($results); - foreach ($results as $result) { - dump($result); - } + dump($result); } catch (\Exception $e) { dump('Query exception: '.$e->getMessage()); } - echo '
'; - - echo "
Petr's version result:"; - $versionStart = microtime(true); - - $result = $this->leadListSegmentRepository->getNewLeadsByListCount($entity->getId(), $segmentFilters, $batchLimiters); - $versionEnd = microtime(true) - $versionStart; - dump('Total query assembly took:'.$versionEnd.'ms'); return $result; } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 513d5b3a7fc..aebae7faf8b 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1396,8 +1396,8 @@ public function addJoinCondition($alias, $expr) { $result = $parts = $this->getQueryPart('join'); - foreach ($parts as $tbl=>$joins) { - foreach ($joins as $key=>$join) { + foreach ($parts as $tbl => $joins) { + foreach ($joins as $key => $join) { if ($join['joinAlias'] == $alias) { $result[$tbl][$key]['joinCondition'] = $join['joinCondition'].' and '.$expr; } @@ -1449,6 +1449,12 @@ public function setParametersPairs($parameters, $filterParameters) return $this; } + /** + * @param string $table + * @param null $joinType allowed values: inner, left, right + * + * @return array|bool|string + */ public function getTableAlias($table, $joinType = null) { if (is_null($joinType)) { @@ -1474,6 +1480,11 @@ public function getTableAlias($table, $joinType = null) return !count($result) ? false : count($result) == 1 ? array_shift($result) : $result; } + /** + * @param $tableName + * + * @return bool + */ public function getTableJoins($tableName) { foreach ($this->getQueryParts()['join'] as $join) { @@ -1487,6 +1498,11 @@ public function getTableJoins($tableName) return false; } + /** + * Return aliases of all currently registered tables. + * + * @return array + */ public function getTableAliases() { $queryParts = $this->getQueryParts(); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index 7d269dc7fdc..81f26742550 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -8,8 +8,6 @@ namespace Mautic\LeadBundle\Services; -use Mautic\LeadBundle\Segment\Query\QueryBuilder; - trait LeadSegmentFilterQueryBuilderTrait { // @todo make static to asure single instance @@ -34,33 +32,4 @@ protected function generateRandomParameterName() return $this->generateRandomParameterName(); } - - public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) - { - $queryBuilder->select('max(l.id) maxId, count(l.id) as leadCount'); - - $parts = $queryBuilder->getQueryParts(); - $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); - - $tableAlias = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id'); - $queryBuilder->addSelect($tableAlias.'.lead_id'); - - $expression = $queryBuilder->expr()->andX( - $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $leadListId), - $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$whatever['dateTime']."'") - ); - - $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'.lead_id'); - - $queryBuilder->addJoinCondition($tableAlias, $expression); - - if ($setHaving) { - $queryBuilder->andHaving($restrictionExpression); - } else { - $queryBuilder->andWhere($restrictionExpression); - } - - return $queryBuilder; - } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index c56344c1d8a..865fd18eacb 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -12,9 +12,6 @@ namespace Mautic\LeadBundle\Services; use Doctrine\ORM\EntityManager; -use Mautic\CoreBundle\Helper\InputHelper; -use Mautic\LeadBundle\Event\LeadListFilteringEvent; -use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilters; use Mautic\LeadBundle\Segment\Query\QueryBuilder; @@ -22,16 +19,12 @@ class LeadSegmentQueryBuilder { - use LeadSegmentFilterQueryBuilderTrait; - /** @var EntityManager */ private $entityManager; /** @var RandomParameterName */ private $randomParameterName; - private $tableAliases = []; - /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ private $schema; @@ -42,10 +35,10 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r $this->schema = $this->entityManager->getConnection()->getSchemaManager(); } - public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) + public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { /** @var QueryBuilder $queryBuilder */ - $queryBuilder = new \Mautic\LeadBundle\Segment\Query\QueryBuilder($this->entityManager->getConnection()); + $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); $queryBuilder->select('*')->from('leads', 'l'); @@ -55,722 +48,46 @@ public function getLeadsQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters } return $queryBuilder; - echo 'SQL parameters:'; - dump($q->getParameters()); - - // Leads that do not have any record in the lead_lists_leads table for this lead list - // For non null fields - it's apparently better to use left join over not exists due to not using nullable - // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ - $listOnExpr = $q->expr()->andX($q->expr()->eq('ll.leadlist_id', $id), $q->expr()->eq('ll.lead_id', 'l.id')); - - if (!empty($batchLimiters['dateTime'])) { - // Only leads in the list at the time of count - $listOnExpr->add($q->expr()->lte('ll.date_added', $q->expr()->literal($batchLimiters['dateTime']))); - } - - $q->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll', $listOnExpr); - - $expr->add($q->expr()->isNull('ll.lead_id')); - - if ($batchExpr->count()) { - $expr->add($batchExpr); - } - - if ($expr->count()) { - $q->andWhere($expr); - } - - if (!empty($limit)) { - $q->setFirstResult($start)->setMaxResults($limit); - } - - // remove any possible group by - $q->resetQueryPart('groupBy'); - - var_dump($q->getSQL()); - echo 'SQL parameters:'; - dump($q->getParameters()); - - $results = $q->execute()->fetchAll(); - - $leads = []; - foreach ($results as $r) { - $leads = ['count' => $r['lead_count'], 'maxId' => $r['max_id']]; - if ($withMinId) { - $leads['minId'] = $r['min_id']; - } - } - - return $leads; } - private function getQueryPart(LeadSegmentFilter $filter, QueryBuilder $qb) + public function wrapInCount(QueryBuilder $qb) { - $qb = $filter->createQuery($qb); - - return $qb; - - // //the next one will determine the group - // if ($leadSegmentFilter->getGlue() === 'or') { - // // Create a new group of andX expressions - // if ($groupExpr->count()) { - // $groups[] = $groupExpr; - // $groupExpr = $q->expr() - // ->andX() - // ; - // } - // } - - $parameterName = $this->generateRandomParameterName(); - - //@todo what is this? - // $ignoreAutoFilter = false; - // - // $func = $filter->getFunc(); - // - // // Generate a unique alias - // $alias = $this->generateRandomParameterName(); - // - // var_dump($func . ":" . $leadSegmentFilter->getField()); - // var_dump($exprParameter); - - switch ($leadSegmentFilter->getField()) { - case 'hit_url': - case 'referer': - case 'source': - case 'source_id': - case 'url_title': - $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp', 'startsWith', 'endsWith', 'contains']) ? 'EXISTS' : 'NOT EXISTS'; - - $ignoreAutoFilter = true; - $column = $leadSegmentFilter->getField(); - - if ($column === 'hit_url') { - $column = 'url'; - } - - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') - ->from(MAUTIC_TABLE_PREFIX.'page_hits', $alias); - - switch ($func) { - case 'eq': - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where($q->expr()->andX($q->expr() - ->eq($alias.'.'.$column, $exprParameter), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - break; - case 'regexp': - case 'notRegexp': - $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); - $not = ($func === 'notRegexp') ? ' NOT' : ''; - $subqb->where($q->expr()->andX($q->expr() - ->eq($alias.'.lead_id', 'l.id'), $alias.'.'.$column.$not.' REGEXP '.$exprParameter)); - break; - case 'like': - case 'notLike': - case 'startsWith': - case 'endsWith': - case 'contains': - switch ($func) { - case 'like': - case 'notLike': - case 'contains': - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; - break; - case 'startsWith': - $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; - break; - case 'endsWith': - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); - break; - } - - $subqb->where($q->expr()->andX($q->expr() - ->like($alias.'.'.$column, $exprParameter), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - break; - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'device_model': - $ignoreAutoFilter = true; - $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp']) ? 'EXISTS' : 'NOT EXISTS'; - - $column = $leadSegmentFilter->getField(); - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') - ->from(MAUTIC_TABLE_PREFIX.'lead_devices', $alias); - switch ($func) { - case 'eq': - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where($q->expr()->andX($q->expr() - ->eq($alias.'.'.$column, $exprParameter), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - break; - case 'like': - case '!like': - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; - $subqb->where($q->expr()->andX($q->expr() - ->like($alias.'.'.$column, $exprParameter), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - break; - case 'regexp': - case 'notRegexp': - $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); - $not = ($func === 'notRegexp') ? ' NOT' : ''; - $subqb->where($q->expr()->andX($q->expr() - ->eq($alias.'.lead_id', 'l.id'), $alias.'.'.$column.$not.' REGEXP '.$exprParameter)); - break; - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - - break; - case 'hit_url_date': - case 'lead_email_read_date': - $operand = (in_array($func, ['eq', 'gt', 'lt', 'gte', 'lte', 'between'])) ? 'EXISTS' : 'NOT EXISTS'; - $table = 'page_hits'; - $column = 'date_hit'; - - if ($leadSegmentFilter->getField() === 'lead_email_read_date') { - $column = 'date_read'; - $table = 'email_stats'; - } - - if ($filterField == 'lead_email_read_date') { - var_dump($func); - } - - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('id') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - switch ($func) { - case 'eq': - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subqb->where($q->expr()->andX($q->expr() - ->eq($alias.'.'.$column, $exprParameter), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - break; - case 'between': - case 'notBetween': - // Filter should be saved with double || to separate options - $parameter2 = $this->generateRandomParameterName(); - $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; - $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; - $exprParameter2 = ":$parameter2"; - $ignoreAutoFilter = true; - $field = $column; - - if ($func === 'between') { - $subqb->where($q->expr()->andX($q->expr() - ->gte($alias.'.'.$field, $exprParameter), $q->expr() - ->lt($alias.'.'.$field, $exprParameter2), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - } else { - $subqb->where($q->expr()->andX($q->expr() - ->lt($alias.'.'.$field, $exprParameter), $q->expr() - ->gte($alias.'.'.$field, $exprParameter2), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - } - break; - default: - $parameter2 = $this->generateRandomParameterName(); - - if ($filterField == 'lead_email_read_date') { - var_dump($exprParameter); - } - $parameters[$parameter2] = $leadSegmentFilter->getFilter(); - - $subqb->where($q->expr()->andX($q->expr() - ->$func($alias.'.'.$column, $parameter2), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - break; - } - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'page_id': - case 'email_id': - case 'redirect_id': - case 'notification': - $operand = ($func === 'eq') ? 'EXISTS' : 'NOT EXISTS'; - $column = $leadSegmentFilter->getField(); - $table = 'page_hits'; - $select = 'id'; - - if ($leadSegmentFilter->getField() === 'notification') { - $table = 'push_ids'; - $column = 'id'; - } - - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - if ($leadSegmentFilter->getFilter() == 1) { - $subqb->where($q->expr()->andX($q->expr()->isNotNull($alias.'.'.$column), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - } else { - $subqb->where($q->expr()->andX($q->expr()->isNull($alias.'.'.$column), $q->expr() - ->eq($alias.'.lead_id', 'l.id'))); - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'sessions': - $operand = 'EXISTS'; - $table = 'page_hits'; - $select = 'COUNT(id)'; - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - $alias2 = $this->generateRandomParameterName(); - $subqb2 = $this->entityManager->getConnection()->createQueryBuilder()->select($alias2.'.id') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); - - $subqb2->where($q->expr()->andX($q->expr()->eq($alias2.'.lead_id', 'l.id'), $q->expr() - ->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), $q->expr() - ->lt($alias2.'.date_hit', $alias.'.date_hit'))); - - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subqb->where($q->expr()->andX($q->expr()->eq($alias.'.lead_id', 'l.id'), $q->expr() - ->isNull($alias.'.email_id'), $q->expr() - ->isNull($alias.'.redirect_id'), sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()))); - - $opr = ''; - switch ($func) { - case 'eq': - $opr = '='; - break; - case 'gt': - $opr = '>'; - break; - case 'gte': - $opr = '>='; - break; - case 'lt': - $opr = '<'; - break; - case 'lte': - $opr = '<='; - break; - } - if ($opr) { - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); - } - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'hit_url_count': - case 'lead_email_read_count': - $operand = 'EXISTS'; - $table = 'page_hits'; - $select = 'COUNT(id)'; - if ($leadSegmentFilter->getField() === 'lead_email_read_count') { - $table = 'email_stats'; - $select = 'COALESCE(SUM(open_count),0)'; - } - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where($q->expr()->andX($q->expr()->eq($alias.'.lead_id', 'l.id'))); - - $opr = ''; - switch ($func) { - case 'eq': - $opr = '='; - break; - case 'gt': - $opr = '>'; - break; - case 'gte': - $opr = '>='; - break; - case 'lt': - $opr = '<'; - break; - case 'lte': - $opr = '<='; - break; - } - - if ($opr) { - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - - case 'dnc_bounced': - case 'dnc_unsubscribed': - case 'dnc_bounced_sms': - case 'dnc_unsubscribed_sms': - // Special handling of do not contact - $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; - - $parts = explode('_', $leadSegmentFilter->getField()); - $channel = 'email'; - - if (count($parts) === 3) { - $channel = $parts[2]; - } - - $channelParameter = $this->generateRandomParameterName(); - $subqb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') - ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) - ->where($q->expr()->andX($q->expr() - ->eq($alias.'.reason', $exprParameter), $q->expr() - ->eq($alias.'.lead_id', 'l.id'), $q->expr() - ->eq($alias.'.channel', ":$channelParameter"))); - - $groupExpr->add(sprintf('%s (%s)', $func, $subqb->getSQL())); - - // Filter will always be true and differentiated via EXISTS/NOT EXISTS - $leadSegmentFilter->setFilter(true); - - $ignoreAutoFilter = true; - - $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; - $parameters[$channelParameter] = $channel; - - break; - - case 'leadlist': - $table = 'lead_lists_leads'; - $column = 'leadlist_id'; - $falseParameter = $this->generateRandomParameterName(); - $parameters[$falseParameter] = false; - $trueParameter = $this->generateRandomParameterName(); - $parameters[$trueParameter] = true; - $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; - $ignoreAutoFilter = true; - - if ($filterListIds = (array) $leadSegmentFilter->getFilter()) { - $listQb = $this->entityManager->getConnection()->createQueryBuilder()->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'l'); - $listQb->where($listQb->expr()->in('l.id', $filterListIds)); - $filterLists = $listQb->execute()->fetchAll(); - $not = 'NOT EXISTS' === $func; - - // Each segment's filters must be appended as ORs so that each list is evaluated individually - $existsExpr = $not ? $listQb->expr()->andX() : $listQb->expr()->orX(); - - foreach ($filterLists as $list) { - $alias = $this->generateRandomParameterName(); - $id = (int) $list['id']; - if ($id === (int) $listId) { - // Ignore as somehow self is included in the list - continue; - } - - $listFilters = unserialize($list['filters']); - if (empty($listFilters)) { - // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list - $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $id, $parameters, [$alias.'.manually_removed' => $falseParameter]); - } else { - // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet - // but also leverage the current membership to take into account those manually added or removed from the segment - - // Build a "live" query based on current filters to catch those that have not been processed yet - $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); - $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); - - // Left join membership to account for manually added and removed - $membershipAlias = $this->generateRandomParameterName(); - $subQb->leftJoin($alias, MAUTIC_TABLE_PREFIX.$table, $membershipAlias, "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id") - ->where($subQb->expr()->orX($filterExpr, $subQb->expr() - ->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ))->andWhere($subQb->expr()->eq("$alias.id", 'l.id'), $subQb->expr() - ->orX($subQb->expr() - ->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr() - ->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - )); - } - - $existsExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); - } - - if ($existsExpr->count()) { - $groupExpr->add($existsExpr); - } - } - - break; - case 'tags': - case 'globalcategory': - case 'lead_email_received': - case 'lead_email_sent': - case 'device_type': - case 'device_brand': - case 'device_os': - // Special handling of lead lists and tags - $func = in_array($func, ['eq', 'in'], true) ? 'EXISTS' : 'NOT EXISTS'; - - $ignoreAutoFilter = true; + // Add count functions to the query + $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); + $qb->addSelect('l.id as leadIdPrimary'); + $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId')->from('('.$qb->getSQL().')', 'sss'); + $queryBuilder->setParameters($qb->getParameters()); - // Collect these and apply after building the query because we'll want to apply the lead first for each of the subqueries - $subQueryFilters = []; - switch ($leadSegmentFilter->getField()) { - case 'tags': - $table = 'lead_tags_xref'; - $column = 'tag_id'; - break; - case 'globalcategory': - $table = 'lead_categories'; - $column = 'category_id'; - break; - case 'lead_email_received': - $table = 'email_stats'; - $column = 'email_id'; - - $trueParameter = $this->generateRandomParameterName(); - $subQueryFilters[$alias.'.is_read'] = $trueParameter; - $parameters[$trueParameter] = true; - break; - case 'lead_email_sent': - $table = 'email_stats'; - $column = 'email_id'; - break; - case 'device_type': - $table = 'lead_devices'; - $column = 'device'; - break; - case 'device_brand': - $table = 'lead_devices'; - $column = 'device_brand'; - break; - case 'device_os': - $table = 'lead_devices'; - $column = 'device_os_name'; - break; - } - - $subQb = $this->createFilterExpressionSubQuery($table, $alias, $column, $leadSegmentFilter->getFilter(), $parameters, $subQueryFilters); - - $groupExpr->add(sprintf('%s (%s)', $func, $subQb->getSQL())); - break; - case 'stage': - // A note here that SQL EXISTS is being used for the eq and neq cases. - // I think this code might be inefficient since the sub-query is rerun - // for every row in the outer query's table. This might have to be refactored later on - // if performance is desired. - - $subQb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') - ->from(MAUTIC_TABLE_PREFIX.'stages', $alias); - - switch ($func) { - case 'empty': - $groupExpr->add($q->expr()->isNull('l.stage_id')); - break; - case 'notEmpty': - $groupExpr->add($q->expr()->isNotNull('l.stage_id')); - break; - case 'eq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subQb->where($q->expr()->andX($q->expr()->eq($alias.'.id', 'l.stage_id'), $q->expr() - ->eq($alias.'.id', ":$parameter"))); - $groupExpr->add(sprintf('EXISTS (%s)', $subQb->getSQL())); - break; - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subQb->where($q->expr()->andX($q->expr()->eq($alias.'.id', 'l.stage_id'), $q->expr() - ->eq($alias.'.id', ":$parameter"))); - $groupExpr->add(sprintf('NOT EXISTS (%s)', $subQb->getSQL())); - break; - } - - break; - case 'integration_campaigns': - $parameter2 = $this->generateRandomParameterName(); - $operand = in_array($func, ['eq', 'neq']) ? 'EXISTS' : 'NOT EXISTS'; - $ignoreAutoFilter = true; - - $subQb = $this->entityManager->getConnection()->createQueryBuilder()->select('null') - ->from(MAUTIC_TABLE_PREFIX.'integration_entity', $alias); - switch ($func) { - case 'eq': - case 'neq': - if (strpos($leadSegmentFilter->getFilter(), '::') !== false) { - list($integrationName, $campaignId) = explode('::', $leadSegmentFilter->getFilter()); - } else { - // Assuming this is a Salesforce integration for BC with pre 2.11.0 - $integrationName = 'Salesforce'; - $campaignId = $leadSegmentFilter->getFilter(); - } - - $parameters[$parameter] = $campaignId; - $parameters[$parameter2] = $integrationName; - $subQb->where($q->expr()->andX($q->expr() - ->eq($alias.'.integration', ":$parameter2"), $q->expr() - ->eq($alias.'.integration_entity', "'CampaignMember'"), $q->expr() - ->eq($alias.'.integration_entity_id', ":$parameter"), $q->expr() - ->eq($alias.'.internal_entity', "'lead'"), $q->expr() - ->eq($alias.'.internal_entity_id', 'l.id'))); - break; - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subQb->getSQL())); - - break; - default: - if (!$column) { - // Column no longer exists so continue - continue; - } - - switch ($func) { - case 'between': - case 'notBetween': - // Filter should be saved with double || to separate options - $parameter2 = $this->generateRandomParameterName(); - $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; - $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; - $exprParameter2 = ":$parameter2"; - $ignoreAutoFilter = true; - - if ($func === 'between') { - $groupExpr->add($q->expr()->andX($q->expr()->gte($field, $exprParameter), $q->expr() - ->lt($field, $exprParameter2))); - } else { - $groupExpr->add($q->expr()->andX($q->expr()->lt($field, $exprParameter), $q->expr() - ->gte($field, $exprParameter2))); - } - break; - - case 'notEmpty': - $groupExpr->add($q->expr()->andX($q->expr()->isNotNull($field), $q->expr() - ->neq($field, $q->expr() - ->literal('')))); - $ignoreAutoFilter = true; - break; - - case 'empty': - $leadSegmentFilter->setFilter(''); - $groupExpr->add($this->generateFilterExpression($q, $field, 'eq', $exprParameter, true)); - break; - - case 'in': - case 'notIn': - $cleanFilter = []; - foreach ($leadSegmentFilter->getFilter() as $key => $value) { - $cleanFilter[] = $q->expr()->literal(InputHelper::clean($value)); - } - $leadSegmentFilter->setFilter($cleanFilter); - - if ($leadSegmentFilter->getType() === 'multiselect') { - foreach ($leadSegmentFilter->getFilter() as $filter) { - $filter = trim($filter, "'"); - - if (substr($func, 0, 3) === 'not') { - $operator = 'NOT REGEXP'; - } else { - $operator = 'REGEXP'; - } + return $queryBuilder; + } - $groupExpr->add($field." $operator '\\\\|?$filter\\\\|?'"); - } - } else { - $groupExpr->add($this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null)); - } - $ignoreAutoFilter = true; - break; + public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) + { + $queryBuilder->select('max(l.id) maxId, count(distinct l.id) as leadCount'); - case 'neq': - $groupExpr->add($this->generateFilterExpression($q, $field, $func, $exprParameter, null)); - break; + $parts = $queryBuilder->getQueryParts(); + $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); - case 'like': - case 'notLike': - case 'startsWith': - case 'endsWith': - case 'contains': - $ignoreAutoFilter = true; + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id'); + $queryBuilder->addSelect($tableAlias.'.lead_id'); - switch ($func) { - case 'like': - case 'notLike': - $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%'.$leadSegmentFilter->getFilter().'%' : $leadSegmentFilter->getFilter(); - break; - case 'startsWith': - $func = 'like'; - $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; - break; - case 'endsWith': - $func = 'like'; - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); - break; - case 'contains': - $func = 'like'; - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; - break; - } + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $leadListId), + $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$whatever['dateTime']."'") + ); - $groupExpr->add($this->generateFilterExpression($q, $field, $func, $exprParameter, null)); - break; - case 'regexp': - case 'notRegexp': - $ignoreAutoFilter = true; - $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); - $not = ($func === 'notRegexp') ? ' NOT' : ''; - $groupExpr->add(// Escape single quotes while accounting for those that may already be escaped - $field.$not.' REGEXP '.$exprParameter); - break; - default: - $ignoreAutoFilter = true; - $groupExpr->add($q->expr()->$func($field, $exprParameter)); - $parameters[$exprParameter] = $leadSegmentFilter->getFilter(); - } - } + $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'.lead_id'); - if (!$ignoreAutoFilter) { - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - } - - if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { - $event = new LeadListFilteringEvent($leadSegmentFilter->toArray(), null, $alias, $func, $q, $this->entityManager); - $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); - if ($event->isFilteringDone()) { - $groupExpr->add($event->getSubQuery()); - } - } - - // Get the last of the filters - if ($groupExpr->count()) { - $groups[] = $groupExpr; - } - if (count($groups) === 1) { - // Only one andX expression - $expr = $groups[0]; - } elseif (count($groups) > 1) { - // Sets of expressions grouped by OR - $orX = $q->expr()->orX(); - $orX->addMultiple($groups); + $queryBuilder->addJoinCondition($tableAlias, $expression); - // Wrap in a andX for other functions to append - $expr = $q->expr()->andX($orX); + if ($setHaving) { + $queryBuilder->andHaving($restrictionExpression); } else { - $expr = $groupExpr; + $queryBuilder->andWhere($restrictionExpression); } - foreach ($parameters as $k => $v) { - $paramType = null; - - if (is_array($v) && isset($v['type'], $v['value'])) { - $paramType = $v['type']; - $v = $v['value']; - } - $q->setParameter($k, $v, $paramType); - } - - return $expr; + return $queryBuilder; } /** @@ -783,109 +100,6 @@ private function generateRandomParameterName() return $this->randomParameterName->generateRandomParameterName(); } - /** - * @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q - * @param $column - * @param $operator - * @param $parameter - * @param $includeIsNull true/false or null to auto determine based on operator - * - * @return mixed - */ - public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) - { - // in/notIn for dbal will use a raw array - if (!is_array($parameter) && strpos($parameter, ':') !== 0) { - $parameter = ":$parameter"; - } - - if (null === $includeIsNull) { - // Auto determine based on negate operators - $includeIsNull = (in_array($operator, ['neq', 'notLike', 'notIn'])); - } - - if ($includeIsNull) { - $expr = $q->expr()->orX($q->expr()->$operator($column, $parameter), $q->expr()->isNull($column)); - } else { - $expr = $q->expr()->$operator($column, $parameter); - } - - return $expr; - } - - /** - * @param $table - * @param $alias - * @param $column - * @param $value - * @param array $parameters - * @param null $leadId - * @param array $subQueryFilters - * - * @return QueryBuilder - */ - protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) - { - $subQb = $this->entityManager->getConnection()->createQueryBuilder(); - $subExpr = $subQb->expr()->andX(); - - if ('leads' !== $table) { - $subExpr->add($subQb->expr()->eq($alias.'.lead_id', 'l.id')); - } - - foreach ($subQueryFilters as $subColumn => $subParameter) { - $subExpr->add($subQb->expr()->eq($subColumn, ":$subParameter")); - } - - if (null !== $value && !empty($column)) { - $subFilterParamter = $this->generateRandomParameterName(); - $subFunc = 'eq'; - if (is_array($value)) { - $subFunc = 'in'; - $subExpr->add($subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter")); - $parameters[$subFilterParamter] = ['value' => $value, 'type' => \Doctrine\DBAL\Connection::PARAM_STR_ARRAY]; - } else { - $parameters[$subFilterParamter] = $value; - } - - $subExpr->add($subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter")); - } - - $subQb->select('null')->from(MAUTIC_TABLE_PREFIX.$table, $alias)->where($subExpr); - - return $subQb; - } - - /** - * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should - * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. - * - * @param QueryBuilder $q - * @param LeadSegmentFilters $leadSegmentFilters - */ - private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) - { - $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; - // Join company tables for query optimization - $q->$joinType('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'l.id = cl.lead_id') - ->$joinType('cl', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'cl.company_id = comp.id'); - - // Return only unique contacts - $q->groupBy('l.id'); - } - - private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) - { - var_dump(debug_backtrace()[1]['function']); - $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); - - if ($leadSegmentFilters->isHasCompanyFilter()) { - $this->applyCompanyFieldFilters($q, $leadSegmentFilters); - } - - return $expr; - } - /** * @return LeadSegmentFilterDescriptor */ From aa0674e82f8903b62afcba0224cc76ebb76cee3d Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 17 Jan 2018 13:33:51 +0100 Subject: [PATCH 048/778] add automated developer testing for segments add command for running 2 query builders and comparing the result implement 2 temporary methods to ListModel simply empty/notEmpty to is(Not)Null only skip query builder for leadlist and dump warning return output consistent to old method in service's new method for count modify SQB to return query without any aggregation and functions --- .../Command/CheckQueryBuildersCommand.php | 121 ++++++++++++++++++ .../LeadBundle/Entity/LeadListRepository.php | 1 + app/bundles/LeadBundle/Model/ListModel.php | 38 ++++++ .../BaseFilterQueryBuilder.php | 13 +- .../LeadListFilterQueryBuilder.php | 3 + .../LeadBundle/Segment/LeadSegmentService.php | 3 + .../Services/LeadSegmentQueryBuilder.php | 2 +- 7 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php new file mode 100644 index 00000000000..5bb0f2d7a49 --- /dev/null +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -0,0 +1,121 @@ +setName('mautic:segments:check-builders') + ->setAliases(['mautic:segments:check-query-builders']) + ->setDescription('Compare output of query builders for given segments') + ->addOption('--segment-id', '-i', InputOption::VALUE_OPTIONAL, 'Set the ID of segment to process') + ; + + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $container = $this->getContainer(); + + /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ + $listModel = $container->get('mautic.lead.model.list'); + + $id = $input->getOption('segment-id'); + $verbose = $input->getOption('verbose'); + + $verbose = false; + + if ($id) { + $list = $listModel->getEntity($id); + $this->runSegment($output, $verbose, $list, $listModel); + } else { + $lists = $listModel->getEntities( + [ + 'iterator_mode' => true, + ] + ); + + while (($l = $lists->next()) !== false) { + // Get first item; using reset as the key will be the ID and not 0 + $l = reset($l); + + $this->runSegment($output, $verbose, $l, $listModel); + } + + unset($l); + + unset($lists); + } + + return 0; + } + + private function runSegment($output, $verbose, $l, $listModel) + { + $output->writeln('Running segment '.$l->getId().'...'); + $output->writeln(''); + + if (!$verbose) { + ob_start(); + } + + $output->writeln('old:'); + $timer1 = microtime(true); + $processed = $listModel->getVersionOld($l); + $timer1 = round((microtime(true) - $timer1) * 1000); + + $output->writeln('new:'); + $timer2 = microtime(true); + $processed2 = $listModel->getVersionNew($l); + $timer2 = round((microtime(true) - $timer2) * 1000); + + $output->writeln(''); + + if ($processed['count'] != $processed2['count'] or $processed['maxId'] != $processed2['maxId']) { + $output->write(''); + } else { + $output->write(''); + } + + $output->writeln( + sprintf('old: c: %d, m: %d, time: %dms <--> new: c: %d, m: %s, time: %dms', + $processed['count'], + $processed['maxId'], + $timer1, + $processed2['count'], + $processed2['maxId'], + $timer2 + ) + ); + + if ($processed['count'] != $processed2['count'] or $processed['maxId'] != $processed2['maxId']) { + $output->write(''); + } else { + $output->write(''); + } + + $output->writeln(''); + $output->writeln('-------------------------------------------------------------------------'); + + if (!$verbose) { + ob_clean(); + } + } +} diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 4718b52ad05..168f01b751a 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -506,6 +506,7 @@ public function getLeadsByList($lists, $args = []) // remove any possible group by $q->resetQueryPart('groupBy'); } + dump($q->getSQL()); $start = microtime(true); diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index d6e24660e3f..e760432e726 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -759,6 +759,44 @@ public function getGlobalLists() return $lists; } + public function getVersionNew(LeadList $entity) + { + $id = $entity->getId(); + $list = ['id' => $id, 'filters' => $entity->getFilters()]; + $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); + + $batchLimiters = [ + 'dateTime' => $dtHelper->toUtcString(), + ]; + + return $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); + } + + public function getVersionOld(LeadList $entity) + { + $id = $entity->getId(); + $list = ['id' => $id, 'filters' => $entity->getFilters()]; + $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); + + $batchLimiters = [ + 'dateTime' => $dtHelper->toUtcString(), + ]; + + $newLeadsCount = $this->getLeadsByList( + $list, + true, + [ + 'countOnly' => true, + 'newOnly' => true, + 'batchLimiters' => $batchLimiters, + ] + ); + + $return = array_shift($newLeadsCount); + + return $return; + } + /** * Rebuild lead lists. * diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index c62f177797d..22843f14348 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -109,18 +109,11 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter switch ($filterOperator) { case 'empty': - $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) - ); - $queryBuilder->setParameter($emptyParameter, ''); + $expression = $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()); + break; case 'notEmpty': - $expression = $queryBuilder->expr()->andX( - $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), ':'.$emptyParameter = $this->generateRandomParameterName()) - ); - $queryBuilder->setParameter($emptyParameter, ''); + $expression = $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()); break; case 'startsWith': case 'endsWith': diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php index e2d3b9742ab..7c60a60bc49 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php @@ -24,6 +24,9 @@ public static function getServiceId() public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { + dump('This is definitely an @todo!!!!'); + + return $queryBuilder; dump('lead list'); die(); $parts = explode('_', $filter->getCrate('field')); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 2292d8c296f..d32f60765d2 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -62,6 +62,9 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) echo '
New version result:'; $versionStart = microtime(true); + if (!count($segmentFilters)) { + return [0]; + } /** @var QueryBuilder $qb */ $qb = $this->queryBuilder->getLeadsSegmentQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); $qb = $this->queryBuilder->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 865fd18eacb..39fc5d88b19 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -63,7 +63,7 @@ public function wrapInCount(QueryBuilder $qb) public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) { - $queryBuilder->select('max(l.id) maxId, count(distinct l.id) as leadCount'); + $queryBuilder->select('l.id'); $parts = $queryBuilder->getQueryParts(); $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); From b866f0cdbee907a8e2217a46bb124e2b64089645 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 17 Jan 2018 15:20:21 +0100 Subject: [PATCH 049/778] add regexp processing --- app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php | 4 ++-- .../Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php | 4 ++++ .../FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php | 5 ----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 5bb0f2d7a49..13d5447def5 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -79,12 +79,12 @@ private function runSegment($output, $verbose, $l, $listModel) $output->writeln('old:'); $timer1 = microtime(true); $processed = $listModel->getVersionOld($l); - $timer1 = round((microtime(true) - $timer1) * 1000); + $timer1 = round((microtime(true) - $timer1) * 1000, 3); $output->writeln('new:'); $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); - $timer2 = round((microtime(true) - $timer2) * 1000); + $timer2 = round((microtime(true) - $timer2) * 1000, 3); $output->writeln(''); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 22843f14348..4d14953721e 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -78,6 +78,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'lt': case 'lte': case 'in': + case 'regexp': + case 'notRegexp': //@todo this logic needs to if ($filterAggr) { $queryBuilder->leftJoin( @@ -127,6 +129,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'lte': case 'notIn': case 'in': + case 'regexp': + case 'notRegexp': if ($filterAggr) { $expression = $queryBuilder->expr()->$filterOperator( sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php index 37f9affb8ff..178d47a23c5 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php @@ -58,17 +58,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = false; } - // dump($filter->getTable()); if ($filter->getTable()=='companies') { - // dump('companies'); - // } - if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); switch ($filterOperator) { case 'notLike': case 'notIn': - case 'empty': case 'startsWith': case 'gt': case 'eq': From e177d16b546425e4e4f11cc51531fded8cf0b7a3 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 17 Jan 2018 15:29:21 +0100 Subject: [PATCH 050/778] add regexp processing, fix --- .../Query/Expression/CompositeExpression.php | 2 +- .../Query/Expression/ExpressionBuilder.php | 55 ++++++++++++++++--- .../LeadBundle/Segment/Query/QueryBuilder.php | 16 +++++- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php b/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php index 334bfb9ee24..a717322e203 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php @@ -17,7 +17,7 @@ * . */ -namespace Doctrine\DBAL\Query\Expression; +namespace Mautic\LeadBundle\Segment\Query\Expression; /** * Composite expression is responsible to build a group of similar expression. diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index 72d5791607c..6bfff5114af 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -17,7 +17,7 @@ * . */ -namespace Doctrine\DBAL\Query\Expression; +namespace Mautic\LeadBundle\Segment\Query\Expression; use Doctrine\DBAL\Connection; @@ -32,12 +32,13 @@ */ class ExpressionBuilder { - const EQ = '='; - const NEQ = '<>'; - const LT = '<'; - const LTE = '<='; - const GT = '>'; - const GTE = '>='; + const EQ = '='; + const NEQ = '<>'; + const LT = '<'; + const LTE = '<='; + const GT = '>'; + const GTE = '>='; + const REGEXP = 'REGEXP'; /** * The DBAL Connection. @@ -108,6 +109,46 @@ public function comparison($x, $operator, $y) return $x.' '.$operator.' '.$y; } + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function regexp($x, $y) + { + return $this->comparison($x, self::REGEXP, $y); + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * + * @param mixed $x the left expression + * @param mixed $y the right expression + * + * @return string + */ + public function notRegexp($x, $y) + { + return $this->comparison($x, self::REGEXP, $y); + } + /** * Creates an equality comparison expression with the given arguments. * diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index aebae7faf8b..68366f78564 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -21,6 +21,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\Expression\CompositeExpression; +use Mautic\LeadBundle\Segment\Query\Expression\ExpressionBuilder; /** * QueryBuilder class is responsible to dynamically create SQL queries. @@ -61,6 +62,11 @@ class QueryBuilder */ private $connection; + /** + * @var ExpressionBuilder + */ + private $_expr; + /** * @var array the array of SQL parts collected */ @@ -156,11 +162,17 @@ public function __construct(Connection $connection) * For more complex expression construction, consider storing the expression * builder object in a local variable. * - * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder + * @return ExpressionBuilder */ public function expr() { - return $this->connection->getExpressionBuilder(); + if (!is_null($this->_expr)) { + return $this->_expr; + } + + $this->_expr = new ExpressionBuilder($this->connection); + + return $this->_expr; } /** From 58aa037480cb03afc4ef5194031bedd3d4c0137e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 18 Jan 2018 10:22:54 +0100 Subject: [PATCH 051/778] Filter cleaning - remove dependency on BaseDecorator - moved to interface --- .../LeadBundle/Segment/Decorator/FilterDecoratorInterface.php | 4 ++++ app/bundles/LeadBundle/Segment/LeadSegmentFilter.php | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index bc3989b92f6..26152f05a23 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -24,4 +24,8 @@ public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate); public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument); public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate); + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate); + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 3e9b5fb0497..0c890b458cc 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -12,7 +12,6 @@ namespace Mautic\LeadBundle\Segment; use Doctrine\ORM\EntityManager; -use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; @@ -29,7 +28,7 @@ class LeadSegmentFilter public $leadSegmentFilterCrate; /** - * @var FilterDecoratorInterface|BaseDecorator + * @var FilterDecoratorInterface */ private $filterDecorator; From 3516f9cfe80ea67200bad1a0a4b2dc072180be6e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 18 Jan 2018 11:37:54 +0100 Subject: [PATCH 052/778] Decorator refactoring - new Custom mapped decorator --- app/bundles/LeadBundle/Config/config.php | 11 ++- .../Segment/Decorator/BaseDecorator.php | 45 ++---------- .../Decorator/CustomMappedDecorator.php | 73 +++++++++++++++++++ .../Segment/LeadSegmentFilterFactory.php | 38 ++++++++-- 4 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 201b8b25e9c..05a9821d71c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -808,8 +808,10 @@ 'arguments' => [ 'mautic.lead.model.lead_segment_filter_date', 'doctrine.orm.entity_manager', - 'mautic.lead.model.lead_segment_decorator_base', '@service_container', + 'mautic.lead.repository.lead_segment_filter_descriptor', + 'mautic.lead.model.lead_segment_decorator_base', + 'mautic.lead.model.lead_segment_decorator_custom_mapped', ], ], 'mautic.lead.model.relative_date' => [ @@ -839,6 +841,13 @@ 'mautic.lead.repository.lead_segment_filter_descriptor', ], ], + 'mautic.lead.model.lead_segment_decorator_custom_mapped' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment_filter_operator', + 'mautic.lead.repository.lead_segment_filter_descriptor', + ], + ], 'mautic.lead.model.random_parameter_name' => [ 'class' => \Mautic\LeadBundle\Segment\RandomParameterName::class, ], diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 3d83cd5b049..2772f5c68d9 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -15,7 +15,6 @@ use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\LeadSegmentFilterOperator; -use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class BaseDecorator implements FilterDecoratorInterface { @@ -24,45 +23,26 @@ class BaseDecorator implements FilterDecoratorInterface /** * @var LeadSegmentFilterOperator */ - private $leadSegmentFilterOperator; - - /** - * @var LeadSegmentFilterDescriptor - */ - private $leadSegmentFilterDescriptor; + protected $leadSegmentFilterOperator; public function __construct( - LeadSegmentFilterOperator $leadSegmentFilterOperator, - LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor + LeadSegmentFilterOperator $leadSegmentFilterOperator ) { $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; - $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; } public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); - - if (empty($this->leadSegmentFilterDescriptor[$originalField]['field'])) { - return $originalField; - } - - return $this->leadSegmentFilterDescriptor[$originalField]['field']; + return $leadSegmentFilterCrate->getField(); } public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); - - if (empty($this->leadSegmentFilterDescriptor[$originalField]['foreign_table'])) { - if ($leadSegmentFilterCrate->isLeadType()) { - return 'leads'; - } - - return 'companies'; + if ($leadSegmentFilterCrate->isLeadType()) { + return 'leads'; } - return $this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; + return 'companies'; } public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) @@ -82,13 +62,7 @@ public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); - - if (!isset($this->leadSegmentFilterDescriptor[$originalField]['type'])) { - return BaseFilterQueryBuilder::getServiceId(); - } - - return $this->leadSegmentFilterDescriptor[$originalField]['type']; + return BaseFilterQueryBuilder::getServiceId(); } public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) @@ -136,9 +110,6 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); - - return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? - $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; + return false; } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php new file mode 100644 index 00000000000..4d3c620eefe --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -0,0 +1,73 @@ +leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; + } + + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField]['field'])) { + return parent::getField($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['field']; + } + + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField]['foreign_table'])) { + return parent::getTable($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; + } + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (!isset($this->leadSegmentFilterDescriptor[$originalField]['type'])) { + return parent::getQueryType($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['type']; + } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? + $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 7d443566a32..47a337d1dfa 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -14,8 +14,10 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; +use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; +use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; use Symfony\Component\DependencyInjection\Container; class LeadSegmentFilterFactory @@ -30,26 +32,40 @@ class LeadSegmentFilterFactory */ private $entityManager; + /** + * @var Container + */ + private $container; + + /** + * @var LeadSegmentFilterDescriptor + */ + private $leadSegmentFilterDescriptor; + /** * @var BaseDecorator */ private $baseDecorator; /** - * @var Container + * @var CustomMappedDecorator */ - private $container; + private $customMappedDecorator; public function __construct( LeadSegmentFilterDate $leadSegmentFilterDate, EntityManager $entityManager, + Container $container, + LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, BaseDecorator $baseDecorator, - Container $container + CustomMappedDecorator $customMappedDecorator ) { - $this->leadSegmentFilterDate = $leadSegmentFilterDate; - $this->entityManager = $entityManager; - $this->baseDecorator = $baseDecorator; - $this->container = $container; + $this->leadSegmentFilterDate = $leadSegmentFilterDate; + $this->entityManager = $entityManager; + $this->container = $container; + $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; + $this->baseDecorator = $baseDecorator; + $this->customMappedDecorator = $customMappedDecorator; } /** @@ -98,6 +114,12 @@ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) */ protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return $this->baseDecorator; + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + return $this->baseDecorator; + } + + return $this->customMappedDecorator; } } From 5bf96ce3a648bb70be330bce147bf1a687e0e0b6 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 19 Jan 2018 09:04:16 +0100 Subject: [PATCH 053/778] add manually susbcribed and unsubsribed users to query --- app/bundles/LeadBundle/Config/config.php | 2 +- .../LeadBundle/Segment/LeadSegmentFilter.php | 6 +++- .../LeadBundle/Segment/LeadSegmentService.php | 4 ++- .../LeadBundle/Segment/Query/QueryBuilder.php | 1 + .../Services/LeadSegmentFilterDescriptor.php | 18 ++++++++++ .../Services/LeadSegmentQueryBuilder.php | 35 +++++++++++++++++-- 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 05a9821d71c..363d5231331 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -740,7 +740,7 @@ ], 'mautic.lead.query.builder.special.leadlist' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder::class, - 'arguments' => [], + 'arguments' => ['mautic.lead.repository.lead_segment_query_builder', 'doctrine.orm.entity_manager', 'mautic.lead.model.lead_segment_filter_factory'], ], ], 'helpers' => [ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 0c890b458cc..3d53660fd05 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -180,7 +180,11 @@ public function setFilterQueryBuilder($filterQueryBuilder) */ public function __toString() { - return sprintf('%s %s', $this->getTable(), $this->getField()); + if (!is_array($this->getParameterValue())) { + return sprintf('table:%s field:%s holder:%s value:%s', $this->getTable(), $this->getField(), $this->getParameterHolder('holder'), $this->getParameterValue()); + } + + return sprintf('table:%s field:%s holder:%s value:%s', $this->getTable(), $this->getField(), print_r($this->getParameterHolder($this->getParameterValue()), true), print_r($this->getParameterValue(), true)); } /** diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index d32f60765d2..fd232cbaee1 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -66,8 +66,10 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) return [0]; } /** @var QueryBuilder $qb */ - $qb = $this->queryBuilder->getLeadsSegmentQueryBuilder($entity->getId(), $segmentFilters, $batchLimiters); + $qb = $this->queryBuilder->getLeadsSegmentQueryBuilder($entity->getId(), $segmentFilters); $qb = $this->queryBuilder->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); + $qb = $this->queryBuilder->addManuallySubscribedQuery($qb, $entity->getId()); + $qb = $this->queryBuilder->addManuallyUnsubsribedQuery($qb, $entity->getId()); $qb = $this->queryBuilder->wrapInCount($qb); // Debug output diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 68366f78564..bdfb31c2420 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -38,6 +38,7 @@ * * @author Guilherme Blanco * @author Benjamin Eberlei + * @author Jan Kozak */ class QueryBuilder { diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index 4eba405f80f..678ff6e4b42 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -34,6 +34,16 @@ public function __construct() 'field' => 'open_count', ]; + $this->translations['hit_url_count'] = [ + 'type' => ForeignFuncFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_table_field' => 'lead_id', + 'table' => 'leads', + 'table_field' => 'id', + 'func' => 'count', + 'field' => 'id', + ]; + $this->translations['lead_email_read_date'] = [ 'type' => ForeignValueFilterQueryBuilder::getServiceId(), 'foreign_table' => 'page_hits', @@ -51,6 +61,14 @@ public function __construct() 'type' => DncFilterQueryBuilder::getServiceId(), ]; + $this->translations['dnc_unsubscribed'] = [ + 'type' => DncFilterQueryBuilder::getServiceId(), + ]; + + $this->translations['dnc_unsubscribed_sms'] = [ + 'type' => DncFilterQueryBuilder::getServiceId(), + ]; + $this->translations['leadlist'] = [ 'type' => LeadListFilterQueryBuilder::getServiceId(), ]; diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php index 39fc5d88b19..424264060a5 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php @@ -55,7 +55,8 @@ public function wrapInCount(QueryBuilder $qb) // Add count functions to the query $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); $qb->addSelect('l.id as leadIdPrimary'); - $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId')->from('('.$qb->getSQL().')', 'sss'); + $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId') + ->from('('.$qb->getSQL().')', 'sss'); $queryBuilder->setParameters($qb->getParameters()); return $queryBuilder; @@ -66,7 +67,7 @@ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $queryBuilder->select('l.id'); $parts = $queryBuilder->getQueryParts(); - $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); + $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); $tableAlias = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id'); @@ -90,6 +91,36 @@ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, return $queryBuilder; } + public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadListId) + { + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, + 'l.id = '.$tableAlias.'.lead_id and '.$tableAlias.'.leadlist_id = '.intval($leadListId)); + $queryBuilder->addJoinCondition($tableAlias, + $queryBuilder->expr()->andX( + $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.manually_removed'), + $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 0) + ), + $queryBuilder->expr()->eq($tableAlias.'.manually_added', 1) + ) + ); + $queryBuilder->orWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); + + return $queryBuilder; + } + + public function addManuallyUnsubsribedQuery(QueryBuilder $queryBuilder, $leadListId) + { + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, + 'l.id = '.$tableAlias.'.lead_id and '.$tableAlias.'.leadlist_id = '.intval($leadListId)); + $queryBuilder->addJoinCondition($tableAlias, $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 1)); + $queryBuilder->andWhere($queryBuilder->expr()->isNull($tableAlias.'.lead_id')); + + return $queryBuilder; + } + /** * Generate a unique parameter name. * From b1193892b7715d3fced018e34053b7d79f22997a Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 19 Jan 2018 17:43:15 +0100 Subject: [PATCH 054/778] Date decorator - handles day times (today, yesterday, tomorrow) --- app/bundles/LeadBundle/Config/config.php | 13 ++ .../LeadBundle/Entity/LeadListRepository.php | 10 +- .../Decorator/Date/DateAnniversary.php | 16 ++ .../Segment/Decorator/Date/DateDayToday.php | 30 ++++ .../Decorator/Date/DateDayTomorrow.php | 31 ++++ .../Decorator/Date/DateDayYesterday.php | 31 ++++ .../Segment/Decorator/Date/DateDefault.php | 16 ++ .../Segment/Decorator/Date/DateFactory.php | 62 +++++++ .../Segment/Decorator/Date/DateMonthLast.php | 16 ++ .../Segment/Decorator/Date/DateMonthNext.php | 16 ++ .../Segment/Decorator/Date/DateMonthThis.php | 16 ++ .../Decorator/Date/DateOptionAbstract.php | 92 +++++++++++ .../Decorator/Date/DateOptionsInterface.php | 20 +++ .../Segment/Decorator/Date/DateWeekLast.php | 16 ++ .../Segment/Decorator/Date/DateWeekNext.php | 16 ++ .../Segment/Decorator/Date/DateWeekThis.php | 16 ++ .../Segment/Decorator/Date/DateYearLast.php | 16 ++ .../Segment/Decorator/Date/DateYearNext.php | 16 ++ .../Segment/Decorator/Date/DateYearThis.php | 16 ++ .../Segment/Decorator/DateDecorator.php | 153 ++++++++++++++++++ .../Segment/LeadSegmentFilterFactory.php | 15 +- 21 files changed, 631 insertions(+), 2 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateYearLast.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateYearNext.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateYearThis.php create mode 100644 app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 05a9821d71c..31b92876bc3 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -812,6 +812,7 @@ 'mautic.lead.repository.lead_segment_filter_descriptor', 'mautic.lead.model.lead_segment_decorator_base', 'mautic.lead.model.lead_segment_decorator_custom_mapped', + 'mautic.lead.model.lead_segment_decorator_date', ], ], 'mautic.lead.model.relative_date' => [ @@ -848,6 +849,18 @@ 'mautic.lead.repository.lead_segment_filter_descriptor', ], ], + 'mautic.lead.model.lead_segment_decorator_date' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\DateDecorator::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment_filter_operator', + 'mautic.lead.repository.lead_segment_filter_descriptor', + 'mautic.lead.model.relative_date', + 'mautic.lead.model.lead_segment.decorator.date.dateFactory', + ], + ], + 'mautic.lead.model.lead_segment.decorator.date.dateFactory' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\Date\DateFactory::class, + ], 'mautic.lead.model.random_parameter_name' => [ 'class' => \Mautic\LeadBundle\Segment\RandomParameterName::class, ], diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 168f01b751a..5b7f64044e4 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -507,7 +507,15 @@ public function getLeadsByList($lists, $args = []) $q->resetQueryPart('groupBy'); } - dump($q->getSQL()); + // Debug output + //dump($q->getSQL()); + $sql = $q->getSQL(); + foreach ($q->getParameters() as $k=>$v) { + $sql = str_replace(":$k", "'$v'", $sql); + } + + echo '
'; + dump($sql); $start = microtime(true); $results = $q->execute()->fetchAll(); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php new file mode 100644 index 00000000000..c4588335a83 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php @@ -0,0 +1,16 @@ +dateTimeHelper->modify('+1 day'); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 day'; + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php new file mode 100644 index 00000000000..3f66bf35f7e --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php @@ -0,0 +1,31 @@ +dateTimeHelper->modify('-1 day'); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 day'; + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php new file mode 100644 index 00000000000..d545dd69329 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php @@ -0,0 +1,16 @@ +dateTimeHelper = $dateTimeHelper; + $this->requiresBetween = $requiresBetween; + $this->includeMidnigh = $includeMidnigh; + $this->isTimestamp = $isTimestamp; + } + + /** + * @return array|string + */ + public function getDateValue() + { + $this->modifyBaseDate(); + + $modifier = $this->getModifierForBetweenRange(); + $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; + + if ($this->requiresBetween) { + $startWith = $this->dateTimeHelper->toUtcString($dateFormat); + + $this->dateTimeHelper->modify($modifier); + $endWith = $this->dateTimeHelper->toUtcString($dateFormat); + + return [$startWith, $endWith]; + } + + if ($this->includeMidnigh) { + $modifier .= ' -1 second'; + $this->dateTimeHelper->modify($modifier); + } + + return $this->dateTimeHelper->toUtcString($dateFormat); + } + + /** + * This function is responsible for setting date. $this->dateTimeHelper holds date with midnight today. + * Eg. +1 day for "tomorrow", -1 for yesterday etc. + */ + abstract protected function modifyBaseDate(); + + /** + * This function is responsible for date modification for between operator. + * Eg. +1 day for "today", "tomorrow" and "yesterday", +1 week for "this week", "last week", "next week" etc. + * + * @return string + */ + abstract protected function getModifierForBetweenRange(); +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php new file mode 100644 index 00000000000..8ec155e6a0b --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php @@ -0,0 +1,20 @@ +leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; + $this->relativeDate = $relativeDate; + $this->dateFactory = $dateFactory; + } + + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + if ($this->isAnniversary($leadSegmentFilterCrate)) { + return 'like'; + } + + if ($this->requiresBetween($leadSegmentFilterCrate)) { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; + } + + return parent::getOperator($leadSegmentFilterCrate); + } + + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + if ($this->isAnniversary($leadSegmentFilterCrate)) { + return '%'.date('-m-d'); + } + + $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); + $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); + $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); + $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); + + $date = $this->dateFactory->getDate($timeframe, $requiresBetween, $includeMidnigh, $isTimestamp); + + return $date->getDateValue(); + } + + private function isAnniversary(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); + + return $timeframe === 'anniversary' || $timeframe === 'birthday'; + } + + private function requiresBetween(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); + } + + private function shouldIncludeMidnight(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return in_array($this->getOperator($leadSegmentFilterCrate), ['gt', 'lte'], true); + } + + private function isTimestamp(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getType() === 'datetime'; + } + + /** + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * + * @return string + */ + private function getTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); + $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); + + return str_replace('mautic.lead.list.', '', $key); + } + + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField]['field'])) { + return parent::getField($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['field']; + } + + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField]['foreign_table'])) { + return parent::getTable($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; + } + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (!isset($this->leadSegmentFilterDescriptor[$originalField]['type'])) { + return parent::getQueryType($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['type']; + } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? + $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 47a337d1dfa..2c367af87bd 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; +use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; @@ -52,13 +53,19 @@ class LeadSegmentFilterFactory */ private $customMappedDecorator; + /** + * @var DateDecorator + */ + private $dateDecorator; + public function __construct( LeadSegmentFilterDate $leadSegmentFilterDate, EntityManager $entityManager, Container $container, LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, BaseDecorator $baseDecorator, - CustomMappedDecorator $customMappedDecorator + CustomMappedDecorator $customMappedDecorator, + DateDecorator $dateDecorator ) { $this->leadSegmentFilterDate = $leadSegmentFilterDate; $this->entityManager = $entityManager; @@ -66,6 +73,7 @@ public function __construct( $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; $this->baseDecorator = $baseDecorator; $this->customMappedDecorator = $customMappedDecorator; + $this->dateDecorator = $dateDecorator; } /** @@ -114,6 +122,11 @@ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) */ protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) { + $type = $leadSegmentFilterCrate->getType(); + if ($type === 'datetime' || $type === 'date') { + return $this->dateDecorator; + } + $originalField = $leadSegmentFilterCrate->getField(); if (empty($this->leadSegmentFilterDescriptor[$originalField])) { From 64d0a2d6e0db78bff2b1c5b6d116d06fce5caee9 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Sun, 21 Jan 2018 20:44:14 +0100 Subject: [PATCH 055/778] code documenting, cleanup, enable exceptions, add two special fields, --- app/bundles/LeadBundle/Config/config.php | 17 ++- .../BaseFilterQueryBuilder.php | 77 ++++++---- .../DncFilterQueryBuilder.php | 26 ++-- .../FilterQueryBuilderInterface.php | 8 +- .../ForeignFuncFilterQueryBuilder.php | 42 +++--- .../ForeignValueFilterQueryBuilder.php | 17 ++- .../LeadListFilterQueryBuilder.php | 132 +++++++++++++----- .../LeadBundle/Segment/LeadSegmentFilter.php | 39 ++++++ .../LeadBundle/Segment/LeadSegmentService.php | 12 ++ .../Services/LeadSegmentFilterDescriptor.php | 9 +- .../LeadSegmentFilterQueryBuilderTrait.php | 14 +- 11 files changed, 273 insertions(+), 120 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 363d5231331..57703adbbdb 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -717,30 +717,33 @@ ], ], // Segment Filter Query builders - 'mautic.lead.query.builder.basic' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder::class, - 'arguments' => [], + 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.foreign.value' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder::class, - 'arguments' => [], + 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.foreign.func' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder::class, - 'arguments' => [], + 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.dnc' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder::class, - 'arguments' => [], + 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.sessions' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\SessionsFilterQueryBuilder::class, - 'arguments' => [], + 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.leadlist' => [ 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder::class, - 'arguments' => ['mautic.lead.repository.lead_segment_query_builder', 'doctrine.orm.entity_manager', 'mautic.lead.model.lead_segment_filter_factory'], + 'arguments' => [ + 'mautic.lead.model.random_parameter_name', + 'mautic.lead.repository.lead_segment_query_builder', + 'doctrine.orm.entity_manager', + 'mautic.lead.model.lead_segment_filter_factory', ], ], ], 'helpers' => [ diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php index 4d14953721e..5f4319ec236 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php @@ -1,40 +1,56 @@ parameterNameGenerator = $randomParameterNameService; + } + /** + * {@inheritdoc} + */ public static function getServiceId() { return 'mautic.lead.query.builder.basic'; } + /** + * {@inheritdoc} + */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { $filterOperator = $filter->getOperator(); $filterGlue = $filter->getGlue(); $filterAggr = $filter->getAggregateFunction(); - // @debug we do not need this, it's just to verify we reference an existing database column - try { - $filter->getColumn(); - } catch (\Exception $e) { - dump(' * ERROR * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); - - return $queryBuilder; - } + // Verify the column exists in database, this might be removed after tested + $filter->getColumn(); $filterParameters = $filter->getParameterValue(); @@ -58,10 +74,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = false; } -// dump($filter->getTable()); if ($filter->getTable()=='companies') { -// dump('companies'); -// } - if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); @@ -104,8 +116,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } break; default: - //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); - dump('Dunno how to handle operator "'.$filterOperator.'"'); + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } } @@ -144,9 +155,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } break; default: - dump(' * IGNORED * - Dunno how to handle operator "'.$filterOperator.'"'); - //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); - $expression = '1=1'; + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } if ($queryBuilder->isJoinTable($filter->getTable())) { @@ -163,4 +172,24 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter return $queryBuilder; } + + /** + * @param RandomParameterName $parameterNameGenerator + * + * @return BaseFilterQueryBuilder + */ + public function setParameterNameGenerator($parameterNameGenerator) + { + $this->parameterNameGenerator = $parameterNameGenerator; + + return $this; + } + + /** + * @return string + */ + protected function generateRandomParameterName() + { + return $this->parameterNameGenerator->generateRandomParameterName(); + } } diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php index 5d8f4066727..095bb2e2b54 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php @@ -1,9 +1,11 @@ getCrate('field')); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php index 810c83afef9..c18070a34a1 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php @@ -1,6 +1,6 @@ getOperator(); $filterGlue = $filter->getGlue(); $filterAggr = $filter->getAggregateFunction(); - // @debug we do not need this, it's just to verify we reference an existing database column - try { - $filter->getColumn(); - } catch (\Exception $e) { - dump(' * ERROR * - Unhandled field '.sprintf(' %s, operator: %s, %s', $filter->__toString(), $filter->getOperator(), print_r($filterAggr, true))); - - return $queryBuilder; - } + $filter->getColumn(); $filterParameters = $filter->getParameterValue(); @@ -97,8 +98,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } break; default: - //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); - dump('Dunno how to handle operator "'.$filterOperator.'"'); + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } } @@ -142,9 +142,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } break; default: - dump(' * IGNORED * - Dunno how to handle operator "'.$filterOperator.'"'); - //throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); - $expression = '1=1'; + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } if ($queryBuilder->isJoinTable($filter->getTable())) { diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php index 2f8727e7b55..257230e2f74 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php @@ -1,9 +1,11 @@ getOperator(); diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php index 7c60a60bc49..572b79c508b 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php @@ -1,65 +1,121 @@ leadSegmentQueryBuilder = $leadSegmentQueryBuilder; + $this->leadSegmentFilterFactory = $leadSegmentFilterFactory; + $this->entityManager = $entityManager; + } + /** + * @return string + */ public static function getServiceId() { return 'mautic.lead.query.builder.special.leadlist'; } + /** + * @param QueryBuilder $queryBuilder + * @param LeadSegmentFilter $filter + * + * @return QueryBuilder + */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { - dump('This is definitely an @todo!!!!'); - - return $queryBuilder; - dump('lead list'); - die(); - $parts = explode('_', $filter->getCrate('field')); - $channel = 'email'; + $segmentIds = $filter->getParameterValue(); - if (count($parts) === 3) { - $channel = $parts[2]; + if (!is_array($segmentIds)) { + $segmentIds = [intval($segmentIds)]; } - $tableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'left'); - - if (!$tableAlias) { - $tableAlias = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact.lead_id = l.id'); + $leftIds = []; + $innerIds = []; + + foreach ($segmentIds as $segmentId) { + $ids[] = $segmentId; + $exclusion = ($filter->getOperator() == 'exists'); + if ($exclusion) { + $leftIds[] = $segmentId; + } else { + if (!isset($innerAlias)) { + $innerAlias = $this->generateRandomParameterName(); + } + $innerIds[] = $segmentId; + } } - $exprParameter = $this->generateRandomParameterName(); - $channelParameter = $this->generateRandomParameterName(); - - $expression = $queryBuilder->expr()->andX( - $queryBuilder->expr()->eq($tableAlias.'.reason', ":$exprParameter"), - $queryBuilder->expr() - ->eq($tableAlias.'.channel', ":$channelParameter") - ); - - $queryBuilder->addJoinCondition($tableAlias, $expression); - - $queryType = $filter->getOperator() === 'eq' ? 'isNull' : 'isNotNull'; + if (count($leftIds)) { + $leftAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $leftAlias, + $queryBuilder->expr()->andX( + $queryBuilder->expr()->in('l.id', $leftIds), + $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) + ); + $queryBuilder->andWhere($queryBuilder->expr()->isNull($leftAlias.'.lead_id')); + } - $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); + if (count($innerIds)) { + $leftAlias = $this->generateRandomParameterName(); + $queryBuilder->innerJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $leftAlias, + $queryBuilder->expr()->andX( + $queryBuilder->expr()->in('l.id', $innerIds), + $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) + ); + } - $queryBuilder->setParameter($exprParameter, ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED); - $queryBuilder->setParameter($channelParameter, $channel); + $queryBuilder->innerJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $innerAlias, 'l.id = '.$innerAlias.'.lead_id'); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 3d53660fd05..e2cea3529d7 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -52,6 +52,11 @@ public function __construct( $this->em = $em; } + /** + * @return \Doctrine\DBAL\Schema\Column + * + * @throws \Exception + */ public function getColumn() { $columns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getTable()); @@ -62,6 +67,11 @@ public function getColumn() return $columns[$this->getField()]; } + /** + * @return string + * + * @deprecated This function might be not used at all + */ public function getEntity() { $converter = new CamelCaseToSnakeCaseNameConverter(); @@ -92,36 +102,65 @@ public function getOperator() return $this->filterDecorator->getOperator($this->leadSegmentFilterCrate); } + /** + * @return mixed + */ public function getField() { return $this->filterDecorator->getField($this->leadSegmentFilterCrate); } + /** + * @return mixed + */ public function getTable() { return $this->filterDecorator->getTable($this->leadSegmentFilterCrate); } + /** + * @param $argument + * + * @return mixed + */ public function getParameterHolder($argument) { return $this->filterDecorator->getParameterHolder($this->leadSegmentFilterCrate, $argument); } + /** + * @return mixed + */ public function getParameterValue() { return $this->filterDecorator->getParameterValue($this->leadSegmentFilterCrate); } + /** + * @return null|string + */ public function getGlue() { return $this->leadSegmentFilterCrate->getGlue(); } + /** + * @return mixed + */ public function getAggregateFunction() { return $this->filterDecorator->getAggregateFunc($this->leadSegmentFilterCrate); } + /** + * @todo check whether necessary and replace or remove + * + * @param null $field + * + * @return array|mixed + * + * @throws \Exception + */ public function getCrate($field = null) { $fields = (array) $this->toArray(); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index fd232cbaee1..36a19dc7f1d 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -36,6 +36,13 @@ class LeadSegmentService */ private $queryBuilder; + /** + * LeadSegmentService constructor. + * + * @param LeadSegmentFilterFactory $leadSegmentFilterFactory + * @param LeadListSegmentRepository $leadListSegmentRepository + * @param LeadSegmentQueryBuilder $queryBuilder + */ public function __construct( LeadSegmentFilterFactory $leadSegmentFilterFactory, LeadListSegmentRepository $leadListSegmentRepository, @@ -46,6 +53,11 @@ public function __construct( $this->queryBuilder = $queryBuilder; } + /** + * @param Doctrine_Query $query + * + * @return string + */ public function getDqlWithParams(Doctrine_Query $query) { $vals = $query->getFlattenedParams(); diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index 678ff6e4b42..81ddd58a5c8 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -45,11 +45,14 @@ public function __construct() ]; $this->translations['lead_email_read_date'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'email_stats', + 'field' => 'date_read', + ]; + + $this->translations['hit_url_date'] = [ 'type' => ForeignValueFilterQueryBuilder::getServiceId(), 'foreign_table' => 'page_hits', - 'foreign_table_field' => 'lead_id', - 'table' => 'leads', - 'table_field' => 'id', 'field' => 'date_hit', ]; diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php index 81f26742550..eb9d3d3046a 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php @@ -16,20 +16,12 @@ trait LeadSegmentFilterQueryBuilderTrait /** * Generate a unique parameter name. * + * @todo make use of the service, this is VERY unreliable + * * @return string */ protected function generateRandomParameterName() { - $alpha_numeric = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - $paramName = substr(str_shuffle($alpha_numeric), 0, 8); - - if (!in_array($paramName, $this->parameterAliases)) { - $this->parameterAliases[] = $paramName; - - return $paramName; - } - - return $this->generateRandomParameterName(); + throw new \Exception('This function is obsole, remove references to it.'); } } From 09def8c2bf0db6c75d2ac2cfdf13c42cdfa491cf Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 22 Jan 2018 09:48:31 +0100 Subject: [PATCH 056/778] Date decorator - handles default date (WIP) --- .../LeadBundle/Entity/LeadListRepository.php | 3 ++- .../Segment/Decorator/Date/DateDefault.php | 16 +++++++++++++++- .../Segment/Decorator/Date/DateFactory.php | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 5b7f64044e4..ebe4b7defeb 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -511,7 +511,8 @@ public function getLeadsByList($lists, $args = []) //dump($q->getSQL()); $sql = $q->getSQL(); foreach ($q->getParameters() as $k=>$v) { - $sql = str_replace(":$k", "'$v'", $sql); + $value = is_array($v) ? implode(', ', $v) : $v; + $sql = str_replace(":$k", "'$value'", $sql); } echo '
'; diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php index d545dd69329..f1cd4b29bd2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php @@ -11,6 +11,20 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateDefault +class DateDefault extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + } + + /** + * {@inheritdoc} + */ + protected function getModifierForBetweenRange() + { + return '+0 day'; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php index f49b483de43..255ed91bbb0 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php @@ -56,7 +56,7 @@ public function getDate($timeframe, $requiresBetween, $includeMidnigh, $isTimest case 'year_this': return new DateYearThis(); default: - return new DateDefault(); + return new DateDefault($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); } } } From 4331cca5b51c41212d98e846f3f3b3a6136cfb23 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 10:13:05 +0100 Subject: [PATCH 057/778] move filterQueryBuilders to another namespace --- app/bundles/LeadBundle/Config/config.php | 12 +- .../Segment/Decorator/BaseDecorator.php | 2 +- .../SessionsFilterQueryBuilder.php | 107 ------------------ .../LeadBundle/Segment/LeadSegmentFilter.php | 1 - .../Segment/LeadSegmentFilterFactory.php | 1 - .../Filter}/BaseFilterQueryBuilder.php | 2 +- .../Filter}/DncFilterQueryBuilder.php | 2 +- .../Filter}/FilterQueryBuilderInterface.php | 2 +- .../Filter}/ForeignFuncFilterQueryBuilder.php | 2 +- .../ForeignValueFilterQueryBuilder.php | 2 +- .../Filter}/LeadListFilterQueryBuilder.php | 2 +- .../Filter/SessionsFilterQueryBuilder.php | 69 +++++++++++ .../Services/LeadSegmentFilterDescriptor.php | 12 +- 13 files changed, 88 insertions(+), 128 deletions(-) delete mode 100644 app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php rename app/bundles/LeadBundle/Segment/{FilterQueryBuilder => Query/Filter}/BaseFilterQueryBuilder.php (99%) rename app/bundles/LeadBundle/Segment/{FilterQueryBuilder => Query/Filter}/DncFilterQueryBuilder.php (97%) rename app/bundles/LeadBundle/Segment/{FilterQueryBuilder => Query/Filter}/FilterQueryBuilderInterface.php (93%) rename app/bundles/LeadBundle/Segment/{FilterQueryBuilder => Query/Filter}/ForeignFuncFilterQueryBuilder.php (99%) rename app/bundles/LeadBundle/Segment/{FilterQueryBuilder => Query/Filter}/ForeignValueFilterQueryBuilder.php (97%) rename app/bundles/LeadBundle/Segment/{FilterQueryBuilder => Query/Filter}/LeadListFilterQueryBuilder.php (98%) create mode 100644 app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 0c9822ed9c2..1f738eb4f51 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -718,27 +718,27 @@ ], // Segment Filter Query builders 'mautic.lead.query.builder.basic' => [ - 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.foreign.value' => [ - 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\ForeignValueFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.foreign.func' => [ - 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\ForeignFuncFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.dnc' => [ - 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\DncFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.sessions' => [ - 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\SessionsFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.leadlist' => [ - 'class' => \Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\LeadListFilterQueryBuilder::class, 'arguments' => [ 'mautic.lead.model.random_parameter_name', 'mautic.lead.repository.lead_segment_query_builder', diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 2772f5c68d9..7f86f8c948b 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -12,9 +12,9 @@ namespace Mautic\LeadBundle\Segment\Decorator; use Mautic\LeadBundle\Entity\RegexTrait; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\LeadSegmentFilterOperator; +use Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder; class BaseDecorator implements FilterDecoratorInterface { diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php deleted file mode 100644 index c914252a268..00000000000 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/SessionsFilterQueryBuilder.php +++ /dev/null @@ -1,107 +0,0 @@ -getOperator(); - - $filterParameters = $filter->getParameterValue(); - - if (is_array($filterParameters)) { - $parameters = []; - foreach ($filterParameters as $filterParameter) { - $parameters[] = $this->generateRandomParameterName(); - } - } else { - $parameters = $this->generateRandomParameterName(); - } - - $filterParametersHolder = $filter->getParameterHolder($parameters); - - $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); - - if (!$tableAlias) { - $tableAlias = $this->generateRandomParameterName(); - - $queryBuilder = $queryBuilder->leftJoin( - $queryBuilder->getTableAlias('leads'), - $filter->getTable(), - $tableAlias, - $tableAlias.'.lead_id = l.id' - ); - } - - $expression = $queryBuilder->expr()->$filterOperator( - 'count('.$tableAlias.'.id)', - $filterParametersHolder - ); - $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); - $queryBuilder->setParametersPairs($parameters, $filterParameters); - - $queryBuilder->andHaving($expression); - - return $queryBuilder; - } -} - -//$operand = 'EXISTS'; -//$table = 'page_hits'; -//$select = 'COUNT(id)'; -//$subqb = $this->entityManager->getConnection()->createQueryBuilder()->select($select) -// ->from(MAUTIC_TABLE_PREFIX.$table, $alias); -// -//$alias2 = $this->generateRandomParameterName(); -//$subqb2 = $this->entityManager->getConnection()->createQueryBuilder()->select($alias2.'.id') -// ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); -// -//$subqb2->where($q->expr()->andX($q->expr()->eq($alias2.'.lead_id', 'l.id'), $q->expr() -// ->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), $q->expr() -// ->lt($alias2.'.date_hit', $alias.'.date_hit'))); -// -//$parameters[$parameter] = $leadSegmentFilter->getFilter(); -// -//$subqb->where($q->expr()->andX($q->expr()->eq($alias.'.lead_id', 'l.id'), $q->expr() -// ->isNull($alias.'.email_id'), $q->expr() -// ->isNull($alias.'.redirect_id'), sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()))); -// -//$opr = ''; -//switch ($func) { -// case 'eq': -// $opr = '='; -// break; -// case 'gt': -// $opr = '>'; -// break; -// case 'gte': -// $opr = '>='; -// break; -// case 'lt': -// $opr = '<'; -// break; -// case 'lte': -// $opr = '<='; -// break; -//} -//if ($opr) { -// $parameters[$parameter] = $leadSegmentFilter->getFilter(); -// $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); -//} -//$groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); -//break; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index e2cea3529d7..9fbbe226188 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -13,7 +13,6 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 2c367af87bd..7fe39c88ab7 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -17,7 +17,6 @@ use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; use Symfony\Component\DependencyInjection\Container; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php similarity index 99% rename from app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 5f4319ec236..d5f3f53455b 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -8,7 +8,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php similarity index 97% rename from app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php index 095bb2e2b54..0cf25f1d0b6 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php @@ -8,7 +8,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Segment\LeadSegmentFilter; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php b/app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php similarity index 93% rename from app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php rename to app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php index c18070a34a1..27cdf071b78 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/FilterQueryBuilderInterface.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php @@ -8,7 +8,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php similarity index 99% rename from app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 43f18e4307f..27411354b56 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -8,7 +8,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php similarity index 97% rename from app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 257230e2f74..7b681b0ca78 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -8,7 +8,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; diff --git a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php similarity index 98% rename from app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 572b79c508b..00696372180 100644 --- a/app/bundles/LeadBundle/Segment/FilterQueryBuilder/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -8,7 +8,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\FilterQueryBuilder; +namespace Mautic\LeadBundle\Segment\Query\Filter; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\LeadSegmentFilter; diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php new file mode 100644 index 00000000000..4fdca011376 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php @@ -0,0 +1,69 @@ +getOperator(); + + $filterParameters = $filter->getParameterValue(); + + if (is_array($filterParameters)) { + $parameters = []; + foreach ($filterParameters as $filterParameter) { + $parameters[] = $this->generateRandomParameterName(); + } + } else { + $parameters = $this->generateRandomParameterName(); + } + + $filterParametersHolder = $filter->getParameterHolder($parameters); + + $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + + $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->getTableAlias('leads'), + $filter->getTable(), + $tableAlias, + $tableAlias.'.lead_id = l.id' + ); + } + + $expression = $queryBuilder->expr()->$filterOperator( + 'count('.$tableAlias.'.id)', + $filterParametersHolder + ); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + $queryBuilder->setParametersPairs($parameters, $filterParameters); + + $queryBuilder->andHaving($expression); + + return $queryBuilder; + } +} diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index 81ddd58a5c8..8532765259d 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -11,12 +11,12 @@ namespace Mautic\LeadBundle\Services; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\BaseFilterQueryBuilder; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\DncFilterQueryBuilder; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignFuncFilterQueryBuilder; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\ForeignValueFilterQueryBuilder; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\LeadListFilterQueryBuilder; -use Mautic\LeadBundle\Segment\FilterQueryBuilder\SessionsFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\DncFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\ForeignFuncFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\ForeignValueFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\LeadListFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder; class LeadSegmentFilterDescriptor extends \ArrayIterator { From 1ac05e8cb9cda8181733fd64f934730b7927cd44 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 10:26:16 +0100 Subject: [PATCH 058/778] add between and not between expression methods --- .../Query/Expression/ExpressionBuilder.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index 6bfff5114af..9a81ce2d654 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -109,6 +109,44 @@ public function comparison($x, $operator, $y) return $x.' '.$operator.' '.$y; } + /** + * Creates a between comparison expression. + * + * @param $x + * @param $arr + * + * @throws \Exception + * + * @return string + */ + public function between($x, $arr) + { + if (!is_array($arr) || count($arr) != 2) { + throw new \Exception('Between expression expects send argument to be an array with exactly two elements'); + } + + return $x.' BETWEEN '.$this->comparison($arr[0], 'AND', $arr[1]); + } + + /** + * Creates a not between comparison expression. + * + * @param $x + * @param $arr + * + * @throws \Exception + * + * @return string + */ + public function notBetween($x, $arr) + { + if (!is_array($arr) || count($arr) != 2) { + throw new \Exception('Not between expression expects send argument to be an array with exactly two elements'); + } + + return 'NOT '.$this->between($x, $arr); + } + /** * Creates an equality comparison expression with the given arguments. * From e9dc9a9e23a4347777356585f957b1d185792790 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 10:39:38 +0100 Subject: [PATCH 059/778] add between expressions to list in basic fqb --- .../Segment/Query/Expression/ExpressionBuilder.php | 7 +++++++ .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index 9a81ce2d654..798b88b8a0d 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -131,6 +131,13 @@ public function between($x, $arr) /** * Creates a not between comparison expression. * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * * @param $x * @param $arr * diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index d5f3f53455b..df483e279de 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -78,6 +78,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = $this->generateRandomParameterName(); switch ($filterOperator) { + case 'between': + case 'notBetween': case 'notLike': case 'notIn': case 'empty': @@ -141,6 +143,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'notIn': case 'in': case 'regexp': + case 'between': + case 'notBetween': case 'notRegexp': if ($filterAggr) { $expression = $queryBuilder->expr()->$filterOperator( From 78f67503ac060e857e541ca82738780910c1274f Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 13:07:40 +0100 Subject: [PATCH 060/778] Fix incorrect return types for fluent functions --- .../LeadBundle/Segment/Query/Expression/ExpressionBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index 798b88b8a0d..a0607487fbb 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -69,7 +69,7 @@ public function __construct(Connection $connection) * @param mixed $x Optional clause. Defaults = null, but requires * at least one defined when converting to string. * - * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression */ public function andX($x = null) { @@ -88,7 +88,7 @@ public function andX($x = null) * @param mixed $x Optional clause. Defaults = null, but requires * at least one defined when converting to string. * - * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression */ public function orX($x = null) { From 8644ef8f26bfa977f823a3b366b5ec0138b9594e Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 15:13:01 +0100 Subject: [PATCH 061/778] move segment query builder next to other builders --- app/bundles/LeadBundle/Config/config.php | 2 +- app/bundles/LeadBundle/Segment/LeadSegmentService.php | 2 +- .../Segment/Query/Filter/LeadListFilterQueryBuilder.php | 4 ++-- .../{Services => Segment/Query}/LeadSegmentQueryBuilder.php | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) rename app/bundles/LeadBundle/{Services => Segment/Query}/LeadSegmentQueryBuilder.php (98%) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1f738eb4f51..41993d0dd24 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -792,7 +792,7 @@ 'arguments' => [], ], 'mautic.lead.repository.lead_segment_query_builder' => [ - 'class' => \Mautic\LeadBundle\Services\LeadSegmentQueryBuilder::class, + 'class' => Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder::class, 'arguments' => [ 'doctrine.orm.entity_manager', 'mautic.lead.model.random_parameter_name', diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 36a19dc7f1d..a5bba012f4a 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -14,8 +14,8 @@ use Doctrine\DBAL\Query\QueryBuilder; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListSegmentRepository; +use Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder; use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; -use Mautic\LeadBundle\Services\LeadSegmentQueryBuilder; class LeadSegmentService { diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 00696372180..bd923987611 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -13,9 +13,9 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilterFactory; +use Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\RandomParameterName; -use Mautic\LeadBundle\Services\LeadSegmentQueryBuilder; /** * Class LeadListFilterQueryBuilder. @@ -80,7 +80,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $segmentIds = [intval($segmentIds)]; } - $leftIds = []; + $leftIds = []; $innerIds = []; foreach ($segmentIds as $segmentId) { diff --git a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php similarity index 98% rename from app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 424264060a5..f2cbd630dc8 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -9,12 +9,11 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Services; +namespace Mautic\LeadBundle\Segment\Query; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilters; -use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\RandomParameterName; class LeadSegmentQueryBuilder From dae3882d80d719370a590293e71a6c00b07c17db Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 22 Jan 2018 15:33:36 +0100 Subject: [PATCH 062/778] Date decorator - default date returs correct value --- .../Segment/Decorator/Date/DateDefault.php | 18 ++++++++++++------ .../Segment/Decorator/Date/DateFactory.php | 5 +++-- .../Segment/Decorator/DateDecorator.php | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php index f1cd4b29bd2..00ab4f57fca 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php @@ -11,20 +11,26 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateDefault extends DateOptionAbstract implements DateOptionsInterface +class DateDefault implements DateOptionsInterface { /** - * {@inheritdoc} + * @var string */ - protected function modifyBaseDate() + private $originalValue; + + /** + * @param string $originalValue + */ + public function __construct($originalValue) { + $this->originalValue = $originalValue; } /** - * {@inheritdoc} + * @return string */ - protected function getModifierForBetweenRange() + public function getDateValue() { - return '+0 day'; + return $this->originalValue; } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php index 255ed91bbb0..34990e2b6c0 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php @@ -16,6 +16,7 @@ class DateFactory { /** + * @param string $originalValue * @param string $timeframe * @param bool $requiresBetween * @param bool $includeMidnigh @@ -23,7 +24,7 @@ class DateFactory * * @return DateOptionsInterface */ - public function getDate($timeframe, $requiresBetween, $includeMidnigh, $isTimestamp) + public function getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp) { $dtHelper = new DateTimeHelper('midnight today', null, 'local'); @@ -56,7 +57,7 @@ public function getDate($timeframe, $requiresBetween, $includeMidnigh, $isTimest case 'year_this': return new DateYearThis(); default: - return new DateDefault($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateDefault($originalValue); } } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index 02ce8688feb..d62bf8b10d9 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -65,12 +65,13 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate return '%'.date('-m-d'); } + $originalValue = $leadSegmentFilterCrate->getFilter(); $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); - $date = $this->dateFactory->getDate($timeframe, $requiresBetween, $includeMidnigh, $isTimestamp); + $date = $this->dateFactory->getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp); return $date->getDateValue(); } From 7d879c4edc3507f63caf4116fca0687e0dfffcc7 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 22 Jan 2018 15:40:15 +0100 Subject: [PATCH 063/778] Date decorator - anniversary moved to separate class --- .../Segment/Decorator/Date/DateAnniversary.php | 9 ++++++++- .../LeadBundle/Segment/Decorator/DateDecorator.php | 4 ---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php index c4588335a83..1a30e71e7f2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php @@ -11,6 +11,13 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateAnniversary +class DateAnniversary implements DateOptionsInterface { + /** + * @return string + */ + public function getDateValue() + { + return '%'.date('-m-d'); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index d62bf8b10d9..30aad3e6510 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -61,10 +61,6 @@ public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - if ($this->isAnniversary($leadSegmentFilterCrate)) { - return '%'.date('-m-d'); - } - $originalValue = $leadSegmentFilterCrate->getFilter(); $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); From 73eb6e1014c3899244672deb851ad7632c09acf8 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 22 Jan 2018 16:13:03 +0100 Subject: [PATCH 064/778] Date decorator - dates responsible for week working (last week, this week, next week) --- .../Segment/Decorator/Date/DateFactory.php | 6 +++--- .../Segment/Decorator/Date/DateWeekLast.php | 17 ++++++++++++++++- .../Segment/Decorator/Date/DateWeekNext.php | 17 ++++++++++++++++- .../Segment/Decorator/Date/DateWeekThis.php | 17 ++++++++++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php index 34990e2b6c0..926922ebee6 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php @@ -39,11 +39,11 @@ public function getDate($originalValue, $timeframe, $requiresBetween, $includeMi case 'yesterday': return new DateDayYesterday($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'week_last': - return new DateWeekLast(); + return new DateWeekLast($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'week_next': - return new DateWeekNext(); + return new DateWeekNext($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'week_this': - return new DateWeekThis(); + return new DateWeekThis($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_last': return new DateMonthLast(); case 'month_next': diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php index 66168dc9d0f..5aa67dee761 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php @@ -11,6 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateWeekLast +class DateWeekLast extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight monday last week', null); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 week'; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php index 2dafd993bca..55a949792c8 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php @@ -11,6 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateWeekNext +class DateWeekNext extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight monday next week', null); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 week'; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php index eeb7c4e8acf..fb90ac3c99b 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php @@ -11,6 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateWeekThis +class DateWeekThis extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight monday this week', null); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 week'; + } } From 6aac246603c1de993a60bfbd3b2443523437d14a Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 16:14:29 +0100 Subject: [PATCH 065/778] remove trait and all references, create debug method in query builder, cleanup --- .../LeadBundle/Segment/LeadSegmentFilter.php | 23 ---------------- .../LeadBundle/Segment/LeadSegmentService.php | 20 ++------------ .../LeadBundle/Segment/Query/QueryBuilder.php | 20 ++++++++++++++ .../LeadSegmentFilterQueryBuilderTrait.php | 27 ------------------- app/bundles/LeadBundle/Services/test.sql | 1 - 5 files changed, 22 insertions(+), 69 deletions(-) delete mode 100644 app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php delete mode 100644 app/bundles/LeadBundle/Services/test.sql diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 9fbbe226188..47ca2a6f3a1 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -14,13 +14,9 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\Query\QueryBuilder; -use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; class LeadSegmentFilter { - use LeadSegmentFilterQueryBuilderTrait; - /** * @var LeadSegmentFilterCrate */ @@ -66,25 +62,6 @@ public function getColumn() return $columns[$this->getField()]; } - /** - * @return string - * - * @deprecated This function might be not used at all - */ - public function getEntity() - { - $converter = new CamelCaseToSnakeCaseNameConverter(); - if ($this->getQueryDescription()) { - $table = $this->queryDescription['foreign_table']; - } else { - $table = $this->getObject(); - } - - $entity = sprintf('MauticLeadBundle:%s', ucfirst($converter->denormalize($table))); - - return $entity; - } - /** * @return string */ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index a5bba012f4a..31c756fa272 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -15,12 +15,9 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListSegmentRepository; use Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder; -use Mautic\LeadBundle\Services\LeadSegmentFilterQueryBuilderTrait; class LeadSegmentService { - use LeadSegmentFilterQueryBuilderTrait; - /** * @var LeadListSegmentRepository */ @@ -53,25 +50,10 @@ public function __construct( $this->queryBuilder = $queryBuilder; } - /** - * @param Doctrine_Query $query - * - * @return string - */ - public function getDqlWithParams(Doctrine_Query $query) - { - $vals = $query->getFlattenedParams(); - $sql = $query->getDql(); - $sql = str_replace('?', '%s', $sql); - - return vsprintf($sql, $vals); - } - public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) { $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($entity); - echo '
New version result:'; $versionStart = microtime(true); if (!count($segmentFilters)) { @@ -84,6 +66,8 @@ public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) $qb = $this->queryBuilder->addManuallyUnsubsribedQuery($qb, $entity->getId()); $qb = $this->queryBuilder->wrapInCount($qb); + $debug = $qb->getDebugOutput(); + // Debug output $sql = $qb->getSQL(); foreach ($qb->getParameters() as $k=>$v) { diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index bdfb31c2420..3f45a315bff 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -34,6 +34,9 @@ * even if some vendors such as MySQL support it. * * @see www.doctrine-project.org + * + * @todo rework this to extend the original Query Builder instead of writing new one + * * @since 2.1 * * @author Guilherme Blanco @@ -1554,4 +1557,21 @@ public function isJoinTable($table) return false; } + + /** + * @return mixed + */ + public function getDebugOutput() + { + $params = $this->getParameters(); + $sql = $this->getSQL(); + foreach ($params as $key=>$val) { + if (!is_int($val) and !is_float($val)) { + $val = "'$val'"; + } + $sql = str_replace(":{$key}", $val, $sql); + } + + return $sql; + } } diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php deleted file mode 100644 index eb9d3d3046a..00000000000 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterQueryBuilderTrait.php +++ /dev/null @@ -1,27 +0,0 @@ - "2017-09-10 23:14" GROUP BY l.id HAVING sum(es.open_count) > 0 \ No newline at end of file From 8ddd4c278849cd0cc2f6068ec07422104bb6b579 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 22 Jan 2018 16:17:29 +0100 Subject: [PATCH 066/778] fix type in exception description --- .../LeadBundle/Segment/Query/Expression/ExpressionBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index a0607487fbb..c2bd65d1614 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -122,7 +122,7 @@ public function comparison($x, $operator, $y) public function between($x, $arr) { if (!is_array($arr) || count($arr) != 2) { - throw new \Exception('Between expression expects send argument to be an array with exactly two elements'); + throw new \Exception('Between expression expects second argument to be an array with exactly two elements'); } return $x.' BETWEEN '.$this->comparison($arr[0], 'AND', $arr[1]); @@ -148,7 +148,7 @@ public function between($x, $arr) public function notBetween($x, $arr) { if (!is_array($arr) || count($arr) != 2) { - throw new \Exception('Not between expression expects send argument to be an array with exactly two elements'); + throw new \Exception('Not between expression expects second argument to be an array with exactly two elements'); } return 'NOT '.$this->between($x, $arr); From 5f80cd4f57e29fa43d826dcea014fb0e49990cb0 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 22 Jan 2018 18:27:12 +0100 Subject: [PATCH 067/778] Date decorator - default date fix - parameters for between --- .../Segment/Decorator/Date/DateDefault.php | 15 +++++++++++---- .../Segment/Decorator/Date/DateFactory.php | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php index 00ab4f57fca..b8c14652eef 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php @@ -18,19 +18,26 @@ class DateDefault implements DateOptionsInterface */ private $originalValue; + /** + * @var string + */ + private $requiresBetween; + /** * @param string $originalValue + * @param string $requiresBetween */ - public function __construct($originalValue) + public function __construct($originalValue, $requiresBetween) { - $this->originalValue = $originalValue; + $this->originalValue = $originalValue; + $this->requiresBetween = $requiresBetween; } /** - * @return string + * @return string|array */ public function getDateValue() { - return $this->originalValue; + return $this->requiresBetween ? [$this->originalValue, $this->originalValue] : $this->originalValue; } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php index 926922ebee6..fd02c7be2a2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php @@ -57,7 +57,7 @@ public function getDate($originalValue, $timeframe, $requiresBetween, $includeMi case 'year_this': return new DateYearThis(); default: - return new DateDefault($originalValue); + return new DateDefault($originalValue, $requiresBetween); } } } From fc033e913d79e731d94954dbafd96b2f7355e0f8 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 22 Jan 2018 18:42:01 +0100 Subject: [PATCH 068/778] Date decorator - handles month based dates (last month, this month, next month) --- .../Segment/Decorator/Date/DateFactory.php | 6 +++--- .../Segment/Decorator/Date/DateMonthLast.php | 17 ++++++++++++++++- .../Segment/Decorator/Date/DateMonthNext.php | 17 ++++++++++++++++- .../Segment/Decorator/Date/DateMonthThis.php | 17 ++++++++++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php index fd02c7be2a2..5fd98c5a533 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php @@ -45,11 +45,11 @@ public function getDate($originalValue, $timeframe, $requiresBetween, $includeMi case 'week_this': return new DateWeekThis($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_last': - return new DateMonthLast(); + return new DateMonthLast($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_next': - return new DateMonthNext(); + return new DateMonthNext($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_this': - return new DateMonthThis(); + return new DateMonthThis($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'year_last': return new DateYearLast(); case 'year_next': diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php index 35965f1abbb..d24f6f84f1f 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php @@ -11,6 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateMonthLast +class DateMonthLast extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight first day of last month', null); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 month'; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php index 5c07c97fb42..e7da0488925 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php @@ -11,6 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateMonthNext +class DateMonthNext extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight first day of next month', null); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 month'; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php index 957ca84b480..7c533cbde6a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php @@ -11,6 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateMonthThis +class DateMonthThis extends DateOptionAbstract implements DateOptionsInterface { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight first day of this month', null); + } + + /** + * @return string + */ + protected function getModifierForBetweenRange() + { + return '+1 month'; + } } From d8f1031546d9462d9a188bb4b993c8e68e605955 Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Mon, 22 Jan 2018 14:52:00 -0500 Subject: [PATCH 069/778] preview functionality for email builder --- app/bundles/CoreBundle/Assets/css/app.css | 3 +++ .../Controller/BuilderControllerTrait.php | 1 - .../Translations/en_US/messages.ini | 1 + .../CoreBundle/Views/Helper/builder.html.php | 23 ++++++++++++++++++- .../Controller/EmailController.php | 5 ++++ .../EmailBundle/Views/Email/form.html.php | 4 +++- media/css/app.css | 3 ++- media/js/app.js | 2 +- 8 files changed, 37 insertions(+), 5 deletions(-) diff --git a/app/bundles/CoreBundle/Assets/css/app.css b/app/bundles/CoreBundle/Assets/css/app.css index fe66b73e6a2..21f6039398e 100644 --- a/app/bundles/CoreBundle/Assets/css/app.css +++ b/app/bundles/CoreBundle/Assets/css/app.css @@ -4314,6 +4314,9 @@ lesshat-selector { -lh-property: 0 ; width: 70%; height: 100%; } +.builder-panel #preview .panel-body { + padding: 7px 0; +} .code-mode .builder-panel { width: 50%; position: fixed; diff --git a/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php b/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php index 90207d785c5..cd4245442ab 100644 --- a/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php +++ b/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php @@ -26,7 +26,6 @@ protected function getAssetsForBuilder() /** @var \Symfony\Bundle\FrameworkBundle\Templating\Helper\RouterHelper $routerHelper */ $routerHelper = $this->get('templating.helper.router'); $translator = $this->get('templating.helper.translator'); - $assetsHelper ->setContext(AssetsHelper::CONTEXT_BUILDER) ->addScriptDeclaration("var mauticBasePath = '".$this->request->getBasePath()."';") diff --git a/app/bundles/CoreBundle/Translations/en_US/messages.ini b/app/bundles/CoreBundle/Translations/en_US/messages.ini index cedc886c36e..1170698db2b 100644 --- a/app/bundles/CoreBundle/Translations/en_US/messages.ini +++ b/app/bundles/CoreBundle/Translations/en_US/messages.ini @@ -306,6 +306,7 @@ mautic.core.permissions.viewother="View Others" mautic.core.permissions.viewown="View Own" mautic.core.popupblocked="It seems the browser is blocking popups. Please enable popups for this site and try again." mautic.core.position="Position" +mautic.core.preview="Preview" mautic.core.recent.activity="Recent Activity" mautic.core.redo="Redo" mautic.core.referer="Referer" diff --git a/app/bundles/CoreBundle/Views/Helper/builder.html.php b/app/bundles/CoreBundle/Views/Helper/builder.html.php index eb22ca1484f..1ed0d73ee1c 100644 --- a/app/bundles/CoreBundle/Views/Helper/builder.html.php +++ b/app/bundles/CoreBundle/Views/Helper/builder.html.php @@ -18,7 +18,9 @@
- render('MauticCoreBundle:Helper:builder_buttons.html.php', ['onclick' => "Mautic.closeBuilder('$type');"]); ?> + render('MauticCoreBundle:Helper:builder_buttons.html.php', [ + 'onclick' => "Mautic.closeBuilder('$type');", + ]); ?>
+
+
+

trans('mautic.email.urlvariant'); ?>

+
+
+
+
+ + + + +
+
+
+

trans('mautic.core.slot.types'); ?>

diff --git a/app/bundles/EmailBundle/Controller/EmailController.php b/app/bundles/EmailBundle/Controller/EmailController.php index 99778e2e148..757e1f607bf 100644 --- a/app/bundles/EmailBundle/Controller/EmailController.php +++ b/app/bundles/EmailBundle/Controller/EmailController.php @@ -849,6 +849,11 @@ public function editAction($objectId, $ignorePost = false, $forceTypeSelection = 'builderAssets' => trim(preg_replace('/\s+/', ' ', $this->getAssetsForBuilder())), // strip new lines 'sectionForm' => $sectionForm->createView(), 'permissions' => $permissions, + 'previewUrl' => $this->generateUrl( + 'mautic_email_preview', + ['objectId' => $entity->getId()], + true + ), ], 'contentTemplate' => 'MauticEmailBundle:Email:form.html.php', 'passthroughVars' => [ diff --git a/app/bundles/EmailBundle/Views/Email/form.html.php b/app/bundles/EmailBundle/Views/Email/form.html.php index fac9059e74b..be8d8a28eb9 100644 --- a/app/bundles/EmailBundle/Views/Email/form.html.php +++ b/app/bundles/EmailBundle/Views/Email/form.html.php @@ -276,7 +276,9 @@ 'slots' => $slots, 'sections' => $sections, 'objectId' => $email->getSessionId(), -]); ?> + 'previewUrl' => $previewUrl, +]); +?> getEmailType(); diff --git a/media/css/app.css b/media/css/app.css index 4ea074b7ca4..fa89ba43e6d 100644 --- a/media/css/app.css +++ b/media/css/app.css @@ -446,7 +446,8 @@ solid #ebedf0 !important}.bdr-l{border-left:1px solid #ebedf0 !important}.bdr-r{ .builder-slot .builder-panel-top{margin-bottom:10px}.builder .template-dnd-help, .builder-slot .template-dnd-help, .builder .custom-dnd-help, -.builder-slot .custom-dnd-help{display:table-cell;vertical-align:middle;width:100%}.builder-active{background-color:#fff;position:absolute;top:0;bottom:0;right:0;left:0;width:100%;height:100%;z-index:1030}.builder-panel{position:fixed;top:0;bottom:0;right:0;width:30%;height:100%;padding:15px;background-color:#d5d4d4;overflow-y:auto}.builder-content{position:fixed;left:0;top:0;width:70%;height:100%}.code-mode .builder-panel{width:50%;position:fixed}.code-mode .builder-content{width:50%}.builder-panel .panel +.builder-slot .custom-dnd-help{display:table-cell;vertical-align:middle;width:100%}.builder-active{background-color:#fff;position:absolute;top:0;bottom:0;right:0;left:0;width:100%;height:100%;z-index:1030}.builder-panel{position:fixed;top:0;bottom:0;right:0;width:30%;height:100%;padding:15px;background-color:#d5d4d4;overflow-y:auto}.builder-content{position:fixed;left:0;top:0;width:70%;height:100%}.builder-panel #preview .panel-body{padding:7px +0}.code-mode .builder-panel{width:50%;position:fixed}.code-mode .builder-content{width:50%}.builder-panel .panel a.btn{white-space:normal}.builder-active-slot{background-color:#fff;z-index:1030}.builder-panel-slot{width:50%;padding:15px;background-color:#d5d4d4;overflow-y:auto}.builder-content-slot{left:50%;width:50%}.code-mode .builder-panel-slot{width:50%}.code-mode .builder-content-slot{width:50%}.builder-panel-slot .panel a.btn{white-space:normal}.ui-draggable-iframeFix{z-index:9999 !important}.CodeMirror{border:1px solid #eee;height:auto}.CodeMirror-hints{position:absolute;z-index:9999 !important;overflow:hidden;list-style:none;margin:0;padding:2px;-webkit-box-shadow:2px 3px 5px rgba(0, 0, 0, 0.2);-moz-box-shadow:2px 3px 5px rgba(0, 0, 0, 0.2);box-shadow:2px 3px 5px rgba(0, 0, 0, 0.2);border-radius:3px;border:1px diff --git a/media/js/app.js b/media/js/app.js index f2b8470ba5a..dc1b18caa3d 100644 --- a/media/js/app.js +++ b/media/js/app.js @@ -148,7 +148,7 @@ mQuery(this).attr('name',name);});}});};Mautic.closeGlobalSearchResults=function Mautic.stopPageLoadingBar();};;Mautic.getUrlParameter=function(name){name=name.replace(/[\[]/,'\\[').replace(/[\]]/,'\\]');var regex=new RegExp('[\\?&]'+name+'=([^&#]*)');var results=regex.exec(location.search);return results===null?'':decodeURIComponent(results[1].replace(/\+/g,' '));};Mautic.launchBuilder=function(formName,actionName){var builder=mQuery('.builder');Mautic.codeMode=builder.hasClass('code-mode');Mautic.showChangeThemeWarning=true;mQuery('body').css('overflow-y','hidden');builder.addClass('builder-active').removeClass('hide');if(typeof actionName=='undefined'){actionName=formName;} var builderCss={margin:"0",padding:"0",border:"none",width:"100%",height:"100%"};var themeHtml=mQuery('textarea.builder-html').val();if(Mautic.codeMode){var rawTokens=mQuery.map(Mautic.builderTokens,function(element,index){return index}).sort();Mautic.builderCodeMirror=CodeMirror(document.getElementById('customHtmlContainer'),{value:themeHtml,lineNumbers:true,mode:'htmlmixed',extraKeys:{"Ctrl-Space":"autocomplete"},lineWrapping:true,hintOptions:{hint:function(editor){var cursor=editor.getCursor();var currentLine=editor.getLine(cursor.line);var start=cursor.ch;var end=start;while(end
').css(builderCss).appendTo('.builder-content');btnCloseBuilder.prop('disabled',true);applyBtn.prop('disabled',true);var assets=Mautic.htmlspecialchars_decode(mQuery('[data-builder-assets]').html());themeHtml=themeHtml.replace('',assets+'');Mautic.initBuilderIframe(themeHtml,btnCloseBuilder,applyBtn);};Mautic.inBuilderSubmissionOn=function(form){var inBuilder=mQuery('');form.append(inBuilder);} +Mautic.inBuilderSubmissionOff();},true);});builderPanel.on('scroll',function(e){if(mQuery.find('.fr-popup:visible').length){if(!Mautic.isInViewport(builderPanel.find('.fr-view:visible'))){builderPanel.find('.fr-view:visible').blur();builderPanel.find('input:focus').blur();}}else{builderPanel.find('input:focus').blur();}});var overlay=mQuery('').css(builderCss).appendTo('.builder-content');btnCloseBuilder.prop('disabled',true);applyBtn.prop('disabled',true);var assets=Mautic.htmlspecialchars_decode(mQuery('[data-builder-assets]').html());themeHtml=themeHtml.replace('',assets+'');Mautic.initBuilderIframe(themeHtml,btnCloseBuilder,applyBtn);};Mautic.isInViewport=function(el){var elementTop=mQuery(el).offset().top;var elementBottom=elementTop+mQuery(el).outerHeight();var viewportTop=mQuery(window).scrollTop();var viewportBottom=viewportTop+mQuery(window).height();return elementBottom>viewportTop&&elementTop');form.append(inBuilder);} Mautic.inBuilderSubmissionOff=function(form){Mautic.isInBuilder=false;mQuery('input[name="inBuilder"]').remove();} Mautic.processBuilderErrors=function(response){if(response.validationError){mQuery('.btn-apply-builder').attr('disabled',true);mQuery('#builder-errors').show('fast').text(response.validationError);}};Mautic.formatCode=function(){Mautic.builderCodeMirror.autoFormatRange({line:0,ch:0},{line:Mautic.builderCodeMirror.lineCount()});} Mautic.openMediaManager=function(){Mautic.openServerBrowser(mauticBasePath+'/'+mauticAssetPrefix+'app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/index.html?type=Images',screen.width*0.7,screen.height*0.7);} From 139b5cdd1a102b9efceb801b5c6e5acde4b15c0d Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Mon, 22 Jan 2018 15:12:07 -0500 Subject: [PATCH 070/778] extending functionality to page builder --- app/bundles/PageBundle/Controller/PageController.php | 3 ++- app/bundles/PageBundle/Views/Page/form.html.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/PageBundle/Controller/PageController.php b/app/bundles/PageBundle/Controller/PageController.php index 28846999d95..ed59b966e39 100644 --- a/app/bundles/PageBundle/Controller/PageController.php +++ b/app/bundles/PageBundle/Controller/PageController.php @@ -622,6 +622,7 @@ public function editAction($objectId, $ignorePost = false) 'sections' => $this->buildSlotForms($sections), 'builderAssets' => trim(preg_replace('/\s+/', ' ', $this->getAssetsForBuilder())), // strip new lines 'sectionForm' => $sectionForm->createView(), + 'previewUrl' => $this->generateUrl('mautic_page_preview', ['id' => $objectId], true), 'permissions' => $security->isGranted( [ 'page:preference_center:editown', @@ -629,7 +630,7 @@ public function editAction($objectId, $ignorePost = false) ], 'RETURN_ARRAY' ), - 'security' => $security, + 'security' => $security, ], 'contentTemplate' => 'MauticPageBundle:Page:form.html.php', 'passthroughVars' => [ diff --git a/app/bundles/PageBundle/Views/Page/form.html.php b/app/bundles/PageBundle/Views/Page/form.html.php index fa7cec9b6bb..6ce609f9d94 100644 --- a/app/bundles/PageBundle/Views/Page/form.html.php +++ b/app/bundles/PageBundle/Views/Page/form.html.php @@ -128,4 +128,5 @@ 'slots' => $slots, 'sections' => $sections, 'objectId' => $activePage->getSessionId(), + 'previewUrl' => $previewUrl, ]); ?> From cab6117a0fd9a3bf721a6f8230cb875abda71294 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 23 Jan 2018 11:12:48 +0100 Subject: [PATCH 071/778] use expression function as self constatnt --- .../Query/Expression/ExpressionBuilder.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index c2bd65d1614..b2e5b3a0568 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -32,13 +32,14 @@ */ class ExpressionBuilder { - const EQ = '='; - const NEQ = '<>'; - const LT = '<'; - const LTE = '<='; - const GT = '>'; - const GTE = '>='; - const REGEXP = 'REGEXP'; + const EQ = '='; + const NEQ = '<>'; + const LT = '<'; + const LTE = '<='; + const GT = '>'; + const GTE = '>='; + const REGEXP = 'REGEXP'; + const BETWEEN = 'BETWEEN'; /** * The DBAL Connection. @@ -125,7 +126,7 @@ public function between($x, $arr) throw new \Exception('Between expression expects second argument to be an array with exactly two elements'); } - return $x.' BETWEEN '.$this->comparison($arr[0], 'AND', $arr[1]); + return $x.' '.self::BETWEEN.' '.$this->comparison($arr[0], 'AND', $arr[1]); } /** From 255d61cd271e1201bfa7276984c23cb1e7028a4e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 23 Jan 2018 15:01:50 +0100 Subject: [PATCH 072/778] Date decorator refactored - Instance of DateOption is returned and decorator is part injected to it --- app/bundles/LeadBundle/Config/config.php | 15 ++- .../Decorator/Date/DateAnniversary.php | 51 ++++++++- .../Segment/Decorator/Date/DateDefault.php | 57 ++++++++-- .../Segment/Decorator/Date/DateFactory.php | 100 +++++++++++------- .../Decorator/Date/DateOptionAbstract.php | 53 +++++++++- .../Decorator/Date/DateOptionFactory.php | 84 +++++++++++++++ .../Segment/Decorator/DateDecorator.php | 88 +++------------ .../Segment/LeadSegmentFilterFactory.php | 19 ++-- 8 files changed, 324 insertions(+), 143 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 41993d0dd24..3fedc83af39 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -809,13 +809,12 @@ 'mautic.lead.model.lead_segment_filter_factory' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, 'arguments' => [ - 'mautic.lead.model.lead_segment_filter_date', 'doctrine.orm.entity_manager', '@service_container', 'mautic.lead.repository.lead_segment_filter_descriptor', 'mautic.lead.model.lead_segment_decorator_base', 'mautic.lead.model.lead_segment_decorator_custom_mapped', - 'mautic.lead.model.lead_segment_decorator_date', + 'mautic.lead.model.lead_segment.decorator.date.dateFactory', ], ], 'mautic.lead.model.relative_date' => [ @@ -857,12 +856,20 @@ 'arguments' => [ 'mautic.lead.model.lead_segment_filter_operator', 'mautic.lead.repository.lead_segment_filter_descriptor', - 'mautic.lead.model.relative_date', - 'mautic.lead.model.lead_segment.decorator.date.dateFactory', ], ], 'mautic.lead.model.lead_segment.decorator.date.dateFactory' => [ 'class' => \Mautic\LeadBundle\Segment\Decorator\Date\DateFactory::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment.decorator.date.optionFactory', + 'mautic.lead.model.relative_date', + ], + ], + 'mautic.lead.model.lead_segment.decorator.date.optionFactory' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::class, + 'arguments' => [ + 'mautic.lead.model.lead_segment_decorator_date', + ], ], 'mautic.lead.model.random_parameter_name' => [ 'class' => \Mautic\LeadBundle\Segment\RandomParameterName::class, diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php index 1a30e71e7f2..54d26ee48f5 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php @@ -11,8 +11,22 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateAnniversary implements DateOptionsInterface +use Mautic\LeadBundle\Segment\Decorator\DateDecorator; +use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; + +class DateAnniversary implements DateOptionsInterface, FilterDecoratorInterface { + /** + * @var DateDecorator + */ + private $dateDecorator; + + public function __construct(DateDecorator $dateDecorator) + { + $this->dateDecorator = $dateDecorator; + } + /** * @return string */ @@ -20,4 +34,39 @@ public function getDateValue() { return '%'.date('-m-d'); } + + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getField($leadSegmentFilterCrate); + } + + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getTable($leadSegmentFilterCrate); + } + + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return 'like'; + } + + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + { + return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + } + + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->getDateValue(); + } + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php index b8c14652eef..3410ccc7ce3 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php @@ -11,26 +11,30 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateDefault implements DateOptionsInterface +use Mautic\LeadBundle\Segment\Decorator\DateDecorator; +use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; + +class DateDefault implements DateOptionsInterface, FilterDecoratorInterface { /** - * @var string + * @var DateDecorator */ - private $originalValue; + private $dateDecorator; /** * @var string */ - private $requiresBetween; + private $originalValue; /** - * @param string $originalValue - * @param string $requiresBetween + * @param DateDecorator $dateDecorator + * @param string $originalValue */ - public function __construct($originalValue, $requiresBetween) + public function __construct(DateDecorator $dateDecorator, $originalValue) { + $this->dateDecorator = $dateDecorator; $this->originalValue = $originalValue; - $this->requiresBetween = $requiresBetween; } /** @@ -38,6 +42,41 @@ public function __construct($originalValue, $requiresBetween) */ public function getDateValue() { - return $this->requiresBetween ? [$this->originalValue, $this->originalValue] : $this->originalValue; + return $this->originalValue; + } + + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getField($leadSegmentFilterCrate); + } + + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getTable($leadSegmentFilterCrate); + } + + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getOperator($leadSegmentFilterCrate); + } + + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + { + return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + } + + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->getDateValue(); + } + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php index 5fd98c5a533..278be81834d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php @@ -11,53 +11,71 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -use Mautic\CoreBundle\Helper\DateTimeHelper; +use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; +use Mautic\LeadBundle\Segment\RelativeDate; class DateFactory { /** - * @param string $originalValue - * @param string $timeframe - * @param bool $requiresBetween - * @param bool $includeMidnigh - * @param bool $isTimestamp + * @var DateOptionFactory + */ + private $dateOptionFactory; + + /** + * @var RelativeDate + */ + private $relativeDate; + + public function __construct( + DateOptionFactory $dateOptionFactory, + RelativeDate $relativeDate + ) { + $this->dateOptionFactory = $dateOptionFactory; + $this->relativeDate = $relativeDate; + } + + /** + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate * - * @return DateOptionsInterface + * @return FilterDecoratorInterface */ - public function getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp) + public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalValue = $leadSegmentFilterCrate->getFilter(); + $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); + $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); + $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); + $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); + + return $this->dateOptionFactory->getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp); + } + + private function requiresBetween(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $dtHelper = new DateTimeHelper('midnight today', null, 'local'); - - switch ($timeframe) { - case 'birthday': - case 'anniversary': - return new DateAnniversary(); - case 'today': - return new DateDayToday($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'tomorrow': - return new DateDayTomorrow($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'yesterday': - return new DateDayYesterday($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'week_last': - return new DateWeekLast($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'week_next': - return new DateWeekNext($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'week_this': - return new DateWeekThis($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'month_last': - return new DateMonthLast($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'month_next': - return new DateMonthNext($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'month_this': - return new DateMonthThis($dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); - case 'year_last': - return new DateYearLast(); - case 'year_next': - return new DateYearNext(); - case 'year_this': - return new DateYearThis(); - default: - return new DateDefault($originalValue, $requiresBetween); - } + return in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); + } + + private function shouldIncludeMidnight(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); + } + + private function isTimestamp(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getType() === 'datetime'; + } + + /** + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * + * @return string + */ + private function getTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); + $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); + + return str_replace('mautic.lead.list.', '', $key); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index de2106f9586..80fcb3e68d4 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -12,9 +12,17 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; use Mautic\CoreBundle\Helper\DateTimeHelper; +use Mautic\LeadBundle\Segment\Decorator\DateDecorator; +use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; -abstract class DateOptionAbstract +abstract class DateOptionAbstract implements FilterDecoratorInterface { + /** + * @var DateDecorator + */ + protected $dateDecorator; + /** * @var DateTimeHelper */ @@ -36,13 +44,15 @@ abstract class DateOptionAbstract private $isTimestamp; /** + * @param DateDecorator $dateDecorator * @param DateTimeHelper $dateTimeHelper * @param bool $requiresBetween * @param bool $includeMidnigh * @param bool $isTimestamp */ - public function __construct(DateTimeHelper $dateTimeHelper, $requiresBetween, $includeMidnigh, $isTimestamp) + public function __construct(DateDecorator $dateDecorator, DateTimeHelper $dateTimeHelper, $requiresBetween, $includeMidnigh, $isTimestamp) { + $this->dateDecorator = $dateDecorator; $this->dateTimeHelper = $dateTimeHelper; $this->requiresBetween = $requiresBetween; $this->includeMidnigh = $includeMidnigh; @@ -89,4 +99,43 @@ abstract protected function modifyBaseDate(); * @return string */ abstract protected function getModifierForBetweenRange(); + + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getField($leadSegmentFilterCrate); + } + + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getTable($leadSegmentFilterCrate); + } + + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + if ($this->requiresBetween) { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; + } + + return $this->dateDecorator->getOperator($leadSegmentFilterCrate); + } + + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + { + return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + } + + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->getDateValue(); + } + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php new file mode 100644 index 00000000000..6ed556d695d --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -0,0 +1,84 @@ +dateDecorator = $dateDecorator; + } + + /** + * @param string $originalValue + * @param string $timeframe + * @param bool $requiresBetween + * @param bool $includeMidnigh + * @param bool $isTimestamp + * + * @return FilterDecoratorInterface + */ + public function getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp) + { + $dtHelper = new DateTimeHelper('midnight today', null, 'local'); + + switch ($timeframe) { + case 'birthday': + case 'anniversary': + return new DateAnniversary($this->dateDecorator); + case 'today': + //LIKE 2018-01-23% + return new DateDayToday($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'tomorrow': + //LIKE 2018-01-24% + return new DateDayTomorrow($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'yesterday': + //LIKE 2018-01-22% + return new DateDayYesterday($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'week_last': + return new DateWeekLast($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'week_next': + return new DateWeekNext($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'week_this': + return new DateWeekThis($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'month_last': + //LIKE 2017-12-% + return new DateMonthLast($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'month_next': + //LIKE 2018-02-% + return new DateMonthNext($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'month_this': + //LIKE 2018-01-% + return new DateMonthThis($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + case 'year_last': + //LIKE 2017-% + return new DateYearLast(); + //LIKE 2019-% + case 'year_next': + return new DateYearNext(); + case 'year_this': + //LIKE 2018-% + return new DateYearThis(); + default: + return new DateDefault($this->dateDecorator, $originalValue); + } + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index 30aad3e6510..9324a194995 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -11,10 +11,8 @@ namespace Mautic\LeadBundle\Segment\Decorator; -use Mautic\LeadBundle\Segment\Decorator\Date\DateFactory; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\LeadSegmentFilterOperator; -use Mautic\LeadBundle\Segment\RelativeDate; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; class DateDecorator extends BaseDecorator @@ -24,87 +22,31 @@ class DateDecorator extends BaseDecorator */ private $leadSegmentFilterDescriptor; - /** - * @var RelativeDate - */ - private $relativeDate; - - /** - * @var DateFactory - */ - private $dateFactory; - public function __construct( LeadSegmentFilterOperator $leadSegmentFilterOperator, - LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, - RelativeDate $relativeDate, - DateFactory $dateFactory + LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor ) { parent::__construct($leadSegmentFilterOperator); $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; - $this->relativeDate = $relativeDate; - $this->dateFactory = $dateFactory; } - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - if ($this->isAnniversary($leadSegmentFilterCrate)) { - return 'like'; - } - - if ($this->requiresBetween($leadSegmentFilterCrate)) { - return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; + /* + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + if ($this->isAnniversary($leadSegmentFilterCrate)) { + return 'like'; + } + + if ($this->requiresBetween($leadSegmentFilterCrate)) { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; + } + + return parent::getOperator($leadSegmentFilterCrate); } - - return parent::getOperator($leadSegmentFilterCrate); - } - + */ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $originalValue = $leadSegmentFilterCrate->getFilter(); - $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); - $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); - $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); - $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); - - $date = $this->dateFactory->getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp); - - return $date->getDateValue(); - } - - private function isAnniversary(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); - - return $timeframe === 'anniversary' || $timeframe === 'birthday'; - } - - private function requiresBetween(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); - } - - private function shouldIncludeMidnight(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return in_array($this->getOperator($leadSegmentFilterCrate), ['gt', 'lte'], true); - } - - private function isTimestamp(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return $leadSegmentFilterCrate->getType() === 'datetime'; - } - - /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * - * @return string - */ - private function getTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); - $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); - - return str_replace('mautic.lead.list.', '', $key); + throw new \Exception(); } public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 7fe39c88ab7..51c27e69d1f 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -15,18 +15,13 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; -use Mautic\LeadBundle\Segment\Decorator\DateDecorator; +use Mautic\LeadBundle\Segment\Decorator\Date\DateFactory; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; use Symfony\Component\DependencyInjection\Container; class LeadSegmentFilterFactory { - /** - * @var LeadSegmentFilterDate - */ - private $leadSegmentFilterDate; - /** * @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ @@ -53,26 +48,24 @@ class LeadSegmentFilterFactory private $customMappedDecorator; /** - * @var DateDecorator + * @var DateFactory */ - private $dateDecorator; + private $dateFactory; public function __construct( - LeadSegmentFilterDate $leadSegmentFilterDate, EntityManager $entityManager, Container $container, LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, BaseDecorator $baseDecorator, CustomMappedDecorator $customMappedDecorator, - DateDecorator $dateDecorator + DateFactory $dateFactory ) { - $this->leadSegmentFilterDate = $leadSegmentFilterDate; $this->entityManager = $entityManager; $this->container = $container; $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; $this->baseDecorator = $baseDecorator; $this->customMappedDecorator = $customMappedDecorator; - $this->dateDecorator = $dateDecorator; + $this->dateFactory = $dateFactory; } /** @@ -123,7 +116,7 @@ protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilt { $type = $leadSegmentFilterCrate->getType(); if ($type === 'datetime' || $type === 'date') { - return $this->dateDecorator; + return $this->dateFactory->getDateOption($leadSegmentFilterCrate); } $originalField = $leadSegmentFilterCrate->getField(); From e5d59e7f45227654d06c7d9d2e1e19ff92090e56 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 23 Jan 2018 15:16:24 +0100 Subject: [PATCH 073/778] Remove unnused interface --- .../Decorator/CustomMappedDecorator.php | 2 +- .../Decorator/Date/DateAnniversary.php | 12 +-- .../Segment/Decorator/Date/DateDayToday.php | 2 +- .../Decorator/Date/DateDayTomorrow.php | 2 +- .../Decorator/Date/DateDayYesterday.php | 2 +- .../Segment/Decorator/Date/DateDefault.php | 12 +-- .../Segment/Decorator/Date/DateMonthLast.php | 2 +- .../Segment/Decorator/Date/DateMonthNext.php | 2 +- .../Segment/Decorator/Date/DateMonthThis.php | 2 +- .../Decorator/Date/DateOptionAbstract.php | 48 +++++------- .../Decorator/Date/DateOptionsInterface.php | 20 ----- .../Segment/Decorator/Date/DateWeekLast.php | 2 +- .../Segment/Decorator/Date/DateWeekNext.php | 2 +- .../Segment/Decorator/Date/DateWeekThis.php | 2 +- .../Segment/Decorator/DateDecorator.php | 75 ++----------------- 15 files changed, 39 insertions(+), 148 deletions(-) delete mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php index 4d3c620eefe..2516fa42eab 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -20,7 +20,7 @@ class CustomMappedDecorator extends BaseDecorator /** * @var LeadSegmentFilterDescriptor */ - private $leadSegmentFilterDescriptor; + protected $leadSegmentFilterDescriptor; public function __construct( LeadSegmentFilterOperator $leadSegmentFilterOperator, diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php index 54d26ee48f5..4cadcaec685 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php @@ -15,7 +15,7 @@ use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; -class DateAnniversary implements DateOptionsInterface, FilterDecoratorInterface +class DateAnniversary implements FilterDecoratorInterface { /** * @var DateDecorator @@ -27,14 +27,6 @@ public function __construct(DateDecorator $dateDecorator) $this->dateDecorator = $dateDecorator; } - /** - * @return string - */ - public function getDateValue() - { - return '%'.date('-m-d'); - } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getField($leadSegmentFilterCrate); @@ -57,7 +49,7 @@ public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrat public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return $this->getDateValue(); + return '%'.date('-m-d'); } public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php index 7c2cdb10d44..bb4a44d8c92 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateDayToday extends DateOptionAbstract implements DateOptionsInterface +class DateDayToday extends DateOptionAbstract { /** * {@inheritdoc} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php index a228a507d40..0c14a521400 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateDayTomorrow extends DateOptionAbstract implements DateOptionsInterface +class DateDayTomorrow extends DateOptionAbstract { /** * {@inheritdoc} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php index 3f66bf35f7e..05652685903 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateDayYesterday extends DateOptionAbstract implements DateOptionsInterface +class DateDayYesterday extends DateOptionAbstract { /** * {@inheritdoc} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php index 3410ccc7ce3..ffe120f30a8 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php @@ -15,7 +15,7 @@ use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; -class DateDefault implements DateOptionsInterface, FilterDecoratorInterface +class DateDefault implements FilterDecoratorInterface { /** * @var DateDecorator @@ -37,14 +37,6 @@ public function __construct(DateDecorator $dateDecorator, $originalValue) $this->originalValue = $originalValue; } - /** - * @return string|array - */ - public function getDateValue() - { - return $this->originalValue; - } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getField($leadSegmentFilterCrate); @@ -67,7 +59,7 @@ public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrat public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return $this->getDateValue(); + return $this->originalValue; } public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php index d24f6f84f1f..6bb97e93c3a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateMonthLast extends DateOptionAbstract implements DateOptionsInterface +class DateMonthLast extends DateOptionAbstract { /** * {@inheritdoc} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php index e7da0488925..79541fa0476 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateMonthNext extends DateOptionAbstract implements DateOptionsInterface +class DateMonthNext extends DateOptionAbstract { /** * {@inheritdoc} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php index 7c533cbde6a..8fc1e4fdd6e 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -class DateMonthThis extends DateOptionAbstract implements DateOptionsInterface +class DateMonthThis extends DateOptionAbstract { /** * {@inheritdoc} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 80fcb3e68d4..44dd51a6850 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -59,33 +59,6 @@ public function __construct(DateDecorator $dateDecorator, DateTimeHelper $dateTi $this->isTimestamp = $isTimestamp; } - /** - * @return array|string - */ - public function getDateValue() - { - $this->modifyBaseDate(); - - $modifier = $this->getModifierForBetweenRange(); - $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; - - if ($this->requiresBetween) { - $startWith = $this->dateTimeHelper->toUtcString($dateFormat); - - $this->dateTimeHelper->modify($modifier); - $endWith = $this->dateTimeHelper->toUtcString($dateFormat); - - return [$startWith, $endWith]; - } - - if ($this->includeMidnigh) { - $modifier .= ' -1 second'; - $this->dateTimeHelper->modify($modifier); - } - - return $this->dateTimeHelper->toUtcString($dateFormat); - } - /** * This function is responsible for setting date. $this->dateTimeHelper holds date with midnight today. * Eg. +1 day for "tomorrow", -1 for yesterday etc. @@ -126,7 +99,26 @@ public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrat public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - return $this->getDateValue(); + $this->modifyBaseDate(); + + $modifier = $this->getModifierForBetweenRange(); + $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; + + if ($this->requiresBetween) { + $startWith = $this->dateTimeHelper->toUtcString($dateFormat); + + $this->dateTimeHelper->modify($modifier); + $endWith = $this->dateTimeHelper->toUtcString($dateFormat); + + return [$startWith, $endWith]; + } + + if ($this->includeMidnigh) { + $modifier .= ' -1 second'; + $this->dateTimeHelper->modify($modifier); + } + + return $this->dateTimeHelper->toUtcString($dateFormat); } public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php deleted file mode 100644 index 8ec155e6a0b..00000000000 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionsInterface.php +++ /dev/null @@ -1,20 +0,0 @@ -leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; - } - - /* - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - if ($this->isAnniversary($leadSegmentFilterCrate)) { - return 'like'; - } - - if ($this->requiresBetween($leadSegmentFilterCrate)) { - return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; - } - - return parent::getOperator($leadSegmentFilterCrate); - } - */ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - throw new \Exception(); - } - - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $originalField = $leadSegmentFilterCrate->getField(); - - if (empty($this->leadSegmentFilterDescriptor[$originalField]['field'])) { - return parent::getField($leadSegmentFilterCrate); - } - - return $this->leadSegmentFilterDescriptor[$originalField]['field']; - } - - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $originalField = $leadSegmentFilterCrate->getField(); - - if (empty($this->leadSegmentFilterDescriptor[$originalField]['foreign_table'])) { - return parent::getTable($leadSegmentFilterCrate); - } - - return $this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; - } - - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $originalField = $leadSegmentFilterCrate->getField(); - - if (!isset($this->leadSegmentFilterDescriptor[$originalField]['type'])) { - return parent::getQueryType($leadSegmentFilterCrate); - } - - return $this->leadSegmentFilterDescriptor[$originalField]['type']; - } - - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $originalField = $leadSegmentFilterCrate->getField(); - - return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? - $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; + throw new \Exception('Instance of Date option need to implement this function'); } } From d11400727fbd3b24f0e68537db04b8fd8e337319 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 23 Jan 2018 16:13:29 +0100 Subject: [PATCH 074/778] Implement date operators as like where possible --- .../Decorator/Date/DateOptionAbstract.php | 28 ++++++++--- .../Decorator/Date/DateOptionFactory.php | 23 +++++---- .../Decorator/Date/Day/DateDayAbstract.php | 42 ++++++++++++++++ .../Decorator/Date/{ => Day}/DateDayToday.php | 11 +---- .../Date/{ => Day}/DateDayTomorrow.php | 12 +---- .../Date/{ => Day}/DateDayYesterday.php | 12 +---- .../Date/Month/DateMonthAbstract.php | 42 ++++++++++++++++ .../Date/{ => Month}/DateMonthLast.php | 12 +---- .../Date/{ => Month}/DateMonthNext.php | 12 +---- .../Date/{ => Month}/DateMonthThis.php | 12 +---- .../Date/{ => Other}/DateAnniversary.php | 2 +- .../Date/{ => Other}/DateDefault.php | 2 +- .../Decorator/Date/Week/DateWeekAbstract.php | 49 +++++++++++++++++++ .../Date/{ => Week}/DateWeekLast.php | 12 +---- .../Date/{ => Week}/DateWeekNext.php | 12 +---- .../Date/{ => Week}/DateWeekThis.php | 12 +---- .../Date/{ => Year}/DateYearLast.php | 2 +- .../Date/{ => Year}/DateYearNext.php | 2 +- .../Date/{ => Year}/DateYearThis.php | 2 +- 19 files changed, 190 insertions(+), 111 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Day}/DateDayToday.php (57%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Day}/DateDayTomorrow.php (60%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Day}/DateDayYesterday.php (60%) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Month}/DateMonthLast.php (62%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Month}/DateMonthNext.php (62%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Month}/DateMonthThis.php (62%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Other}/DateAnniversary.php (96%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Other}/DateDefault.php (97%) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Week}/DateWeekLast.php (62%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Week}/DateWeekNext.php (62%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Week}/DateWeekThis.php (62%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Year}/DateYearLast.php (80%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Year}/DateYearNext.php (80%) rename app/bundles/LeadBundle/Segment/Decorator/Date/{ => Year}/DateYearThis.php (80%) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 44dd51a6850..3a4437f5d8c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -41,7 +41,7 @@ abstract class DateOptionAbstract implements FilterDecoratorInterface /** * @var bool */ - private $isTimestamp; + protected $isTimestamp; /** * @param DateDecorator $dateDecorator @@ -73,6 +73,23 @@ abstract protected function modifyBaseDate(); */ abstract protected function getModifierForBetweenRange(); + /** + * This function returns a value if between range is needed. Could return string for like operator or array for between operator + * Eg. //LIKE 2018-01-23% for today, //LIKE 2017-12-% for last month, //LIKE 2017-% for last year, array for this week. + * + * @return string|array + */ + abstract protected function getValueForBetweenRange(); + + /** + * This function returns an operator if between range is needed. Could return like or between. + * + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * + * @return string + */ + abstract protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate); + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getField($leadSegmentFilterCrate); @@ -86,7 +103,7 @@ public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) { if ($this->requiresBetween) { - return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; + return $this->getOperatorForBetweenRange($leadSegmentFilterCrate); } return $this->dateDecorator->getOperator($leadSegmentFilterCrate); @@ -105,12 +122,7 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; if ($this->requiresBetween) { - $startWith = $this->dateTimeHelper->toUtcString($dateFormat); - - $this->dateTimeHelper->modify($modifier); - $endWith = $this->dateTimeHelper->toUtcString($dateFormat); - - return [$startWith, $endWith]; + return $this->getValueForBetweenRange(); } if ($this->includeMidnigh) { diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index 6ed556d695d..424f8da879d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -12,6 +12,20 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; use Mautic\CoreBundle\Helper\DateTimeHelper; +use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday; +use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow; +use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday; +use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthLast; +use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthNext; +use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis; +use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateAnniversary; +use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateDefault; +use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast; +use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext; +use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis; +use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearLast; +use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearNext; +use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; @@ -45,13 +59,10 @@ public function getDate($originalValue, $timeframe, $requiresBetween, $includeMi case 'anniversary': return new DateAnniversary($this->dateDecorator); case 'today': - //LIKE 2018-01-23% return new DateDayToday($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'tomorrow': - //LIKE 2018-01-24% return new DateDayTomorrow($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'yesterday': - //LIKE 2018-01-22% return new DateDayYesterday($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'week_last': return new DateWeekLast($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); @@ -60,22 +71,16 @@ public function getDate($originalValue, $timeframe, $requiresBetween, $includeMi case 'week_this': return new DateWeekThis($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_last': - //LIKE 2017-12-% return new DateMonthLast($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_next': - //LIKE 2018-02-% return new DateMonthNext($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'month_this': - //LIKE 2018-01-% return new DateMonthThis($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); case 'year_last': - //LIKE 2017-% return new DateYearLast(); - //LIKE 2019-% case 'year_next': return new DateYearNext(); case 'year_this': - //LIKE 2018-% return new DateYearThis(); default: return new DateDefault($this->dateDecorator, $originalValue); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php new file mode 100644 index 00000000000..41015610007 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php @@ -0,0 +1,42 @@ +dateTimeHelper->toUtcString('Y-m-d%'); + } + + /** + * {@inheritdoc} + */ + protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notLike' : 'like'; + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php similarity index 57% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php index bb4a44d8c92..3c3ef194f36 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayToday.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php @@ -9,22 +9,15 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; -class DateDayToday extends DateOptionAbstract +class DateDayToday extends DateDayAbstract { /** * {@inheritdoc} */ protected function modifyBaseDate() { - } - /** - * {@inheritdoc} - */ - protected function getModifierForBetweenRange() - { - return '+1 day'; } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php similarity index 60% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php index 0c14a521400..fd45b1b3fa8 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayTomorrow.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; -class DateDayTomorrow extends DateOptionAbstract +class DateDayTomorrow extends DateDayAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->modify('+1 day'); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 day'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php similarity index 60% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php index 05652685903..222c35aa2e7 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDayYesterday.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; -class DateDayYesterday extends DateOptionAbstract +class DateDayYesterday extends DateDayAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->modify('-1 day'); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 day'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php new file mode 100644 index 00000000000..354f4cbdcaa --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php @@ -0,0 +1,42 @@ +dateTimeHelper->toUtcString('Y-m-%'); + } + + /** + * {@inheritdoc} + */ + protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notLike' : 'like'; + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php similarity index 62% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php index 6bb97e93c3a..40e468e5dbc 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; -class DateMonthLast extends DateOptionAbstract +class DateMonthLast extends DateMonthAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->setDateTime('midnight first day of last month', null); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 month'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php similarity index 62% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php index 79541fa0476..81eb3797d80 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; -class DateMonthNext extends DateOptionAbstract +class DateMonthNext extends DateMonthAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->setDateTime('midnight first day of next month', null); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 month'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php similarity index 62% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php index 8fc1e4fdd6e..52d2e60bf9d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateMonthThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; -class DateMonthThis extends DateOptionAbstract +class DateMonthThis extends DateMonthAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->setDateTime('midnight first day of this month', null); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 month'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php similarity index 96% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index 4cadcaec685..78fce183643 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php similarity index 97% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php index ffe120f30a8..45b41ec3448 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php new file mode 100644 index 00000000000..ba32096af0b --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php @@ -0,0 +1,49 @@ +isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; + $startWith = $this->dateTimeHelper->toUtcString($dateFormat); + + $modifier = $this->getModifierForBetweenRange(); + $this->dateTimeHelper->modify($modifier); + $endWith = $this->dateTimeHelper->toUtcString($dateFormat); + + return [$startWith, $endWith]; + } + + /** + * {@inheritdoc} + */ + protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php similarity index 62% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php index 259e1e33f40..382fe955e31 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; -class DateWeekLast extends DateOptionAbstract +class DateWeekLast extends DateWeekAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->setDateTime('midnight monday last week', null); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 week'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php similarity index 62% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php index 4e5bbffa858..c24be66925f 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; -class DateWeekNext extends DateOptionAbstract +class DateWeekNext extends DateWeekAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->setDateTime('midnight monday next week', null); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 week'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php similarity index 62% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php index 3a399050689..1c74cc70f79 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateWeekThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php @@ -9,9 +9,9 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; -class DateWeekThis extends DateOptionAbstract +class DateWeekThis extends DateWeekAbstract { /** * {@inheritdoc} @@ -20,12 +20,4 @@ protected function modifyBaseDate() { $this->dateTimeHelper->setDateTime('midnight monday this week', null); } - - /** - * @return string - */ - protected function getModifierForBetweenRange() - { - return '+1 week'; - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateYearLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php similarity index 80% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateYearLast.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php index 5a636e6239d..30f6406fb9a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateYearLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; class DateYearLast { diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateYearNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php similarity index 80% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateYearNext.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php index eb2f09a4f8b..e9b710f9326 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateYearNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; class DateYearNext { diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateYearThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php similarity index 80% rename from app/bundles/LeadBundle/Segment/Decorator/Date/DateYearThis.php rename to app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php index bb44a341652..c4ed83ec4b8 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateYearThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Segment\Decorator\Date; +namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; class DateYearThis { From e134930457ceb3f5b926824eb13bd9535f0ee242 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 23 Jan 2018 16:24:11 +0100 Subject: [PATCH 075/778] Fix values for between operator - Do not include first second of next day --- .../LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php index ba32096af0b..548ba062d72 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php @@ -32,7 +32,7 @@ protected function getValueForBetweenRange() $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; $startWith = $this->dateTimeHelper->toUtcString($dateFormat); - $modifier = $this->getModifierForBetweenRange(); + $modifier = $this->getModifierForBetweenRange().' -1 second'; $this->dateTimeHelper->modify($modifier); $endWith = $this->dateTimeHelper->toUtcString($dateFormat); From ea95487aec96dd48bba90fc3254d13edfb08ff41 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 23 Jan 2018 16:38:21 +0100 Subject: [PATCH 076/778] Remove unnecessary date factory --- app/bundles/LeadBundle/Config/config.php | 10 +-- .../Segment/Decorator/Date/DateFactory.php | 81 ------------------- .../Decorator/Date/DateOptionFactory.php | 56 +++++++++++-- .../Segment/LeadSegmentFilterFactory.php | 12 +-- 4 files changed, 56 insertions(+), 103 deletions(-) delete mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 3fedc83af39..d90fe85d701 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -814,7 +814,7 @@ 'mautic.lead.repository.lead_segment_filter_descriptor', 'mautic.lead.model.lead_segment_decorator_base', 'mautic.lead.model.lead_segment_decorator_custom_mapped', - 'mautic.lead.model.lead_segment.decorator.date.dateFactory', + 'mautic.lead.model.lead_segment.decorator.date.optionFactory', ], ], 'mautic.lead.model.relative_date' => [ @@ -858,17 +858,11 @@ 'mautic.lead.repository.lead_segment_filter_descriptor', ], ], - 'mautic.lead.model.lead_segment.decorator.date.dateFactory' => [ - 'class' => \Mautic\LeadBundle\Segment\Decorator\Date\DateFactory::class, - 'arguments' => [ - 'mautic.lead.model.lead_segment.decorator.date.optionFactory', - 'mautic.lead.model.relative_date', - ], - ], 'mautic.lead.model.lead_segment.decorator.date.optionFactory' => [ 'class' => \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::class, 'arguments' => [ 'mautic.lead.model.lead_segment_decorator_date', + 'mautic.lead.model.relative_date', ], ], 'mautic.lead.model.random_parameter_name' => [ diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php deleted file mode 100644 index 278be81834d..00000000000 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateFactory.php +++ /dev/null @@ -1,81 +0,0 @@ -dateOptionFactory = $dateOptionFactory; - $this->relativeDate = $relativeDate; - } - - /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * - * @return FilterDecoratorInterface - */ - public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $originalValue = $leadSegmentFilterCrate->getFilter(); - $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); - $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); - $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); - $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); - - return $this->dateOptionFactory->getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp); - } - - private function requiresBetween(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); - } - - private function shouldIncludeMidnight(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); - } - - private function isTimestamp(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return $leadSegmentFilterCrate->getType() === 'datetime'; - } - - /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * - * @return string - */ - private function getTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); - $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); - - return str_replace('mautic.lead.list.', '', $key); - } -} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index 424f8da879d..e73d419da1c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -28,6 +28,8 @@ use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; +use Mautic\LeadBundle\Segment\RelativeDate; class DateOptionFactory { @@ -36,22 +38,32 @@ class DateOptionFactory */ private $dateDecorator; - public function __construct(DateDecorator $dateDecorator) - { + /** + * @var RelativeDate + */ + private $relativeDate; + + public function __construct( + DateDecorator $dateDecorator, + RelativeDate $relativeDate + ) { $this->dateDecorator = $dateDecorator; + $this->relativeDate = $relativeDate; } /** - * @param string $originalValue - * @param string $timeframe - * @param bool $requiresBetween - * @param bool $includeMidnigh - * @param bool $isTimestamp + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate * * @return FilterDecoratorInterface */ - public function getDate($originalValue, $timeframe, $requiresBetween, $includeMidnigh, $isTimestamp) + public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) { + $originalValue = $leadSegmentFilterCrate->getFilter(); + $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); + $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); + $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); + $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); + $dtHelper = new DateTimeHelper('midnight today', null, 'local'); switch ($timeframe) { @@ -86,4 +98,32 @@ public function getDate($originalValue, $timeframe, $requiresBetween, $includeMi return new DateDefault($this->dateDecorator, $originalValue); } } + + private function requiresBetween(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); + } + + private function shouldIncludeMidnight(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); + } + + private function isTimestamp(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getType() === 'datetime'; + } + + /** + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * + * @return string + */ + private function getTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); + $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); + + return str_replace('mautic.lead.list.', '', $key); + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 51c27e69d1f..c29f37e055b 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -15,7 +15,7 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; -use Mautic\LeadBundle\Segment\Decorator\Date\DateFactory; +use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; use Symfony\Component\DependencyInjection\Container; @@ -48,9 +48,9 @@ class LeadSegmentFilterFactory private $customMappedDecorator; /** - * @var DateFactory + * @var DateOptionFactory */ - private $dateFactory; + private $dateOptionFactory; public function __construct( EntityManager $entityManager, @@ -58,14 +58,14 @@ public function __construct( LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, BaseDecorator $baseDecorator, CustomMappedDecorator $customMappedDecorator, - DateFactory $dateFactory + DateOptionFactory $dateOptionFactory ) { $this->entityManager = $entityManager; $this->container = $container; $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; $this->baseDecorator = $baseDecorator; $this->customMappedDecorator = $customMappedDecorator; - $this->dateFactory = $dateFactory; + $this->dateOptionFactory = $dateOptionFactory; } /** @@ -116,7 +116,7 @@ protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilt { $type = $leadSegmentFilterCrate->getType(); if ($type === 'datetime' || $type === 'date') { - return $this->dateFactory->getDateOption($leadSegmentFilterCrate); + return $this->dateOptionFactory->getDateOption($leadSegmentFilterCrate); } $originalField = $leadSegmentFilterCrate->getField(); From 1aa8330ea54c8fd4be3421def583e0ce0d78719b Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 23 Jan 2018 16:39:09 +0100 Subject: [PATCH 077/778] premerge commit --- .../Command/CheckQueryBuildersCommand.php | 4 ++-- app/bundles/LeadBundle/Model/ListModel.php | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 13d5447def5..4f9b1befb78 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -12,6 +12,7 @@ namespace Mautic\LeadBundle\Command; use Mautic\CoreBundle\Command\ModeratedCommand; +use Mautic\LeadBundle\Model\ListModel; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -22,7 +23,6 @@ protected function configure() { $this ->setName('mautic:segments:check-builders') - ->setAliases(['mautic:segments:check-query-builders']) ->setDescription('Compare output of query builders for given segments') ->addOption('--segment-id', '-i', InputOption::VALUE_OPTIONAL, 'Set the ID of segment to process') ; @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } - private function runSegment($output, $verbose, $l, $listModel) + private function runSegment($output, $verbose, $l, ListModel $listModel) { $output->writeln('Running segment '.$l->getId().'...'); $output->writeln(''); diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index e760432e726..62518c37cf1 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -797,6 +797,15 @@ public function getVersionOld(LeadList $entity) return $return; } + public function updateLeadList(LeadList $leadList) + { + } + + public function rebuildListLeads() + { + throw new \Exception('Deprecated function, use updateLeadList instead'); + } + /** * Rebuild lead lists. * @@ -805,9 +814,11 @@ public function getVersionOld(LeadList $entity) * @param bool $maxLeads * @param OutputInterface $output * + * @throws \Exception + * * @return int */ - public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = false, OutputInterface $output = null) + public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = false, OutputInterface $output = null) { defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1); From e9d67c1656220e3445a2a0459ae1fa6e0711c8d4 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 23 Jan 2018 18:02:11 +0100 Subject: [PATCH 078/778] Dates - refactor parameters --- .../Decorator/Date/DateOptionAbstract.php | 40 +++----- .../Decorator/Date/DateOptionFactory.php | 56 +++-------- .../Decorator/Date/DateOptionParameters.php | 94 +++++++++++++++++++ .../Decorator/Date/Week/DateWeekAbstract.php | 2 +- .../Segment/LeadSegmentFilterCrate.php | 10 ++ .../Segment/LeadSegmentFilterFactory.php | 3 +- 6 files changed, 132 insertions(+), 73 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 3a4437f5d8c..30ee0407771 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -29,34 +29,20 @@ abstract class DateOptionAbstract implements FilterDecoratorInterface protected $dateTimeHelper; /** - * @var bool + * @var DateOptionParameters */ - private $requiresBetween; + protected $dateOptionParameters; /** - * @var bool + * @param DateDecorator $dateDecorator + * @param DateTimeHelper $dateTimeHelper + * @param DateOptionParameters $dateOptionParameters */ - private $includeMidnigh; - - /** - * @var bool - */ - protected $isTimestamp; - - /** - * @param DateDecorator $dateDecorator - * @param DateTimeHelper $dateTimeHelper - * @param bool $requiresBetween - * @param bool $includeMidnigh - * @param bool $isTimestamp - */ - public function __construct(DateDecorator $dateDecorator, DateTimeHelper $dateTimeHelper, $requiresBetween, $includeMidnigh, $isTimestamp) + public function __construct(DateDecorator $dateDecorator, DateTimeHelper $dateTimeHelper, DateOptionParameters $dateOptionParameters) { - $this->dateDecorator = $dateDecorator; - $this->dateTimeHelper = $dateTimeHelper; - $this->requiresBetween = $requiresBetween; - $this->includeMidnigh = $includeMidnigh; - $this->isTimestamp = $isTimestamp; + $this->dateDecorator = $dateDecorator; + $this->dateTimeHelper = $dateTimeHelper; + $this->dateOptionParameters = $dateOptionParameters; } /** @@ -102,7 +88,7 @@ public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - if ($this->requiresBetween) { + if ($this->dateOptionParameters->isBetweenRequired()) { return $this->getOperatorForBetweenRange($leadSegmentFilterCrate); } @@ -119,13 +105,13 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate $this->modifyBaseDate(); $modifier = $this->getModifierForBetweenRange(); - $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; + $dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d'; - if ($this->requiresBetween) { + if ($this->dateOptionParameters->isBetweenRequired()) { return $this->getValueForBetweenRange(); } - if ($this->includeMidnigh) { + if ($this->dateOptionParameters->shouldIncludeMidnigh()) { $modifier .= ' -1 second'; $this->dateTimeHelper->modify($modifier); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index e73d419da1c..f0411961998 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -58,36 +58,34 @@ public function __construct( */ public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $originalValue = $leadSegmentFilterCrate->getFilter(); - $isTimestamp = $this->isTimestamp($leadSegmentFilterCrate); - $timeframe = $this->getTimeFrame($leadSegmentFilterCrate); - $requiresBetween = $this->requiresBetween($leadSegmentFilterCrate); - $includeMidnigh = $this->shouldIncludeMidnight($leadSegmentFilterCrate); + $originalValue = $leadSegmentFilterCrate->getFilter(); + $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); + $dateOptionParameters = new DateOptionParameters($leadSegmentFilterCrate, $relativeDateStrings); $dtHelper = new DateTimeHelper('midnight today', null, 'local'); - switch ($timeframe) { + switch ($dateOptionParameters->getTimeframe()) { case 'birthday': case 'anniversary': return new DateAnniversary($this->dateDecorator); case 'today': - return new DateDayToday($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateDayToday($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'tomorrow': - return new DateDayTomorrow($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateDayTomorrow($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'yesterday': - return new DateDayYesterday($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateDayYesterday($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'week_last': - return new DateWeekLast($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateWeekLast($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'week_next': - return new DateWeekNext($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateWeekNext($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'week_this': - return new DateWeekThis($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateWeekThis($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'month_last': - return new DateMonthLast($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateMonthLast($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'month_next': - return new DateMonthNext($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateMonthNext($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'month_this': - return new DateMonthThis($this->dateDecorator, $dtHelper, $requiresBetween, $includeMidnigh, $isTimestamp); + return new DateMonthThis($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'year_last': return new DateYearLast(); case 'year_next': @@ -98,32 +96,4 @@ public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) return new DateDefault($this->dateDecorator, $originalValue); } } - - private function requiresBetween(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); - } - - private function shouldIncludeMidnight(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); - } - - private function isTimestamp(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - return $leadSegmentFilterCrate->getType() === 'datetime'; - } - - /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * - * @return string - */ - private function getTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); - $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); - - return str_replace('mautic.lead.list.', '', $key); - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php new file mode 100644 index 00000000000..acf14f99cba --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php @@ -0,0 +1,94 @@ +hasTimePart = $leadSegmentFilterCrate->hasTimeParts(); + $this->timeframe = $this->parseTimeFrame($leadSegmentFilterCrate, $relativeDateStrings); + $this->requiresBetween = in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); + $this->includeMidnigh = in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); + } + + /** + * @return bool + */ + public function hasTimePart() + { + return $this->hasTimePart; + } + + /** + * @return string + */ + public function getTimeframe() + { + return $this->timeframe; + } + + /** + * @return bool + */ + public function isBetweenRequired() + { + return $this->requiresBetween; + } + + /** + * @return bool + */ + public function shouldIncludeMidnigh() + { + return $this->includeMidnigh; + } + + /** + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * @param array $relativeDateStrings + * + * @return string + */ + private function parseTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) + { + $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); + + return str_replace('mautic.lead.list.', '', $key); + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php index 548ba062d72..8693b117313 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php @@ -29,7 +29,7 @@ protected function getModifierForBetweenRange() */ protected function getValueForBetweenRange() { - $dateFormat = $this->isTimestamp ? 'Y-m-d H:i:s' : 'Y-m-d'; + $dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d'; $startWith = $this->dateTimeHelper->toUtcString($dateFormat); $modifier = $this->getModifierForBetweenRange().' -1 second'; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php index 4643b1f40a6..92cc21f42b1 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php @@ -147,4 +147,14 @@ public function getFunc() { return $this->func; } + + public function isDateType() + { + return $this->getType() === 'date' || $this->hasTimeParts(); + } + + public function hasTimeParts() + { + return $this->getType() === 'datetime'; + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index c29f37e055b..e21bd969c28 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -114,8 +114,7 @@ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) */ protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $type = $leadSegmentFilterCrate->getType(); - if ($type === 'datetime' || $type === 'date') { + if ($leadSegmentFilterCrate->isDateType()) { return $this->dateOptionFactory->getDateOption($leadSegmentFilterCrate); } From 6d08c608044aed21d646e8bcf9425cb5318bbaea Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 23 Jan 2018 20:18:48 +0100 Subject: [PATCH 079/778] stashing changes, actually i would forget the stash --- app/bundles/LeadBundle/Model/ListModel.php | 233 +++++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 62518c37cf1..5153d8fcb71 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -799,6 +799,239 @@ public function getVersionOld(LeadList $entity) public function updateLeadList(LeadList $leadList) { + defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1); + + $id = $entity->getId(); + $list = ['id' => $id, 'filters' => $entity->getFilters()]; + $dtHelper = new DateTimeHelper(); + $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); + + $batchLimiters = [ + 'dateTime' => $dtHelper->toUtcString(), + ]; + + $localDateTime = $dtHelper->getLocalDateTime(); + + $this->dispatcher->dispatch( + LeadEvents::LIST_PRE_PROCESS_LIST, + new ListPreProcessListEvent($list, false) + ); + + // Get a count of leads to add + $newLeadsCount = $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); + + dump($newLeadsCount); + echo '
Original result:'; + + $versionStart = microtime(true); + + // Get a count of leads to add + $newLeadsCount = $this->getLeadsByList( + $list, + true, + [ + 'countOnly' => true, + 'newOnly' => true, + 'batchLimiters' => $batchLimiters, + ] + ); + $versionEnd = microtime(true) - $versionStart; + dump('Total query assembly took:'.(1000 * $versionEnd).'ms'); + + dump(array_shift($newLeadsCount)); + exit; + // Ensure the same list is used each batch + $batchLimiters['maxId'] = (int) $newLeadsCount[$id]['maxId']; + + // Number of total leads to process + $leadCount = (int) $newLeadsCount[$id]['count']; + + if ($output) { + $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_added', ['%leads%' => $leadCount, '%batch%' => $limit])); + } + + // Handle by batches + $start = $lastRoundPercentage = $leadsProcessed = 0; + + // Try to save some memory + gc_enable(); + + if ($leadCount) { + $maxCount = ($maxLeads) ? $maxLeads : $leadCount; + + if ($output) { + $progress = ProgressBarHelper::init($output, $maxCount); + $progress->start(); + } + + // Add leads + while ($start < $leadCount) { + // Keep CPU down for large lists; sleep per $limit batch + $this->batchSleep(); + + $newLeadList = $this->getLeadsByList( + $list, + true, + [ + 'newOnly' => true, + // No start set because of newOnly thus always at 0 + 'limit' => $limit, + 'batchLimiters' => $batchLimiters, + ] + ); + + if (empty($newLeadList[$id])) { + // Somehow ran out of leads so break out + break; + } + + $processedLeads = []; + foreach ($newLeadList[$id] as $l) { + $this->addLead($l, $entity, false, true, -1, $localDateTime); + $processedLeads[] = $l; + unset($l); + + ++$leadsProcessed; + if ($output && $leadsProcessed < $maxCount) { + $progress->setProgress($leadsProcessed); + } + + if ($maxLeads && $leadsProcessed >= $maxLeads) { + break; + } + } + + $start += $limit; + + // Dispatch batch event + if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { + $this->dispatcher->dispatch( + LeadEvents::LEAD_LIST_BATCH_CHANGE, + new ListChangeEvent($processedLeads, $entity, true) + ); + } + + unset($newLeadList); + + // Free some memory + gc_collect_cycles(); + + if ($maxLeads && $leadsProcessed >= $maxLeads) { + if ($output) { + $progress->finish(); + $output->writeln(''); + } + + return $leadsProcessed; + } + } + + if ($output) { + $progress->finish(); + $output->writeln(''); + } + } + + // Unset max ID to prevent capping at newly added max ID + unset($batchLimiters['maxId']); + + // Get a count of leads to be removed + $removeLeadCount = $this->getLeadsByList( + $list, + true, + [ + 'countOnly' => true, + 'nonMembersOnly' => true, + 'batchLimiters' => $batchLimiters, + ] + ); + + // Ensure the same list is used each batch + $batchLimiters['maxId'] = (int) $removeLeadCount[$id]['maxId']; + + // Restart batching + $start = $lastRoundPercentage = 0; + $leadCount = $removeLeadCount[$id]['count']; + + if ($output) { + $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_removed', ['%leads%' => $leadCount, '%batch%' => $limit])); + } + + if ($leadCount) { + $maxCount = ($maxLeads) ? $maxLeads : $leadCount; + + if ($output) { + $progress = ProgressBarHelper::init($output, $maxCount); + $progress->start(); + } + + // Remove leads + while ($start < $leadCount) { + // Keep CPU down for large lists; sleep per $limit batch + $this->batchSleep(); + + $removeLeadList = $this->getLeadsByList( + $list, + true, + [ + // No start because the items are deleted so always 0 + 'limit' => $limit, + 'nonMembersOnly' => true, + 'batchLimiters' => $batchLimiters, + ] + ); + + if (empty($removeLeadList[$id])) { + // Somehow ran out of leads so break out + break; + } + + $processedLeads = []; + foreach ($removeLeadList[$id] as $l) { + $this->removeLead($l, $entity, false, true, true); + $processedLeads[] = $l; + ++$leadsProcessed; + if ($output && $leadsProcessed < $maxCount) { + $progress->setProgress($leadsProcessed); + } + + if ($maxLeads && $leadsProcessed >= $maxLeads) { + break; + } + } + + // Dispatch batch event + if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { + $this->dispatcher->dispatch( + LeadEvents::LEAD_LIST_BATCH_CHANGE, + new ListChangeEvent($processedLeads, $entity, false) + ); + } + + $start += $limit; + + unset($removeLeadList); + + // Free some memory + gc_collect_cycles(); + + if ($maxLeads && $leadsProcessed >= $maxLeads) { + if ($output) { + $progress->finish(); + $output->writeln(''); + } + + return $leadsProcessed; + } + } + + if ($output) { + $progress->finish(); + $output->writeln(''); + } + } + + return $leadsProcessed; } public function rebuildListLeads() From 21fedc28aefd2e1c24f0f530064c4856347b0cb6 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 24 Jan 2018 13:05:02 +0100 Subject: [PATCH 080/778] Date decorator - handles year --- .../Decorator/Date/DateOptionFactory.php | 6 +-- .../Decorator/Date/Year/DateYearAbstract.php | 42 +++++++++++++++++++ .../Decorator/Date/Year/DateYearLast.php | 9 +++- .../Decorator/Date/Year/DateYearNext.php | 9 +++- .../Decorator/Date/Year/DateYearThis.php | 9 +++- 5 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index f0411961998..da02d087f2c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -87,11 +87,11 @@ public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) case 'month_this': return new DateMonthThis($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'year_last': - return new DateYearLast(); + return new DateYearLast($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'year_next': - return new DateYearNext(); + return new DateYearNext($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'year_this': - return new DateYearThis(); + return new DateYearThis($this->dateDecorator, $dtHelper, $dateOptionParameters); default: return new DateDefault($this->dateDecorator, $originalValue); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php new file mode 100644 index 00000000000..83d0b6da7bc --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php @@ -0,0 +1,42 @@ +dateTimeHelper->toUtcString('Y-%'); + } + + /** + * {@inheritdoc} + */ + protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notLike' : 'like'; + } +} diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php index 30f6406fb9a..7f332b897de 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php @@ -11,6 +11,13 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; -class DateYearLast +class DateYearLast extends DateYearAbstract { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight first day of last year', null); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php index e9b710f9326..134c51b8d5a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php @@ -11,6 +11,13 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; -class DateYearNext +class DateYearNext extends DateYearAbstract { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight first day of next year', null); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php index c4ed83ec4b8..11ccffa4bd5 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php @@ -11,6 +11,13 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; -class DateYearThis +class DateYearThis extends DateYearAbstract { + /** + * {@inheritdoc} + */ + protected function modifyBaseDate() + { + $this->dateTimeHelper->setDateTime('midnight first day of this year', null); + } } From 32cc080141ca670d6703ebf7c583bccc6a46aea5 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 24 Jan 2018 15:28:24 +0100 Subject: [PATCH 081/778] Dates - handle relative intervals like +1 day, -1 year etc. --- .../Decorator/Date/DateOptionFactory.php | 6 +- .../Decorator/Date/DateOptionParameters.php | 5 ++ .../Date/Other/DateRelativeInterval.php | 90 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index da02d087f2c..054dd3eadc3 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -20,6 +20,7 @@ use Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis; use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateAnniversary; use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateDefault; +use Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext; use Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis; @@ -64,7 +65,8 @@ public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) $dtHelper = new DateTimeHelper('midnight today', null, 'local'); - switch ($dateOptionParameters->getTimeframe()) { + $timeframe = $dateOptionParameters->getTimeframe(); + switch ($timeframe) { case 'birthday': case 'anniversary': return new DateAnniversary($this->dateDecorator); @@ -92,6 +94,8 @@ public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) return new DateYearNext($this->dateDecorator, $dtHelper, $dateOptionParameters); case 'year_this': return new DateYearThis($this->dateDecorator, $dtHelper, $dateOptionParameters); + case $timeframe && (false !== strpos($timeframe[0], '-') || false !== strpos($timeframe[0], '+')): + return new DateRelativeInterval($this->dateDecorator, $originalValue); default: return new DateDefault($this->dateDecorator, $originalValue); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php index acf14f99cba..7bac94f7efb 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php @@ -89,6 +89,11 @@ private function parseTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate, { $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); + if ($key === false) { + // Time frame does not match any option from $relativeDateStrings, so return original value + return $leadSegmentFilterCrate->getFilter(); + } + return str_replace('mautic.lead.list.', '', $key); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php new file mode 100644 index 00000000000..06a69e5c911 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php @@ -0,0 +1,90 @@ +dateDecorator = $dateDecorator; + $this->originalValue = $originalValue; + } + + public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getField($leadSegmentFilterCrate); + } + + public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getTable($leadSegmentFilterCrate); + } + + public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + if ($leadSegmentFilterCrate->getOperator() === '=') { + return 'like'; + } + if ($leadSegmentFilterCrate->getOperator() === '!=') { + return 'notLike'; + } + + return $this->dateDecorator->getOperator($leadSegmentFilterCrate); + } + + public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + { + return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + } + + public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $date = new \DateTime('now'); + $date->modify($this->originalValue); + + $operator = $this->getOperator($leadSegmentFilterCrate); + $format = 'Y-m-d'; + if ($operator === 'like' || $operator === 'notLike') { + $format .= '%'; + } + + return $date->format($format); + } + + public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + } + + public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + } +} From 62c6cfeff3e891b8fa574294328f9bfa937ca28b Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 24 Jan 2018 15:33:12 +0100 Subject: [PATCH 082/778] Remove Filter date class - refactored to Date decorator --- app/bundles/LeadBundle/Config/config.php | 6 - .../Segment/LeadSegmentFilterDate.php | 232 ------------------ 2 files changed, 238 deletions(-) delete mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index d90fe85d701..6247b62b00c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -823,12 +823,6 @@ 'translator', ], ], - 'mautic.lead.model.lead_segment_filter_date' => [ - 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterDate::class, - 'arguments' => [ - 'mautic.lead.model.relative_date', - ], - ], 'mautic.lead.model.lead_segment_filter_operator' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterOperator::class, 'arguments' => [ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php deleted file mode 100644 index 61eb335c121..00000000000 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterDate.php +++ /dev/null @@ -1,232 +0,0 @@ -relativeDate = $relativeDate; - } - - public function fixDateOptions(LeadSegmentFilter $leadSegmentFilter) - { - $type = $leadSegmentFilter->getType(); - if ($type !== 'datetime' && $type !== 'date') { - return; - } - - if (is_array($leadSegmentFilter->getFilter())) { - foreach ($leadSegmentFilter->getFilter() as $filterValue) { - $this->getDate($filterValue, $leadSegmentFilter); - } - } else { - $this->getDate($leadSegmentFilter->getFilter(), $leadSegmentFilter); - } - } - - private function getDate($string, LeadSegmentFilter $leadSegmentFilter) - { - $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); - - // Check if the column type is a date/time stamp - $isTimestamp = $leadSegmentFilter->getType() === 'datetime'; - - $key = array_search($string, $relativeDateStrings, true); - $dtHelper = new DateTimeHelper('midnight today', null, 'local'); - $requiresBetween = in_array($leadSegmentFilter->getFunc(), ['eq', 'neq'], true) && $isTimestamp; - $timeframe = str_replace('mautic.lead.list.', '', $key); - $modifier = false; - $isRelative = true; - - switch ($timeframe) { - case 'birthday': - case 'anniversary': - $isRelative = false; - $leadSegmentFilter->setOperator('like'); - $leadSegmentFilter->setFilter('%'.date('-m-d')); - break; - case 'today': - case 'tomorrow': - case 'yesterday': - if ($timeframe === 'yesterday') { - $dtHelper->modify('-1 day'); - } elseif ($timeframe === 'tomorrow') { - $dtHelper->modify('+1 day'); - } - - // Today = 2015-08-28 00:00:00 - if ($requiresBetween) { - // eq: - // field >= 2015-08-28 00:00:00 - // field < 2015-08-29 00:00:00 - - // neq: - // field < 2015-08-28 00:00:00 - // field >= 2015-08-29 00:00:00 - $modifier = '+1 day'; - } else { - // lt: - // field < 2015-08-28 00:00:00 - // gt: - // field > 2015-08-28 23:59:59 - - // lte: - // field <= 2015-08-28 23:59:59 - // gte: - // field >= 2015-08-28 00:00:00 - if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { - $modifier = '+1 day -1 second'; - } - } - break; - case 'week_last': - case 'week_next': - case 'week_this': - $interval = str_replace('week_', '', $timeframe); - $dtHelper->setDateTime('midnight monday '.$interval.' week', null); - - // This week: Monday 2015-08-24 00:00:00 - if ($requiresBetween) { - // eq: - // field >= Mon 2015-08-24 00:00:00 - // field < Mon 2015-08-31 00:00:00 - - // neq: - // field < Mon 2015-08-24 00:00:00 - // field >= Mon 2015-08-31 00:00:00 - $modifier = '+1 week'; - } else { - // lt: - // field < Mon 2015-08-24 00:00:00 - // gt: - // field > Sun 2015-08-30 23:59:59 - - // lte: - // field <= Sun 2015-08-30 23:59:59 - // gte: - // field >= Mon 2015-08-24 00:00:00 - if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { - $modifier = '+1 week -1 second'; - } - } - break; - - case 'month_last': - case 'month_next': - case 'month_this': - $interval = substr($key, -4); - $dtHelper->setDateTime('midnight first day of '.$interval.' month', null); - - // This month: 2015-08-01 00:00:00 - if ($requiresBetween) { - // eq: - // field >= 2015-08-01 00:00:00 - // field < 2015-09:01 00:00:00 - - // neq: - // field < 2015-08-01 00:00:00 - // field >= 2016-09-01 00:00:00 - $modifier = '+1 month'; - } else { - // lt: - // field < 2015-08-01 00:00:00 - // gt: - // field > 2015-08-31 23:59:59 - - // lte: - // field <= 2015-08-31 23:59:59 - // gte: - // field >= 2015-08-01 00:00:00 - if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { - $modifier = '+1 month -1 second'; - } - } - break; - case 'year_last': - case 'year_next': - case 'year_this': - $interval = substr($key, -4); - $dtHelper->setDateTime('midnight first day of '.$interval.' year', null); - - // This year: 2015-01-01 00:00:00 - if ($requiresBetween) { - // eq: - // field >= 2015-01-01 00:00:00 - // field < 2016-01-01 00:00:00 - - // neq: - // field < 2015-01-01 00:00:00 - // field >= 2016-01-01 00:00:00 - $modifier = '+1 year'; - } else { - // lt: - // field < 2015-01-01 00:00:00 - // gt: - // field > 2015-12-31 23:59:59 - - // lte: - // field <= 2015-12-31 23:59:59 - // gte: - // field >= 2015-01-01 00:00:00 - if (in_array($leadSegmentFilter->getFunc(), ['gt', 'lte'], true)) { - $modifier = '+1 year -1 second'; - } - } - break; - default: - $isRelative = false; - break; - } - - // check does this match php date params pattern? - if ($timeframe !== 'anniversary' && - (stristr($string[0], '-') || stristr($string[0], '+'))) { - $date = new \DateTime('now'); - $date->modify($string); - - $dateTime = $date->format('Y-m-d H:i:s'); - $dtHelper->setDateTime($dateTime, null); - - $isRelative = true; - } - - if ($isRelative) { - if ($requiresBetween) { - $startWith = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); - - $dtHelper->modify($modifier); - $endWith = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); - - // Use a between statement - $func = ($leadSegmentFilter->getFunc() === 'neq') ? 'notBetween' : 'between'; - $leadSegmentFilter->setFunc($func); - - $leadSegmentFilter->setFilter([$startWith, $endWith]); - } else { - if ($modifier) { - $dtHelper->modify($modifier); - } - - $filter = $isTimestamp ? $dtHelper->toUtcString('Y-m-d H:i:s') : $dtHelper->toUtcString('Y-m-d'); - $leadSegmentFilter->setFilter($filter); - } - } - } -} From 121e4ef61613a71018a4ded7ef6947c3804f2794 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 24 Jan 2018 15:36:33 +0100 Subject: [PATCH 083/778] premerge --- app/bundles/LeadBundle/Config/config.php | 1 + .../Segment/Exception/SegmentQueryException.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index d90fe85d701..6503f5f2c29 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -804,6 +804,7 @@ 'mautic.lead.model.lead_segment_filter_factory', 'mautic.lead.repository.lead_list_segment_repository', 'mautic.lead.repository.lead_segment_query_builder', + 'monolog.logger.mautic', ], ], 'mautic.lead.model.lead_segment_filter_factory' => [ diff --git a/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php b/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php new file mode 100644 index 00000000000..fc1aac451b8 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php @@ -0,0 +1,16 @@ + Date: Wed, 24 Jan 2018 15:47:48 +0100 Subject: [PATCH 084/778] lead list new leads processing refactoring to use new methods implement query exception and use it ignore non-existing columns as they are supposedly removed custom fields --- app/AppKernel.php | 10 + .../Command/CheckQueryBuildersCommand.php | 28 +-- .../Command/UpdateLeadListsCommand.php | 2 +- .../LeadBundle/Controller/ListController.php | 4 +- .../LeadBundle/Entity/LeadListRepository.php | 18 +- app/bundles/LeadBundle/Model/ListModel.php | 118 ++++++----- .../LeadBundle/Segment/LeadSegmentFilter.php | 5 +- .../LeadBundle/Segment/LeadSegmentService.php | 183 ++++++++++++++---- .../Query/Expression/ExpressionBuilder.php | 9 +- .../Query/Filter/BaseFilterQueryBuilder.php | 9 +- .../Filter/ForeignFuncFilterQueryBuilder.php | 7 +- .../Segment/Query/LeadSegmentQueryBuilder.php | 6 + 12 files changed, 256 insertions(+), 143 deletions(-) diff --git a/app/AppKernel.php b/app/AppKernel.php index d3d92d8b4f1..c81edddab2e 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -558,4 +558,14 @@ protected function buildContainer() return $container; } + + public function init() + { + if ($this->debug) { + error_reporting(E_ALL & E_NOTICE & ~E_STRICT & ~E_DEPRECATED); + } else { + ini_set('display_errors', 0); + } + parent::init(); // TODO: Change the autogenerated stub + } } diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 4f9b1befb78..dee3f5141bc 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -69,32 +69,25 @@ protected function execute(InputInterface $input, OutputInterface $output) private function runSegment($output, $verbose, $l, ListModel $listModel) { - $output->writeln('Running segment '.$l->getId().'...'); - $output->writeln(''); + $output->write('Running segment '.$l->getId().'...'); - if (!$verbose) { - ob_start(); - } - - $output->writeln('old:'); $timer1 = microtime(true); $processed = $listModel->getVersionOld($l); $timer1 = round((microtime(true) - $timer1) * 1000, 3); - $output->writeln('new:'); $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); $timer2 = round((microtime(true) - $timer2) * 1000, 3); - $output->writeln(''); + $processed2 = array_shift($processed2); - if ($processed['count'] != $processed2['count'] or $processed['maxId'] != $processed2['maxId']) { + if ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))) { $output->write(''); } else { $output->write(''); } - $output->writeln( + $output->write( sprintf('old: c: %d, m: %d, time: %dms <--> new: c: %d, m: %s, time: %dms', $processed['count'], $processed['maxId'], @@ -105,17 +98,10 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) ) ); - if ($processed['count'] != $processed2['count'] or $processed['maxId'] != $processed2['maxId']) { - $output->write(''); + if ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))) { + $output->writeln(''); } else { - $output->write(''); - } - - $output->writeln(''); - $output->writeln('-------------------------------------------------------------------------'); - - if (!$verbose) { - ob_clean(); + $output->writeln(''); } } } diff --git a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php index 3445140a5a5..dc9557bd0dd 100644 --- a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php +++ b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php @@ -57,7 +57,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $list = $listModel->getEntity($id); if ($list !== null) { $output->writeln(''.$translator->trans('mautic.lead.list.rebuild.rebuilding', ['%id%' => $id]).''); - $processed = $listModel->rebuildListLeads($list, $batch, $max, $output); + $processed = $listModel->updateLeadList($list, $batch, $max, $output); $output->writeln( ''.$translator->trans('mautic.lead.list.rebuild.leads_affected', ['%leads%' => $processed]).'' ); diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 2cd1e0cccb9..2a1a2f0094b 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -563,8 +563,8 @@ public function viewAction($objectId) $list = $listModel->getEntity($objectId); //dump($list); - $processed = $listModel->rebuildListLeads($list, 2, 2); - dump($processed); + $processed = $listModel->updateLeadList($list); + var_dump($processed); exit; /** @var \Mautic\LeadBundle\Model\ListModel $model */ diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index ebe4b7defeb..070bba5394b 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -312,9 +312,13 @@ public function getLeadCount($listIds) } /** + * This function is weird, you should not use it, use LeadSegmentService instead. + * * @param $lists * @param array $args * + * @deprecated + * * @return array */ public function getLeadsByList($lists, $args = []) @@ -507,21 +511,7 @@ public function getLeadsByList($lists, $args = []) $q->resetQueryPart('groupBy'); } - // Debug output - //dump($q->getSQL()); - $sql = $q->getSQL(); - foreach ($q->getParameters() as $k=>$v) { - $value = is_array($v) ? implode(', ', $v) : $v; - $sql = str_replace(":$k", "'$value'", $sql); - } - - echo '
'; - dump($sql); - - $start = microtime(true); $results = $q->execute()->fetchAll(); - $end = microtime(true) - $start; - dump('Query took '.(1000 * $end).'ms'); foreach ($results as $r) { if ($countOnly) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 5153d8fcb71..a2d35847053 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -50,7 +50,7 @@ class ListModel extends FormModel /** * @var LeadSegmentService */ - private $leadSegment; + private $leadSegmentService; /** * ListModel constructor. @@ -60,7 +60,7 @@ class ListModel extends FormModel public function __construct(CoreParametersHelper $coreParametersHelper, LeadSegmentService $leadSegment) { $this->coreParametersHelper = $coreParametersHelper; - $this->leadSegment = $leadSegment; + $this->leadSegmentService = $leadSegment; } /** @@ -759,6 +759,13 @@ public function getGlobalLists() return $lists; } + /** + * @param LeadList $entity + * + * @return array + * + * @throws \Exception + */ public function getVersionNew(LeadList $entity) { $id = $entity->getId(); @@ -769,9 +776,14 @@ public function getVersionNew(LeadList $entity) 'dateTime' => $dtHelper->toUtcString(), ]; - return $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); + return $this->leadSegmentService->getNewLeadListLeadsCount($entity, $batchLimiters); } + /** + * @param LeadList $entity + * + * @return mixed + */ public function getVersionOld(LeadList $entity) { $id = $entity->getId(); @@ -797,54 +809,41 @@ public function getVersionOld(LeadList $entity) return $return; } - public function updateLeadList(LeadList $leadList) + /** + * @param LeadList $leadList + * @param int $limit + * @param bool $maxLeads + * @param OutputInterface|null $output + * + * @return int + * + * @throws \Doctrine\ORM\ORMException + * @throws \Exception + */ + public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = false, OutputInterface $output = null) { defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1); - $id = $entity->getId(); - $list = ['id' => $id, 'filters' => $entity->getFilters()]; $dtHelper = new DateTimeHelper(); - $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); - $batchLimiters = [ - 'dateTime' => $dtHelper->toUtcString(), - ]; + $batchLimiters = ['dateTime' => $dtHelper->toUtcString()]; + $list = ['id' => $leadList->getId(), 'filters' => $leadList->getFilters()]; - $localDateTime = $dtHelper->getLocalDateTime(); + //@todo remove this debug line + $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); $this->dispatcher->dispatch( - LeadEvents::LIST_PRE_PROCESS_LIST, - new ListPreProcessListEvent($list, false) + LeadEvents::LIST_PRE_PROCESS_LIST, new ListPreProcessListEvent($list, false) ); // Get a count of leads to add - $newLeadsCount = $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); - - dump($newLeadsCount); - echo '
Original result:'; - - $versionStart = microtime(true); - - // Get a count of leads to add - $newLeadsCount = $this->getLeadsByList( - $list, - true, - [ - 'countOnly' => true, - 'newOnly' => true, - 'batchLimiters' => $batchLimiters, - ] - ); - $versionEnd = microtime(true) - $versionStart; - dump('Total query assembly took:'.(1000 * $versionEnd).'ms'); + $newLeadsCount = $this->leadSegmentService->getNewLeadListLeadsCount($leadList, $batchLimiters); - dump(array_shift($newLeadsCount)); - exit; // Ensure the same list is used each batch - $batchLimiters['maxId'] = (int) $newLeadsCount[$id]['maxId']; + $batchLimiters['maxId'] = (int) $newLeadsCount[$leadList->getId()]['maxId']; // Number of total leads to process - $leadCount = (int) $newLeadsCount[$id]['count']; + $leadCount = (int) $newLeadsCount[$leadList->getId()]['count']; if ($output) { $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_added', ['%leads%' => $leadCount, '%batch%' => $limit])); @@ -869,25 +868,19 @@ public function updateLeadList(LeadList $leadList) // Keep CPU down for large lists; sleep per $limit batch $this->batchSleep(); - $newLeadList = $this->getLeadsByList( - $list, - true, - [ - 'newOnly' => true, - // No start set because of newOnly thus always at 0 - 'limit' => $limit, - 'batchLimiters' => $batchLimiters, - ] - ); + $newLeadList = $this->leadSegmentService->getNewLeadListLeads($leadList, $batchLimiters, $limit); + dump('Got list:'); + dump($newLeadList); + die(); - if (empty($newLeadList[$id])) { + if (empty($newLeadList[$leadList->getId()])) { // Somehow ran out of leads so break out break; } $processedLeads = []; - foreach ($newLeadList[$id] as $l) { - $this->addLead($l, $entity, false, true, -1, $localDateTime); + foreach ($newLeadList[$leadList->getId()] as $l) { + $this->addLead($l, $leadList, false, true, -1, $dtHelper->getLocalDateTime()); $processedLeads[] = $l; unset($l); @@ -907,7 +900,7 @@ public function updateLeadList(LeadList $leadList) if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { $this->dispatcher->dispatch( LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $entity, true) + new ListChangeEvent($processedLeads, $leadList, true) ); } @@ -932,6 +925,9 @@ public function updateLeadList(LeadList $leadList) } } + dump('added leads'); + die(); + // Unset max ID to prevent capping at newly added max ID unset($batchLimiters['maxId']); @@ -947,7 +943,7 @@ public function updateLeadList(LeadList $leadList) ); // Ensure the same list is used each batch - $batchLimiters['maxId'] = (int) $removeLeadCount[$id]['maxId']; + $batchLimiters['maxId'] = (int) $removeLeadCount[$leadList->getId()]['maxId']; // Restart batching $start = $lastRoundPercentage = 0; @@ -1042,7 +1038,7 @@ public function rebuildListLeads() /** * Rebuild lead lists. * - * @param LeadList $entity + * @param LeadList $leadList * @param int $limit * @param bool $maxLeads * @param OutputInterface $output @@ -1051,12 +1047,12 @@ public function rebuildListLeads() * * @return int */ - public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = false, OutputInterface $output = null) + public function rebuildListLeadsOld(LeadList $leadList, $limit = 1000, $maxLeads = false, OutputInterface $output = null) { defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1); - $id = $entity->getId(); - $list = ['id' => $id, 'filters' => $entity->getFilters()]; + $id = $leadList->getId(); + $list = ['id' => $id, 'filters' => $leadList->getFilters()]; $dtHelper = new DateTimeHelper(); $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); @@ -1068,11 +1064,11 @@ public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = $this->dispatcher->dispatch( LeadEvents::LIST_PRE_PROCESS_LIST, - new ListPreProcessListEvent($list, false) + $list, false ); // Get a count of leads to add - $newLeadsCount = $this->leadSegment->getNewLeadsByListCount($entity, $batchLimiters); + $newLeadsCount = $this->leadSegmentService->getNewLeadsByListCount($leadList, $batchLimiters); dump($newLeadsCount); echo '
Original result:'; @@ -1141,7 +1137,7 @@ public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = $processedLeads = []; foreach ($newLeadList[$id] as $l) { - $this->addLead($l, $entity, false, true, -1, $localDateTime); + $this->addLead($l, $leadList, false, true, -1, $localDateTime); $processedLeads[] = $l; unset($l); @@ -1161,7 +1157,7 @@ public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { $this->dispatcher->dispatch( LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $entity, true) + new ListChangeEvent($processedLeads, $leadList, true) ); } @@ -1242,7 +1238,7 @@ public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = $processedLeads = []; foreach ($removeLeadList[$id] as $l) { - $this->removeLead($l, $entity, false, true, true); + $this->removeLead($l, $leadList, false, true, true); $processedLeads[] = $l; ++$leadsProcessed; if ($output && $leadsProcessed < $maxCount) { @@ -1258,7 +1254,7 @@ public function rebuildListLeadsOld(LeadList $entity, $limit = 1000, $maxLeads = if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { $this->dispatcher->dispatch( LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $entity, false) + new ListChangeEvent($processedLeads, $leadList, false) ); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 47ca2a6f3a1..79ac0824444 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\Query\QueryBuilder; +use Mautic\LeadBundle\Segment\Query\QueryException; class LeadSegmentFilter { @@ -50,13 +51,13 @@ public function __construct( /** * @return \Doctrine\DBAL\Schema\Column * - * @throws \Exception + * @throws QueryException */ public function getColumn() { $columns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getTable()); if (!isset($columns[$this->getField()])) { - throw new \Exception(sprintf('Database schema does not contain field %s.%s', $this->getTable(), $this->getField())); + throw new QueryException(sprintf('Database schema does not contain field %s.%s', $this->getTable(), $this->getField())); } return $columns[$this->getField()]; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 31c756fa272..cef59ae2330 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -11,10 +11,11 @@ namespace Mautic\LeadBundle\Segment; -use Doctrine\DBAL\Query\QueryBuilder; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListSegmentRepository; use Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder; +use Mautic\LeadBundle\Segment\Query\QueryBuilder; +use Symfony\Bridge\Monolog\Logger; class LeadSegmentService { @@ -31,7 +32,17 @@ class LeadSegmentService /** * @var LeadSegmentQueryBuilder */ - private $queryBuilder; + private $leadSegmentQueryBuilder; + + /** + * @var Logger + */ + private $logger; + + /** + * @var QueryBuilder + */ + private $preparedQB; /** * LeadSegmentService constructor. @@ -39,62 +50,164 @@ class LeadSegmentService * @param LeadSegmentFilterFactory $leadSegmentFilterFactory * @param LeadListSegmentRepository $leadListSegmentRepository * @param LeadSegmentQueryBuilder $queryBuilder + * @param Logger $logger */ public function __construct( LeadSegmentFilterFactory $leadSegmentFilterFactory, LeadListSegmentRepository $leadListSegmentRepository, - LeadSegmentQueryBuilder $queryBuilder) - { + LeadSegmentQueryBuilder $queryBuilder, + Logger $logger + ) { $this->leadListSegmentRepository = $leadListSegmentRepository; $this->leadSegmentFilterFactory = $leadSegmentFilterFactory; - $this->queryBuilder = $queryBuilder; + $this->leadSegmentQueryBuilder = $queryBuilder; + $this->logger = $logger; } - public function getNewLeadsByListCount(LeadList $entity, array $batchLimiters) + /** + * @param LeadList $leadList + * @param $segmentFilters + * @param $batchLimiters + * + * @return Query\QueryBuilder|QueryBuilder + */ + private function getNewLeadListLeadsQuery(LeadList $leadList, $segmentFilters, $batchLimiters) { - $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($entity); + if (!is_null($this->preparedQB)) { + return $this->preparedQB; + } + /** @var QueryBuilder $queryBuilder */ + $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); + $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); + $queryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $leadList->getId()); + $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); - $versionStart = microtime(true); + return $queryBuilder; + } + + /** + * @param LeadList $leadList + * @param array $batchLimiters + * + * @return array + * + * @throws \Exception + */ + public function getNewLeadListLeadsCount(LeadList $leadList, array $batchLimiters) + { + $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); if (!count($segmentFilters)) { - return [0]; + $this->logger->debug('Segment QB: Segment has no filters', ['segmentId' => $leadList->getId()]); + + return [$leadList->getId() => [ + 'count' => '0', + 'maxId' => '0', + ], + ]; } - /** @var QueryBuilder $qb */ - $qb = $this->queryBuilder->getLeadsSegmentQueryBuilder($entity->getId(), $segmentFilters); - $qb = $this->queryBuilder->addNewLeadsRestrictions($qb, $entity->getId(), $batchLimiters); - $qb = $this->queryBuilder->addManuallySubscribedQuery($qb, $entity->getId()); - $qb = $this->queryBuilder->addManuallyUnsubsribedQuery($qb, $entity->getId()); - $qb = $this->queryBuilder->wrapInCount($qb); - - $debug = $qb->getDebugOutput(); - - // Debug output - $sql = $qb->getSQL(); - foreach ($qb->getParameters() as $k=>$v) { - $sql = str_replace(":$k", "'$v'", $sql); + + $qb = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); + $qb = $this->leadSegmentQueryBuilder->wrapInCount($qb); + + $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $leadList->getId()]); + + $result = $this->timedFetch($qb, $leadList->getId()); + + return [$leadList->getId() => $result]; + } + + /** + * @param LeadList $leadList + * @param array $batchLimiters + * @param int $limit + * + * @return array + * + * @throws \Exception + */ + public function getNewLeadListLeads(LeadList $leadList, array $batchLimiters, $limit = 1000) + { + $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); + + $qb = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); + $qb->select('l.*'); + + $this->logger->debug('Segment QB: Create Leads SQL: '.$qb->getDebugOutput(), ['segmentId' => $leadList->getId()]); + + $qb->setMaxResults($limit); + + if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { + $qb->andWhere( + $qb->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") + ); + } elseif (!empty($batchLimiters['maxId'])) { + $qb->andWhere( + $qb->expr()->lte('l.id', $batchLimiters['maxId']) + ); } - echo '
'; - dump($sql); - try { - $start = microtime(true); + if (!empty($batchLimiters['dateTime'])) { + // Only leads in the list at the time of count + $qb->andWhere( + $qb->expr()->lte('l.date_added', $qb->expr()->literal($batchLimiters['dateTime'])) + ); + } - $stmt = $qb->execute(); - $results = $stmt->fetchAll(); + $result = $this->timedFetchAll($qb, $leadList->getId()); + + return [$leadList->getId() => $result]; + } + + /** + * @param QueryBuilder $qb + * @param int $segmentId + * + * @return mixed + * + * @throws \Exception + */ + private function timedFetch(QueryBuilder $qb, $segmentId) + { + try { + $start = microtime(true); + $result = $qb->execute()->fetch(\PDO::FETCH_ASSOC); $end = microtime(true) - $start; - dump('Query took '.(1000 * $end).'ms'); - $start = microtime(true); + $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result: '.$qb->getDebugOutput(), ['segmentId' => $segmentId]); + } catch (\Exception $e) { + $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ + 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), + ]); + throw $e; + } + + return $result; + } - $result = $qb->execute()->fetch(); + /** + * @param QueryBuilder $qb + * @param int $segmentId + * + * @return mixed + * + * @throws \Exception + */ + private function timedFetchAll(QueryBuilder $qb, $segmentId) + { + try { + $start = microtime(true); + $result = $qb->execute()->fetchAll(\PDO::FETCH_ASSOC); - $versionEnd = microtime(true) - $versionStart; - dump('Total query assembly took:'.(1000 * $versionEnd).'ms'); + $end = microtime(true) - $start; - dump($result); + $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result: '.$qb->getDebugOutput(), ['segmentId' => $segmentId]); } catch (\Exception $e) { - dump('Query exception: '.$e->getMessage()); + $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ + 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), + ]); + throw $e; } return $result; diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index b2e5b3a0568..d4d42f09f4c 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -20,6 +20,7 @@ namespace Mautic\LeadBundle\Segment\Query\Expression; use Doctrine\DBAL\Connection; +use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; /** * ExpressionBuilder class is responsible to dynamically create SQL query parts. @@ -116,14 +117,14 @@ public function comparison($x, $operator, $y) * @param $x * @param $arr * - * @throws \Exception + * @throws SegmentQueryException * * @return string */ public function between($x, $arr) { if (!is_array($arr) || count($arr) != 2) { - throw new \Exception('Between expression expects second argument to be an array with exactly two elements'); + throw new SegmentQueryException('Between expression expects second argument to be an array with exactly two elements'); } return $x.' '.self::BETWEEN.' '.$this->comparison($arr[0], 'AND', $arr[1]); @@ -142,14 +143,14 @@ public function between($x, $arr) * @param $x * @param $arr * - * @throws \Exception + * @throws SegmentQueryException * * @return string */ public function notBetween($x, $arr) { if (!is_array($arr) || count($arr) != 2) { - throw new \Exception('Not between expression expects second argument to be an array with exactly two elements'); + throw new SegmentQueryException('Not between expression expects second argument to be an array with exactly two elements'); } return 'NOT '.$this->between($x, $arr); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index df483e279de..ba57c38c6bc 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -12,6 +12,7 @@ use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; +use Mautic\LeadBundle\Segment\Query\QueryException; use Mautic\LeadBundle\Segment\RandomParameterName; /** @@ -49,8 +50,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterGlue = $filter->getGlue(); $filterAggr = $filter->getAggregateFunction(); - // Verify the column exists in database, this might be removed after tested - $filter->getColumn(); + try { + $filter->getColumn(); + } catch (QueryException $e) { + // We do ignore not found fields as they may be just removed custom field + return $queryBuilder; + } $filterParameters = $filter->getParameterValue(); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 27411354b56..ae81d8faf1d 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -35,7 +35,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterGlue = $filter->getGlue(); $filterAggr = $filter->getAggregateFunction(); - $filter->getColumn(); + try { + $filter->getColumn(); + } catch (QueryException $e) { + // We do ignore not found fields as they may be just removed custom field + return $queryBuilder; + } $filterParameters = $filter->getParameterValue(); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index f2cbd630dc8..9b5232da507 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -34,6 +34,12 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r $this->schema = $this->entityManager->getConnection()->getSchemaManager(); } + /** + * @param $id + * @param LeadSegmentFilters $leadSegmentFilters + * + * @return QueryBuilder + */ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { /** @var QueryBuilder $queryBuilder */ From f634c2f43a8c83b43e68269941949dde1d713492 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 24 Jan 2018 16:18:39 +0100 Subject: [PATCH 085/778] remove forgotten debug --- app/AppKernel.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/AppKernel.php b/app/AppKernel.php index c81edddab2e..d3d92d8b4f1 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -558,14 +558,4 @@ protected function buildContainer() return $container; } - - public function init() - { - if ($this->debug) { - error_reporting(E_ALL & E_NOTICE & ~E_STRICT & ~E_DEPRECATED); - } else { - ini_set('display_errors', 0); - } - parent::init(); // TODO: Change the autogenerated stub - } } From 6ede0fd54bc4e54a549fa5ac20851a76d578f177 Mon Sep 17 00:00:00 2001 From: Noa83 Date: Thu, 25 Jan 2018 09:30:29 +0100 Subject: [PATCH 086/778] fix validation email exists --- app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php b/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php index 553a863f63f..c55713ea398 100644 --- a/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php +++ b/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php @@ -129,14 +129,15 @@ public static function findCompany(array $parameters, CompanyModel $companyModel */ protected static function domainExists($email) { + if (!strstr($email, '@')){ //not a valid email adress + return false; + } list($user, $domain) = explode('@', $email); $arr = dns_get_record($domain, DNS_MX); if ($arr && $arr[0]['host'] === $domain) { return $domain; } - - return false; } /** From 1a0b9ec0bf88d8e870019bc35dd2a3dba336ea39 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 25 Jan 2018 17:00:32 +0100 Subject: [PATCH 087/778] create segment processing task using new queries add table schema caching service replace multiple queries to schema with service above add new leads to list processing add orphan leads queries fix incorrect logic operator freeze doctrine-bundle to 1.6.x in composer --- app/bundles/LeadBundle/Config/config.php | 8 +- .../LeadBundle/Controller/ListController.php | 5 +- app/bundles/LeadBundle/Model/ListModel.php | 45 ++++------ .../LeadBundle/Segment/LeadSegmentFilter.php | 11 ++- .../Segment/LeadSegmentFilterFactory.php | 12 ++- .../LeadBundle/Segment/LeadSegmentService.php | 88 ++++++++++++++++--- .../Segment/Query/LeadSegmentQueryBuilder.php | 2 +- .../Segment/TableSchemaColumnsCache.php | 65 ++++++++++++++ composer.json | 2 +- 9 files changed, 182 insertions(+), 56 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 4fd00f282dc..a281b2c6ee6 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -810,7 +810,7 @@ 'mautic.lead.model.lead_segment_filter_factory' => [ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, 'arguments' => [ - 'doctrine.orm.entity_manager', + 'mautic.lead.model.lead_segment_schema_cache', '@service_container', 'mautic.lead.repository.lead_segment_filter_descriptor', 'mautic.lead.model.lead_segment_decorator_base', @@ -818,6 +818,12 @@ 'mautic.lead.model.lead_segment.decorator.date.optionFactory', ], ], + 'mautic.lead.model.lead_segment_schema_cache' => [ + 'class' => \Mautic\LeadBundle\Segment\TableSchemaColumnsCache::class, + 'arguments' => [ + 'doctrine.orm.entity_manager', + ], + ], 'mautic.lead.model.relative_date' => [ 'class' => \Mautic\LeadBundle\Segment\RelativeDate::class, 'arguments' => [ diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 2a1a2f0094b..9496b3cf969 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -561,11 +561,8 @@ public function viewAction($objectId) /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ $listModel = $this->get('mautic.lead.model.list'); - $list = $listModel->getEntity($objectId); - //dump($list); + $list = $listModel->getEntity($objectId); $processed = $listModel->updateLeadList($list); - var_dump($processed); - exit; /** @var \Mautic\LeadBundle\Model\ListModel $model */ $model = $this->getModel('lead.list'); diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index a2d35847053..12754cefc57 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -845,10 +845,13 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal // Number of total leads to process $leadCount = (int) $newLeadsCount[$leadList->getId()]['count']; + $this->logger->info('Segment QB - No new leads for segment found'); + if ($output) { $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_added', ['%leads%' => $leadCount, '%batch%' => $limit])); } + dump('To add: '.$leadCount); // Handle by batches $start = $lastRoundPercentage = $leadsProcessed = 0; @@ -868,21 +871,19 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal // Keep CPU down for large lists; sleep per $limit batch $this->batchSleep(); + $this->logger->debug(sprintf('Segment QB - Fetching new leads for segment [%d] %s', $leadList->getId(), $leadList->getName())); $newLeadList = $this->leadSegmentService->getNewLeadListLeads($leadList, $batchLimiters, $limit); - dump('Got list:'); - dump($newLeadList); - die(); if (empty($newLeadList[$leadList->getId()])) { // Somehow ran out of leads so break out break; } - $processedLeads = []; + $this->logger->debug(sprintf('Segment QB - Adding %d new leads to segment [%d] %s', count($newLeadList), $leadList->getId(), $leadList->getName())); foreach ($newLeadList[$leadList->getId()] as $l) { + $this->logger->debug(sprintf('Segment QB - Adding lead #%s to segment [%d] %s', $l->getId()), $leadList->getId(), $leadList->getName()); + $this->addLead($l, $leadList, false, true, -1, $dtHelper->getLocalDateTime()); - $processedLeads[] = $l; - unset($l); ++$leadsProcessed; if ($output && $leadsProcessed < $maxCount) { @@ -894,13 +895,15 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal } } + $this->logger->info(sprintf('Segment QB - Added %d new leads to segment [%d] %s', count($newLeadList), $leadList->getId(), $leadList->getName())); + $start += $limit; // Dispatch batch event - if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { + if (count($newLeadList[$leadList->getId()]) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { $this->dispatcher->dispatch( LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $leadList, true) + new ListChangeEvent($newLeadList[$leadList->getId()], $leadList, true) ); } @@ -925,29 +928,19 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal } } - dump('added leads'); - die(); - // Unset max ID to prevent capping at newly added max ID unset($batchLimiters['maxId']); - // Get a count of leads to be removed - $removeLeadCount = $this->getLeadsByList( - $list, - true, - [ - 'countOnly' => true, - 'nonMembersOnly' => true, - 'batchLimiters' => $batchLimiters, - ] - ); + $orphanLeadsCount = $this->leadSegmentService->getOrphanedLeadListLeadsCount($leadList); // Ensure the same list is used each batch - $batchLimiters['maxId'] = (int) $removeLeadCount[$leadList->getId()]['maxId']; + $batchLimiters['maxId'] = (int) $orphanLeadsCount[$leadList->getId()]['maxId']; // Restart batching $start = $lastRoundPercentage = 0; - $leadCount = $removeLeadCount[$id]['count']; + $leadCount = $orphanLeadsCount[$leadList->getId()]['count']; + + dump('To remove: '.$leadCount); if ($output) { $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_removed', ['%leads%' => $leadCount, '%batch%' => $limit])); @@ -977,14 +970,14 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal ] ); - if (empty($removeLeadList[$id])) { + if (empty($removeLeadList[$leadList->getId()])) { // Somehow ran out of leads so break out break; } $processedLeads = []; - foreach ($removeLeadList[$id] as $l) { - $this->removeLead($l, $entity, false, true, true); + foreach ($removeLeadList[$leadList->getId()] as $l) { + $this->removeLead($l, $leadList, false, true, true); $processedLeads[] = $l; ++$leadsProcessed; if ($output && $leadsProcessed < $maxCount) { diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 79ac0824444..2449880efc8 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment; -use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; @@ -34,18 +33,18 @@ class LeadSegmentFilter private $filterQueryBuilder; /** - * @var EntityManager + * @var TableSchemaColumnsCache */ - private $em; + private $schemaCache; public function __construct( LeadSegmentFilterCrate $leadSegmentFilterCrate, FilterDecoratorInterface $filterDecorator, - EntityManager $em = null + TableSchemaColumnsCache $cache ) { $this->leadSegmentFilterCrate = $leadSegmentFilterCrate; $this->filterDecorator = $filterDecorator; - $this->em = $em; + $this->schemaCache = $cache; } /** @@ -55,7 +54,7 @@ public function __construct( */ public function getColumn() { - $columns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getTable()); + $columns = $this->schemaCache->getColumns($this->getTable()); if (!isset($columns[$this->getField()])) { throw new QueryException(sprintf('Database schema does not contain field %s.%s', $this->getTable(), $this->getField())); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index e21bd969c28..88cecd528f7 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment; -use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; @@ -52,15 +51,20 @@ class LeadSegmentFilterFactory */ private $dateOptionFactory; + /** + * @var TableSchemaColumnsCache + */ + private $schemaCache; + public function __construct( - EntityManager $entityManager, + TableSchemaColumnsCache $schemaCache, Container $container, LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, BaseDecorator $baseDecorator, CustomMappedDecorator $customMappedDecorator, DateOptionFactory $dateOptionFactory ) { - $this->entityManager = $entityManager; + $this->schemaCache = $schemaCache; $this->container = $container; $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; $this->baseDecorator = $baseDecorator; @@ -84,7 +88,7 @@ public function getLeadListFilters(LeadList $leadList) $decorator = $this->getDecoratorForFilter($leadSegmentFilterCrate); - $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->entityManager); + $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->schemaCache); //$this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); $leadSegmentFilter->setFilterQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index cef59ae2330..56ae56af969 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -130,31 +130,93 @@ public function getNewLeadListLeads(LeadList $leadList, array $batchLimiters, $l { $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); - $qb = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); - $qb->select('l.*'); + $queryBuilder = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); + $queryBuilder->select('l.*'); - $this->logger->debug('Segment QB: Create Leads SQL: '.$qb->getDebugOutput(), ['segmentId' => $leadList->getId()]); + $this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); - $qb->setMaxResults($limit); + $queryBuilder->setMaxResults($limit); if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { - $qb->andWhere( - $qb->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") + $queryBuilder->andWhere( + $queryBuilder->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") ); } elseif (!empty($batchLimiters['maxId'])) { - $qb->andWhere( - $qb->expr()->lte('l.id', $batchLimiters['maxId']) + $queryBuilder->andWhere( + $queryBuilder->expr()->lte('l.id', $batchLimiters['maxId']) ); } if (!empty($batchLimiters['dateTime'])) { // Only leads in the list at the time of count - $qb->andWhere( - $qb->expr()->lte('l.date_added', $qb->expr()->literal($batchLimiters['dateTime'])) + $queryBuilder->andWhere( + $queryBuilder->expr()->lte('l.date_added', $queryBuilder->expr()->literal($batchLimiters['dateTime'])) ); } - $result = $this->timedFetchAll($qb, $leadList->getId()); + $result = $this->timedFetchAll($queryBuilder, $leadList->getId()); + + return [$leadList->getId() => $result]; + } + + /** + * @param LeadList $leadList + * + * @return QueryBuilder + */ + private function getOrphanedLeadListLeadsQueryBuilder(LeadList $leadList) + { + $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); + + $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); + + $queryBuilder->select('l.id'); + $queryBuilder->rightJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp', 'l.id = orp.lead_id and orp.leadlist_id = '.$leadList->getId()); + $queryBuilder->andWhere($queryBuilder->expr()->andX( + $queryBuilder->expr()->isNull('l.id'), + $queryBuilder->expr()->eq('orp.leadlist_id', $leadList->getId()) + )); + + return $queryBuilder; + } + + /** + * @param LeadList $leadList + * @param array $batchLimiters + * @param int $limit + * + * @return array + * + * @throws \Exception + */ + public function getOrphanedLeadListLeadsCount(LeadList $leadList) + { + $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($leadList); + $queryBuilder = $this->leadSegmentQueryBuilder->wrapInCount($queryBuilder); + + $this->logger->debug('Segment QB: Orphan Leads Count SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); + + $result = $this->timedFetch($queryBuilder, $leadList->getId()); + + return [$leadList->getId() => $result]; + } + + /** + * @param LeadList $leadList + * @param array $batchLimiters + * @param int $limit + * + * @return array + * + * @throws \Exception + */ + public function getOrphanedLeadListLeads(LeadList $leadList) + { + $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($leadList); + + $this->logger->debug('Segment QB: Orphan Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); + + $result = $this->timedFetchAll($queryBuilder, $leadList->getId()); return [$leadList->getId() => $result]; } @@ -175,7 +237,7 @@ private function timedFetch(QueryBuilder $qb, $segmentId) $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result: '.$qb->getDebugOutput(), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), @@ -202,7 +264,7 @@ private function timedFetchAll(QueryBuilder $qb, $segmentId) $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result: '.$qb->getDebugOutput(), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 9b5232da507..9461cb445d4 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -110,7 +110,7 @@ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadList $queryBuilder->expr()->eq($tableAlias.'.manually_added', 1) ) ); - $queryBuilder->orWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); + $queryBuilder->andWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php new file mode 100644 index 00000000000..5e4edf805d1 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php @@ -0,0 +1,65 @@ +entityManager = $entityManager; + $this->cache = []; + } + + /** + * @param $tableName + * + * @return array|false + */ + public function getColumns($tableName) + { + if (!isset($this->cache[$tableName])) { + $columns = $this->entityManager->getConnection()->getSchemaManager()->listTableColumns($tableName); + $this->cache[$tableName] = $columns ?: []; + } + + return $this->cache[$tableName]; + } + + /** + * @return $this + */ + public function clear() + { + $this->cache = []; + + return $this; + } +} diff --git a/composer.json b/composer.json index c4ac6e25b26..163578fde36 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "doctrine/cache": "~1.5.4", "doctrine/migrations": "~1.2.2", "doctrine/orm": "~2.5.4", - "doctrine/doctrine-bundle": "~1.6", + "doctrine/doctrine-bundle": "1.6.*", "doctrine/doctrine-cache-bundle": "~1.3.0", "doctrine/doctrine-fixtures-bundle": "~2.3.0", "doctrine/doctrine-migrations-bundle": "~1.1.1", From 0667decf0853a6db9e2fdcb7481a0e6d01f4aa7b Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 26 Jan 2018 12:49:45 +0100 Subject: [PATCH 088/778] Filter factory refactoring - move Decorator creating to new Decorator factory --- app/bundles/LeadBundle/Config/config.php | 14 ++-- .../Segment/Decorator/DecoratorFactory.php | 70 ++++++++++++++++++ .../Segment/LeadSegmentFilterFactory.php | 72 +++---------------- 3 files changed, 91 insertions(+), 65 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index a281b2c6ee6..ef9c58f0ab1 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -812,10 +812,7 @@ 'arguments' => [ 'mautic.lead.model.lead_segment_schema_cache', '@service_container', - 'mautic.lead.repository.lead_segment_filter_descriptor', - 'mautic.lead.model.lead_segment_decorator_base', - 'mautic.lead.model.lead_segment_decorator_custom_mapped', - 'mautic.lead.model.lead_segment.decorator.date.optionFactory', + 'mautic.lead.model.lead_segment_decorator_factory', ], ], 'mautic.lead.model.lead_segment_schema_cache' => [ @@ -838,6 +835,15 @@ 'mautic.lead.segment.operator_options', ], ], + 'mautic.lead.model.lead_segment_decorator_factory' => [ + 'class' => \Mautic\LeadBundle\Segment\Decorator\DecoratorFactory::class, + 'arguments' => [ + 'mautic.lead.repository.lead_segment_filter_descriptor', + 'mautic.lead.model.lead_segment_decorator_base', + 'mautic.lead.model.lead_segment_decorator_custom_mapped', + 'mautic.lead.model.lead_segment.decorator.date.optionFactory', + ], + ], 'mautic.lead.model.lead_segment_decorator_base' => [ 'class' => \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::class, 'arguments' => [ diff --git a/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php b/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php new file mode 100644 index 00000000000..03e9a3c7115 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php @@ -0,0 +1,70 @@ +baseDecorator = $baseDecorator; + $this->customMappedDecorator = $customMappedDecorator; + $this->dateOptionFactory = $dateOptionFactory; + $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; + } + + /** + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * + * @return FilterDecoratorInterface + */ + public function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + if ($leadSegmentFilterCrate->isDateType()) { + return $this->dateOptionFactory->getDateOption($leadSegmentFilterCrate); + } + + $originalField = $leadSegmentFilterCrate->getField(); + + if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + return $this->baseDecorator; + } + + return $this->customMappedDecorator; + } +} diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 88cecd528f7..328a3c7b983 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -12,19 +12,15 @@ namespace Mautic\LeadBundle\Segment; use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Segment\Decorator\BaseDecorator; -use Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator; -use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory; -use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; +use Mautic\LeadBundle\Segment\Decorator\DecoratorFactory; use Symfony\Component\DependencyInjection\Container; class LeadSegmentFilterFactory { /** - * @var \Doctrine\DBAL\Schema\AbstractSchemaManager + * @var TableSchemaColumnsCache */ - private $entityManager; + private $schemaCache; /** * @var Container @@ -32,44 +28,18 @@ class LeadSegmentFilterFactory private $container; /** - * @var LeadSegmentFilterDescriptor - */ - private $leadSegmentFilterDescriptor; - - /** - * @var BaseDecorator - */ - private $baseDecorator; - - /** - * @var CustomMappedDecorator - */ - private $customMappedDecorator; - - /** - * @var DateOptionFactory - */ - private $dateOptionFactory; - - /** - * @var TableSchemaColumnsCache + * @var DecoratorFactory */ - private $schemaCache; + private $decoratorFactory; public function __construct( TableSchemaColumnsCache $schemaCache, Container $container, - LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, - BaseDecorator $baseDecorator, - CustomMappedDecorator $customMappedDecorator, - DateOptionFactory $dateOptionFactory + DecoratorFactory $decoratorFactory ) { - $this->schemaCache = $schemaCache; - $this->container = $container; - $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; - $this->baseDecorator = $baseDecorator; - $this->customMappedDecorator = $customMappedDecorator; - $this->dateOptionFactory = $dateOptionFactory; + $this->schemaCache = $schemaCache; + $this->container = $container; + $this->decoratorFactory = $decoratorFactory; } /** @@ -86,10 +56,10 @@ public function getLeadListFilters(LeadList $leadList) // LeadSegmentFilterCrate is for accessing $filter as an object $leadSegmentFilterCrate = new LeadSegmentFilterCrate($filter); - $decorator = $this->getDecoratorForFilter($leadSegmentFilterCrate); + $decorator = $this->decoratorFactory->getDecoratorForFilter($leadSegmentFilterCrate); $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->schemaCache); - //$this->leadSegmentFilterDate->fixDateOptions($leadSegmentFilter); + $leadSegmentFilter->setFilterQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); //@todo replaced in query builder @@ -110,24 +80,4 @@ protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) return $this->container->get($qbServiceId); } - - /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * - * @return FilterDecoratorInterface - */ - protected function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) - { - if ($leadSegmentFilterCrate->isDateType()) { - return $this->dateOptionFactory->getDateOption($leadSegmentFilterCrate); - } - - $originalField = $leadSegmentFilterCrate->getField(); - - if (empty($this->leadSegmentFilterDescriptor[$originalField])) { - return $this->baseDecorator; - } - - return $this->customMappedDecorator; - } } From 030311706b5089325ec9171892c6f450310046bd Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 26 Jan 2018 13:36:15 +0100 Subject: [PATCH 089/778] Setting Filter Query Builder to Filter refactored --- .../LeadBundle/Segment/LeadSegmentFilter.php | 21 ++++++------------- .../Segment/LeadSegmentFilterFactory.php | 15 +++++++------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 2449880efc8..b130e66004e 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -12,6 +12,7 @@ namespace Mautic\LeadBundle\Segment; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; @@ -28,7 +29,7 @@ class LeadSegmentFilter private $filterDecorator; /** - * @var BaseFilterQueryBuilder + * @var FilterQueryBuilderInterface */ private $filterQueryBuilder; @@ -40,11 +41,13 @@ class LeadSegmentFilter public function __construct( LeadSegmentFilterCrate $leadSegmentFilterCrate, FilterDecoratorInterface $filterDecorator, - TableSchemaColumnsCache $cache + TableSchemaColumnsCache $cache, + FilterQueryBuilderInterface $filterQueryBuilder ) { $this->leadSegmentFilterCrate = $leadSegmentFilterCrate; $this->filterDecorator = $filterDecorator; $this->schemaCache = $cache; + $this->filterQueryBuilder = $filterQueryBuilder; } /** @@ -171,25 +174,13 @@ public function toArray() } /** - * @return BaseFilterQueryBuilder + * @return FilterQueryBuilderInterface */ public function getFilterQueryBuilder() { return $this->filterQueryBuilder; } - /** - * @param BaseFilterQueryBuilder $filterQueryBuilder - * - * @return LeadSegmentFilter - */ - public function setFilterQueryBuilder($filterQueryBuilder) - { - $this->filterQueryBuilder = $filterQueryBuilder; - - return $this; - } - /** * @return string */ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 328a3c7b983..4e56ee4d8a1 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -13,6 +13,8 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Decorator\DecoratorFactory; +use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface; use Symfony\Component\DependencyInjection\Container; class LeadSegmentFilterFactory @@ -58,9 +60,9 @@ public function getLeadListFilters(LeadList $leadList) $decorator = $this->decoratorFactory->getDecoratorForFilter($leadSegmentFilterCrate); - $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->schemaCache); + $filterQueryBuilder = $this->getQueryBuilderForFilter($decorator, $leadSegmentFilterCrate); - $leadSegmentFilter->setFilterQueryBuilder($this->getQueryBuilderForFilter($leadSegmentFilter)); + $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->schemaCache, $filterQueryBuilder); //@todo replaced in query builder $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); @@ -70,13 +72,14 @@ public function getLeadListFilters(LeadList $leadList) } /** - * @param LeadSegmentFilter $filter + * @param FilterDecoratorInterface $decorator + * @param LeadSegmentFilterCrate $leadSegmentFilterCrate * - * @return BaseFilterQueryBuilder + * @return FilterQueryBuilderInterface */ - protected function getQueryBuilderForFilter(LeadSegmentFilter $filter) + private function getQueryBuilderForFilter(FilterDecoratorInterface $decorator, LeadSegmentFilterCrate $leadSegmentFilterCrate) { - $qbServiceId = $filter->getQueryType(); + $qbServiceId = $decorator->getQueryType($leadSegmentFilterCrate); return $this->container->get($qbServiceId); } From 8b2d85f246c4118bb794b02de3a4d34ec4c455a5 Mon Sep 17 00:00:00 2001 From: Noa83 Date: Mon, 29 Jan 2018 11:13:34 +0100 Subject: [PATCH 090/778] fix missing return who bring the unit test error. --- .../LeadBundle/Helper/IdentifyCompanyHelper.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php b/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php index c55713ea398..0ed728639cd 100644 --- a/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php +++ b/app/bundles/LeadBundle/Helper/IdentifyCompanyHelper.php @@ -129,14 +129,17 @@ public static function findCompany(array $parameters, CompanyModel $companyModel */ protected static function domainExists($email) { - if (!strstr($email, '@')){ //not a valid email adress + if (!strstr($email, '@')) { //not a valid email adress return false; - } - list($user, $domain) = explode('@', $email); - $arr = dns_get_record($domain, DNS_MX); + } else { + list($user, $domain) = explode('@', $email); + $arr = dns_get_record($domain, DNS_MX); - if ($arr && $arr[0]['host'] === $domain) { - return $domain; + if ($arr && $arr[0]['host'] === $domain) { + return $domain; + } else { + return false; + } } } From 45a56cf31659bcb7f22aba58e615ad4f13f3b8b0 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 29 Jan 2018 15:40:13 +0100 Subject: [PATCH 091/778] fix update command change join structure for orphans, update references to old query builder, disable debug dumps for command, add primary column name guessing for right joins in qb. remove manually subscribed from 'new segment lead query'. --- .../Command/UpdateLeadListsCommand.php | 2 +- .../LeadBundle/Controller/ListController.php | 2 +- app/bundles/LeadBundle/Model/ListModel.php | 22 ++++-------------- .../LeadBundle/Segment/LeadSegmentService.php | 5 ++-- .../Filter/ForeignValueFilterQueryBuilder.php | 2 +- .../Segment/Query/LeadSegmentQueryBuilder.php | 5 +++- .../LeadBundle/Segment/Query/QueryBuilder.php | 23 +++++++++++++++++++ 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php index dc9557bd0dd..6f5a0b01a13 100644 --- a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php +++ b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php @@ -77,7 +77,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln(''.$translator->trans('mautic.lead.list.rebuild.rebuilding', ['%id%' => $l->getId()]).''); - $processed = $listModel->rebuildListLeads($l, $batch, $max, $output); + $processed = $listModel->updateLeadList($l, $batch, $max, $output); $output->writeln( ''.$translator->trans('mautic.lead.list.rebuild.leads_affected', ['%leads%' => $processed]).''."\n" ); diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 9496b3cf969..397dae638dd 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -562,7 +562,7 @@ public function viewAction($objectId) $listModel = $this->get('mautic.lead.model.list'); $list = $listModel->getEntity($objectId); - $processed = $listModel->updateLeadList($list); + //$processed = $listModel->updateLeadList($list); /** @var \Mautic\LeadBundle\Model\ListModel $model */ $model = $this->getModel('lead.list'); diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 12754cefc57..f5b00421fca 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -851,7 +851,6 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_added', ['%leads%' => $leadCount, '%batch%' => $limit])); } - dump('To add: '.$leadCount); // Handle by batches $start = $lastRoundPercentage = $leadsProcessed = 0; @@ -879,9 +878,9 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal break; } - $this->logger->debug(sprintf('Segment QB - Adding %d new leads to segment [%d] %s', count($newLeadList), $leadList->getId(), $leadList->getName())); + $this->logger->debug(sprintf('Segment QB - Adding %d new leads to segment [%d] %s', count($newLeadList[$leadList->getId()]), $leadList->getId(), $leadList->getName())); foreach ($newLeadList[$leadList->getId()] as $l) { - $this->logger->debug(sprintf('Segment QB - Adding lead #%s to segment [%d] %s', $l->getId()), $leadList->getId(), $leadList->getName()); + $this->logger->debug(sprintf('Segment QB - Adding lead #%s to segment [%d] %s', $l['id'], $leadList->getId(), $leadList->getName())); $this->addLead($l, $leadList, false, true, -1, $dtHelper->getLocalDateTime()); @@ -895,7 +894,7 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal } } - $this->logger->info(sprintf('Segment QB - Added %d new leads to segment [%d] %s', count($newLeadList), $leadList->getId(), $leadList->getName())); + $this->logger->info(sprintf('Segment QB - Added %d new leads to segment [%d] %s', count($newLeadList[$leadList->getId()]), $leadList->getId(), $leadList->getName())); $start += $limit; @@ -940,8 +939,6 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal $start = $lastRoundPercentage = 0; $leadCount = $orphanLeadsCount[$leadList->getId()]['count']; - dump('To remove: '.$leadCount); - if ($output) { $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_removed', ['%leads%' => $leadCount, '%batch%' => $limit])); } @@ -959,16 +956,7 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal // Keep CPU down for large lists; sleep per $limit batch $this->batchSleep(); - $removeLeadList = $this->getLeadsByList( - $list, - true, - [ - // No start because the items are deleted so always 0 - 'limit' => $limit, - 'nonMembersOnly' => true, - 'batchLimiters' => $batchLimiters, - ] - ); + $removeLeadList = $this->leadSegmentService->getOrphanedLeadListLeads($leadList); if (empty($removeLeadList[$leadList->getId()])) { // Somehow ran out of leads so break out @@ -993,7 +981,7 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { $this->dispatcher->dispatch( LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $entity, false) + new ListChangeEvent($processedLeads, $leadList, false) ); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 56ae56af969..d40ea8984bb 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -79,7 +79,7 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $segmentFilters, $ /** @var QueryBuilder $queryBuilder */ $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); - $queryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $leadList->getId()); + //$queryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $leadList->getId()); $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); return $queryBuilder; @@ -170,13 +170,14 @@ private function getOrphanedLeadListLeadsQueryBuilder(LeadList $leadList) $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); - $queryBuilder->select('l.id'); $queryBuilder->rightJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp', 'l.id = orp.lead_id and orp.leadlist_id = '.$leadList->getId()); $queryBuilder->andWhere($queryBuilder->expr()->andX( $queryBuilder->expr()->isNull('l.id'), $queryBuilder->expr()->eq('orp.leadlist_id', $leadList->getId()) )); + $queryBuilder->select($queryBuilder->guessPrimaryLeadIdColumn().' as id'); + return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 7b681b0ca78..76131f6601a 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -47,7 +47,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); - $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder = $queryBuilder->innerJoin( $queryBuilder->getTableAlias('leads'), $filter->getTable(), $tableAlias, diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 9461cb445d4..31bd4e2ab20 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -59,7 +59,10 @@ public function wrapInCount(QueryBuilder $qb) { // Add count functions to the query $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); - $qb->addSelect('l.id as leadIdPrimary'); + // If there is any right join in the query we need to select its it + $primary = $qb->guessPrimaryLeadIdColumn(); + + $qb->addSelect($primary.' as leadIdPrimary'); $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId') ->from('('.$qb->getSQL().')', 'sss'); $queryBuilder->setParameters($qb->getParameters()); diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 3f45a315bff..fc3d64bf960 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1514,6 +1514,29 @@ public function getTableJoins($tableName) return false; } + /** + * Functions returns either the 'lead.id' or the primary key from right joined table. + * + * @return string + */ + public function guessPrimaryLeadIdColumn() + { + $parts = $this->getQueryParts(); + $leadTable = $parts['from'][0]['alias']; + $joins = $parts['join'][$leadTable]; + + foreach ($joins as $join) { + if ($join['joinType'] == 'right') { + $matches = null; + if (preg_match('/'.$leadTable.'\.id \= ([^\ ]+)/i', $join['joinCondition'], $matches)) { + return $matches[1]; + } + } + } + + return $leadTable.'.id'; + } + /** * Return aliases of all currently registered tables. * From aca63235031150500b4ad7e5a8e4c9796cabe3d1 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 30 Jan 2018 12:31:57 +0100 Subject: [PATCH 092/778] fix non-distinct leads in orphan query --- app/bundles/LeadBundle/Segment/LeadSegmentService.php | 2 +- .../LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index d40ea8984bb..2b5c102831b 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -131,7 +131,7 @@ public function getNewLeadListLeads(LeadList $leadList, array $batchLimiters, $l $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); $queryBuilder = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); - $queryBuilder->select('l.*'); + $queryBuilder->select('DISTINCT l.id'); $this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 31bd4e2ab20..8e24efe5a49 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -62,7 +62,7 @@ public function wrapInCount(QueryBuilder $qb) // If there is any right join in the query we need to select its it $primary = $qb->guessPrimaryLeadIdColumn(); - $qb->addSelect($primary.' as leadIdPrimary'); + $qb->addSelect('DISTINCT '.$primary.' as leadIdPrimary'); $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId') ->from('('.$qb->getSQL().')', 'sss'); $queryBuilder->setParameters($qb->getParameters()); From eb84e11df1ba0b0620bc78795112a19eea81d8e7 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 30 Jan 2018 14:03:30 +0100 Subject: [PATCH 093/778] Cleaning + typo --- app/bundles/LeadBundle/Config/config.php | 1 - .../LeadBundle/Segment/Decorator/DecoratorFactory.php | 1 + .../LeadBundle/Segment/LeadSegmentFilterFactory.php | 1 - app/bundles/LeadBundle/Segment/LeadSegmentService.php | 9 +-------- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index ef9c58f0ab1..01dba4bf7ed 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -802,7 +802,6 @@ 'class' => \Mautic\LeadBundle\Segment\LeadSegmentService::class, 'arguments' => [ 'mautic.lead.model.lead_segment_filter_factory', - 'mautic.lead.repository.lead_list_segment_repository', 'mautic.lead.repository.lead_segment_query_builder', 'monolog.logger.mautic', ], diff --git a/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php b/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php index 03e9a3c7115..c07ddc6742d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php @@ -21,6 +21,7 @@ class DecoratorFactory * @var LeadSegmentFilterDescriptor */ private $leadSegmentFilterDescriptor; + /** * @var BaseDecorator */ diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php index 4e56ee4d8a1..54202357e01 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php @@ -64,7 +64,6 @@ public function getLeadListFilters(LeadList $leadList) $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->schemaCache, $filterQueryBuilder); - //@todo replaced in query builder $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 2b5c102831b..2b1d00598c3 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -19,11 +19,6 @@ class LeadSegmentService { - /** - * @var LeadListSegmentRepository - */ - private $leadListSegmentRepository; - /** * @var LeadSegmentFilterFactory */ @@ -54,11 +49,9 @@ class LeadSegmentService */ public function __construct( LeadSegmentFilterFactory $leadSegmentFilterFactory, - LeadListSegmentRepository $leadListSegmentRepository, LeadSegmentQueryBuilder $queryBuilder, Logger $logger ) { - $this->leadListSegmentRepository = $leadListSegmentRepository; $this->leadSegmentFilterFactory = $leadSegmentFilterFactory; $this->leadSegmentQueryBuilder = $queryBuilder; $this->logger = $logger; @@ -76,7 +69,7 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $segmentFilters, $ if (!is_null($this->preparedQB)) { return $this->preparedQB; } - /** @var QueryBuilder $queryBuilder */ + $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); //$queryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $leadList->getId()); From 7537c074a9428b6f97508d7d09399f8532bce2c8 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 30 Jan 2018 14:25:58 +0100 Subject: [PATCH 094/778] fix incorrect distinct usage in query --- .../Command/UpdateLeadListsCommand.php | 8 ++- .../Segment/Query/LeadSegmentQueryBuilder.php | 52 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php index 6f5a0b01a13..3b12321c479 100644 --- a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php +++ b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php @@ -12,6 +12,7 @@ namespace Mautic\LeadBundle\Command; use Mautic\CoreBundle\Command\ModeratedCommand; +use Mautic\LeadBundle\Segment\Query\QueryException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -57,7 +58,12 @@ protected function execute(InputInterface $input, OutputInterface $output) $list = $listModel->getEntity($id); if ($list !== null) { $output->writeln(''.$translator->trans('mautic.lead.list.rebuild.rebuilding', ['%id%' => $id]).''); - $processed = $listModel->updateLeadList($list, $batch, $max, $output); + try { + $processed = $listModel->updateLeadList($list, $batch, $max, $output); + } catch (QueryException $e) { + $this->getContainer()->get('mautic.logger')->error('Query Builder Exception: '.$e->getMessage()); + } + $output->writeln( ''.$translator->trans('mautic.lead.list.rebuild.leads_affected', ['%leads%' => $processed]).'' ); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 8e24efe5a49..9470ee8addc 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -3,6 +3,7 @@ /* * @copyright 2014-2018 Mautic Contributors. All rights reserved * @author Mautic + * @author Jan Kozak * * @link http://mautic.org * @@ -16,6 +17,11 @@ use Mautic\LeadBundle\Segment\LeadSegmentFilters; use Mautic\LeadBundle\Segment\RandomParameterName; +/** + * Class LeadSegmentQueryBuilder is responsible for building queries for segments. + * + * @todo add exceptions + */ class LeadSegmentQueryBuilder { /** @var EntityManager */ @@ -27,6 +33,12 @@ class LeadSegmentQueryBuilder /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ private $schema; + /** + * LeadSegmentQueryBuilder constructor. + * + * @param EntityManager $entityManager + * @param RandomParameterName $randomParameterName + */ public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName) { $this->entityManager = $entityManager; @@ -55,6 +67,11 @@ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegment return $queryBuilder; } + /** + * @param QueryBuilder $qb + * + * @return QueryBuilder + */ public function wrapInCount(QueryBuilder $qb) { // Add count functions to the query @@ -62,7 +79,19 @@ public function wrapInCount(QueryBuilder $qb) // If there is any right join in the query we need to select its it $primary = $qb->guessPrimaryLeadIdColumn(); - $qb->addSelect('DISTINCT '.$primary.' as leadIdPrimary'); + $currentSelects = []; + foreach ($qb->getQueryParts()['select'] as $select) { + if ($select != $primary) { + $currentSelects[] = $select; + } + } + + $qb->select('DISTINCT '.$primary.' as leadIdPrimary'); + + foreach ($currentSelects as $select) { + $qb->addSelect($select); + } + $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId') ->from('('.$qb->getSQL().')', 'sss'); $queryBuilder->setParameters($qb->getParameters()); @@ -70,6 +99,15 @@ public function wrapInCount(QueryBuilder $qb) return $queryBuilder; } + /** + * Restrict the query to NEW members of segment. + * + * @param QueryBuilder $queryBuilder + * @param $leadListId + * @param $whatever @todo document this field + * + * @return QueryBuilder + */ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) { $queryBuilder->select('l.id'); @@ -99,6 +137,12 @@ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, return $queryBuilder; } + /** + * @param QueryBuilder $queryBuilder + * @param $leadListId + * + * @return QueryBuilder + */ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadListId) { $tableAlias = $this->generateRandomParameterName(); @@ -118,6 +162,12 @@ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadList return $queryBuilder; } + /** + * @param QueryBuilder $queryBuilder + * @param $leadListId + * + * @return QueryBuilder + */ public function addManuallyUnsubsribedQuery(QueryBuilder $queryBuilder, $leadListId) { $tableAlias = $this->generateRandomParameterName(); From 7ec355541b65ff39ab62457eaee1d3abf1b7eef3 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 30 Jan 2018 15:58:58 +0100 Subject: [PATCH 095/778] Add missing table prefix --- .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 6 +++--- .../Query/Filter/ForeignFuncFilterQueryBuilder.php | 10 ++++++---- .../Query/Filter/ForeignValueFilterQueryBuilder.php | 2 +- .../Query/Filter/SessionsFilterQueryBuilder.php | 2 +- .../Segment/Query/LeadSegmentQueryBuilder.php | 12 +++++++++++- .../LeadBundle/Segment/TableSchemaColumnsCache.php | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index ba57c38c6bc..37a8903273b 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -103,7 +103,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($filterAggr) { $queryBuilder->leftJoin( $queryBuilder->getTableAlias('leads'), - $filter->getTable(), + MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); @@ -111,11 +111,11 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($filter->getTable() == 'companies') { $relTable = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); - $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + $queryBuilder->leftJoin($relTable, MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); } else { $queryBuilder->leftJoin( $queryBuilder->getTableAlias('leads'), - $filter->getTable(), + MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index ae81d8faf1d..213e54d2abc 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -28,6 +28,8 @@ public static function getServiceId() /** * {@inheritdoc} + * + * @todo Missing use for a QueryException */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { @@ -57,7 +59,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterGlueFunc = $filterGlue.'Where'; - $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); + $tableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.$filter->getTable()); // for aggregate function we need to create new alias and not reuse the old one if ($filterAggr) { @@ -83,7 +85,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($filterAggr) { $queryBuilder->innerJoin( $queryBuilder->getTableAlias('leads'), - $filter->getTable(), + MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); @@ -91,11 +93,11 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($filter->getTable() == 'companies') { $relTable = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); - $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + $queryBuilder->leftJoin($relTable, MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); } else { $queryBuilder->leftJoin( $queryBuilder->getTableAlias('leads'), - $filter->getTable(), + MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) ); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 76131f6601a..889ab0997cf 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -49,7 +49,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder = $queryBuilder->innerJoin( $queryBuilder->getTableAlias('leads'), - $filter->getTable(), + MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, $tableAlias.'.lead_id = l.id' ); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php index 4fdca011376..afb7e08a877 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php @@ -49,7 +49,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder = $queryBuilder->leftJoin( $queryBuilder->getTableAlias('leads'), - $filter->getTable(), + MAUTIC_TABLE_PREFIX.$filter->getTable(), $tableAlias, $tableAlias.'.lead_id = l.id' ); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 9470ee8addc..16df746a7b5 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -51,13 +51,15 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r * @param LeadSegmentFilters $leadSegmentFilters * * @return QueryBuilder + * + * @todo Remove $id? */ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) { /** @var QueryBuilder $queryBuilder */ $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); - $queryBuilder->select('*')->from('leads', 'l'); + $queryBuilder->select('*')->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { @@ -191,6 +193,8 @@ private function generateRandomParameterName() /** * @return LeadSegmentFilterDescriptor + * + * @todo Remove this function */ public function getTranslator() { @@ -201,6 +205,8 @@ public function getTranslator() * @param LeadSegmentFilterDescriptor $translator * * @return LeadSegmentQueryBuilder + * + * @todo Remove this function */ public function setTranslator($translator) { @@ -211,6 +217,8 @@ public function setTranslator($translator) /** * @return \Doctrine\DBAL\Schema\AbstractSchemaManager + * + * @todo Remove this function */ public function getSchema() { @@ -221,6 +229,8 @@ public function getSchema() * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema * * @return LeadSegmentQueryBuilder + * + * @todo Remove this function */ public function setSchema($schema) { diff --git a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php index 5e4edf805d1..861dd1e278f 100644 --- a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php +++ b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php @@ -46,7 +46,7 @@ public function __construct(EntityManager $entityManager) public function getColumns($tableName) { if (!isset($this->cache[$tableName])) { - $columns = $this->entityManager->getConnection()->getSchemaManager()->listTableColumns($tableName); + $columns = $this->entityManager->getConnection()->getSchemaManager()->listTableColumns(MAUTIC_TABLE_PREFIX.$tableName); $this->cache[$tableName] = $columns ?: []; } From 86477916984df085e7193402367b03f18a2ccff1 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 31 Jan 2018 00:20:18 +0100 Subject: [PATCH 096/778] add table prefix considerations and more improve comparisson command to log filter decorators are made aware of table prefix all query builder can work with table prefix throw exception if requested table has no alias and it is requested remove database namespace from getColumn parameters handling add method to fetch current database name from table schema cache --- .../Command/CheckQueryBuildersCommand.php | 33 +++++++++++++++---- .../Segment/Decorator/BaseDecorator.php | 4 +-- .../Decorator/CustomMappedDecorator.php | 2 +- .../LeadBundle/Segment/LeadSegmentFilter.php | 7 +++- .../LeadBundle/Segment/LeadSegmentService.php | 4 +-- .../Query/Filter/BaseFilterQueryBuilder.php | 8 ++--- .../Filter/ForeignFuncFilterQueryBuilder.php | 23 ++++++------- .../Filter/ForeignValueFilterQueryBuilder.php | 2 +- .../Filter/SessionsFilterQueryBuilder.php | 2 +- .../Segment/Query/LeadSegmentQueryBuilder.php | 2 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 6 +++- .../Segment/TableSchemaColumnsCache.php | 8 +++++ 12 files changed, 70 insertions(+), 31 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index dee3f5141bc..cb5635242b7 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -13,12 +13,16 @@ use Mautic\CoreBundle\Command\ModeratedCommand; use Mautic\LeadBundle\Model\ListModel; +use Monolog\Logger; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CheckQueryBuildersCommand extends ModeratedCommand { + /** @var Logger */ + private $logger; + protected function configure() { $this @@ -32,7 +36,8 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); + $container = $this->getContainer(); + $this->logger = $container->get('monolog.logger.mautic'); /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ $listModel = $container->get('mautic.lead.model.list'); @@ -40,10 +45,14 @@ protected function execute(InputInterface $input, OutputInterface $output) $id = $input->getOption('segment-id'); $verbose = $input->getOption('verbose'); - $verbose = false; - if ($id) { $list = $listModel->getEntity($id); + + if (!$list) { + $output->writeln('Segment with id "'.$id.'" not found'); + + return 1; + } $this->runSegment($output, $verbose, $list, $listModel); } else { $lists = $listModel->getEntities( @@ -67,14 +76,26 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } + private function format_period($inputSeconds) + { + $hours = (int) ($minutes = (int) ($seconds = (int) ($milliseconds = (int) ($inputSeconds * 1000)) / 1000) / 60) / 60; + + return $hours.':'.($minutes % 60).':'.($seconds % 60).(($milliseconds === 0) ? '' : '.'.rtrim($milliseconds % 1000, '0')); + } + private function runSegment($output, $verbose, $l, ListModel $listModel) { - $output->write('Running segment '.$l->getId().'...'); + $output->write('Running segment '.$l->getId().'...old...'); + + $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); $timer1 = microtime(true); $processed = $listModel->getVersionOld($l); $timer1 = round((microtime(true) - $timer1) * 1000, 3); + $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); + + $output->write('new...'); $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); $timer2 = round((microtime(true) - $timer2) * 1000, 3); @@ -91,10 +112,10 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) sprintf('old: c: %d, m: %d, time: %dms <--> new: c: %d, m: %s, time: %dms', $processed['count'], $processed['maxId'], - $timer1, + $this->format_period($timer1), $processed2['count'], $processed2['maxId'], - $timer2 + $this->format_period($timer2) ) ); diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 7f86f8c948b..bb3a3acfbda 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -39,10 +39,10 @@ public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) { if ($leadSegmentFilterCrate->isLeadType()) { - return 'leads'; + return MAUTIC_TABLE_PREFIX.'leads'; } - return 'companies'; + return MAUTIC_TABLE_PREFIX.'companies'; } public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php index 2516fa42eab..4e299392822 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -49,7 +49,7 @@ public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) return parent::getTable($leadSegmentFilterCrate); } - return $this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; + return MAUTIC_TABLE_PREFIX.$this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; } public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index b130e66004e..187ddb78096 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -57,7 +57,12 @@ public function __construct( */ public function getColumn() { - $columns = $this->schemaCache->getColumns($this->getTable()); + $currentDBName = $this->schemaCache->getCurrentDatabaseName(); + + $table = preg_replace("/^{$currentDBName}\./", '', $this->getTable()); + + $columns = $this->schemaCache->getColumns($table); + if (!isset($columns[$this->getField()])) { throw new QueryException(sprintf('Database schema does not contain field %s.%s', $this->getTable(), $this->getField())); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 2b1d00598c3..2086c432edf 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -72,7 +72,6 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $segmentFilters, $ $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); - //$queryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $leadList->getId()); $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); return $queryBuilder; @@ -227,11 +226,12 @@ private function timedFetch(QueryBuilder $qb, $segmentId) { try { $start = microtime(true); + $result = $qb->execute()->fetch(\PDO::FETCH_ASSOC); $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.number_format(round($end * 100, 2), 3, '.', 's ').'ms. Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index ba57c38c6bc..cb0f3b820f3 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -102,10 +102,10 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter //@todo this logic needs to if ($filterAggr) { $queryBuilder->leftJoin( - $queryBuilder->getTableAlias('leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); } else { if ($filter->getTable() == 'companies') { @@ -114,10 +114,10 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); } else { $queryBuilder->leftJoin( - $queryBuilder->getTableAlias('leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index ae81d8faf1d..7bb6617e243 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -12,6 +12,7 @@ use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; +use Mautic\LeadBundle\Segment\Query\QueryException; /** * Class ForeignFuncFilterQueryBuilder. @@ -35,13 +36,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterGlue = $filter->getGlue(); $filterAggr = $filter->getAggregateFunction(); - try { - $filter->getColumn(); - } catch (QueryException $e) { - // We do ignore not found fields as they may be just removed custom field - return $queryBuilder; - } - $filterParameters = $filter->getParameterValue(); if (is_array($filterParameters)) { @@ -53,6 +47,13 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $parameters = $this->generateRandomParameterName(); } + try { + $filter->getColumn(); + } catch (QueryException $e) { + // We do ignore not found fields as they may be just removed custom field, it's bad! + return $queryBuilder; + } + $filterParametersHolder = $filter->getParameterHolder($parameters); $filterGlueFunc = $filterGlue.'Where'; @@ -82,10 +83,10 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter //@todo this logic needs to if ($filterAggr) { $queryBuilder->innerJoin( - $queryBuilder->getTableAlias('leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); } else { if ($filter->getTable() == 'companies') { @@ -94,10 +95,10 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); } else { $queryBuilder->leftJoin( - $queryBuilder->getTableAlias('leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias('leads'), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 76131f6601a..efec7570188 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -48,7 +48,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = $this->generateRandomParameterName(); $queryBuilder = $queryBuilder->innerJoin( - $queryBuilder->getTableAlias('leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, $tableAlias.'.lead_id = l.id' diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php index 4fdca011376..aa70320cb3b 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php @@ -48,7 +48,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = $this->generateRandomParameterName(); $queryBuilder = $queryBuilder->leftJoin( - $queryBuilder->getTableAlias('leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, $tableAlias.'.lead_id = l.id' diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 9470ee8addc..582d167b087 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -57,7 +57,7 @@ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegment /** @var QueryBuilder $queryBuilder */ $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); - $queryBuilder->select('*')->from('leads', 'l'); + $queryBuilder->select('*')->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index fc3d64bf960..d068f502e99 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1493,7 +1493,11 @@ public function getTableAlias($table, $joinType = null) } } - return !count($result) ? false : count($result) == 1 ? array_shift($result) : $result; + if (!count($result)) { + throw new QueryException(sprintf('No alias found for %s, available tables: [%s]', $table, join(', ', $this->getTableAliases()))); + } + + return count($result) == 1 ? array_shift($result) : $result; } /** diff --git a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php index 5e4edf805d1..ec9d1ca1200 100644 --- a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php +++ b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php @@ -62,4 +62,12 @@ public function clear() return $this; } + + /** + * @return string + */ + public function getCurrentDatabaseName() + { + return $this->entityManager->getConnection()->getDatabase(); + } } From f1c34f3077d174877a92bf423064e76bf3091d27 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 31 Jan 2018 10:53:24 +0100 Subject: [PATCH 097/778] fix benchmarking, add sql from old query to debug log, format bench time --- .../Command/CheckQueryBuildersCommand.php | 10 +++--- .../LeadBundle/Entity/LeadListRepository.php | 32 +++++++++++++++---- app/bundles/LeadBundle/Model/ListModel.php | 17 ++++++---- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index cb5635242b7..ca33a2104db 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -78,9 +78,9 @@ protected function execute(InputInterface $input, OutputInterface $output) private function format_period($inputSeconds) { - $hours = (int) ($minutes = (int) ($seconds = (int) ($milliseconds = (int) ($inputSeconds * 1000)) / 1000) / 60) / 60; + $now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', '')); - return $hours.':'.($minutes % 60).':'.($seconds % 60).(($milliseconds === 0) ? '' : '.'.rtrim($milliseconds % 1000, '0')); + return $now->format('H:i:s.u'); } private function runSegment($output, $verbose, $l, ListModel $listModel) @@ -91,14 +91,14 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $timer1 = microtime(true); $processed = $listModel->getVersionOld($l); - $timer1 = round((microtime(true) - $timer1) * 1000, 3); + $timer1 = microtime(true) - $timer1; $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); $output->write('new...'); $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); - $timer2 = round((microtime(true) - $timer2) * 1000, 3); + $timer2 = microtime(true) - $timer2; $processed2 = array_shift($processed2); @@ -109,7 +109,7 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) } $output->write( - sprintf('old: c: %d, m: %d, time: %dms <--> new: c: %d, m: %s, time: %dms', + sprintf('old: c: %d, m: %d, time: %s(est) <--> new: c: %d, m: %s, time: %s', $processed['count'], $processed['maxId'], $this->format_period($timer1), diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 070bba5394b..11eb2b1a061 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -25,6 +25,7 @@ use Mautic\LeadBundle\Event\LeadListFilteringEvent; use Mautic\LeadBundle\Event\LeadListFiltersOperatorsEvent; use Mautic\LeadBundle\LeadEvents; +use Monolog\Logger; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -311,17 +312,21 @@ public function getLeadCount($listIds) return ($returnArray) ? $return : $return[$listIds[0]]; } + private function format_period($inputSeconds) + { + $now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', '')); + + return $now->format('H:i:s.u'); + } + /** - * This function is weird, you should not use it, use LeadSegmentService instead. - * - * @param $lists - * @param array $args - * - * @deprecated + * @param $lists + * @param array $args + * @param Logger $logger * * @return array */ - public function getLeadsByList($lists, $args = []) + public function getLeadsByList($lists, $args = [], Logger $logger) { // Return only IDs $idOnly = (!array_key_exists('idOnly', $args)) ? false : $args['idOnly']; //Always TRUE @@ -511,7 +516,20 @@ public function getLeadsByList($lists, $args = []) $q->resetQueryPart('groupBy'); } + $params = $q->getParameters(); + $sqlT = $q->getSQL(); + foreach ($params as $key=>$val) { + if (!is_int($val) and !is_float($val)) { + $val = "'$val'"; + } + $sqlT = str_replace(":{$key}", $val, $sqlT); + } + + $logger->debug(sprintf('Old version SQL: %s', $sqlT)); + $timer = microtime(true); $results = $q->execute()->fetchAll(); + $timer = microtime(true) - $timer; + $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); foreach ($results as $r) { if ($countOnly) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index f5b00421fca..2964958edb7 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -30,6 +30,7 @@ use Mautic\LeadBundle\Helper\FormFieldHelper; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\LeadSegmentService; +use Monolog\Logger; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -801,7 +802,8 @@ public function getVersionOld(LeadList $entity) 'countOnly' => true, 'newOnly' => true, 'batchLimiters' => $batchLimiters, - ] + ], + $this->logger ); $return = array_shift($newLeadsCount); @@ -1528,17 +1530,18 @@ public function removeLead($lead, $lists, $manuallyRemoved = false, $batchProces } /** - * @param $lists - * @param bool $idOnly - * @param array $args + * @param $lists + * @param bool $idOnly + * @param array $args + * @param Logger $logger * - * @return mixed + * @return array */ - public function getLeadsByList($lists, $idOnly = false, array $args = []) + public function getLeadsByList($lists, $idOnly = false, array $args = [], Logger $logger) { $args['idOnly'] = $idOnly; - return $this->getRepository()->getLeadsByList($lists, $args); + return $this->getRepository()->getLeadsByList($lists, $args, $logger); } /** From d07fd3a6d47abea45eb64377be186e3da5866ab7 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 31 Jan 2018 12:22:10 +0100 Subject: [PATCH 098/778] leadlist query builder changes --- .../LeadBundle/Segment/LeadSegmentFilter.php | 19 +++- .../Query/Filter/DncFilterQueryBuilder.php | 1 + .../Filter/LeadListFilterQueryBuilder.php | 99 +++++++++++++++++-- .../Segment/Query/LeadSegmentQueryBuilder.php | 1 + 4 files changed, 107 insertions(+), 13 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 187ddb78096..20f199f91b7 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -137,12 +137,14 @@ public function getAggregateFunction() } /** - * @todo check whether necessary and replace or remove + * @todo check where necessary and remove after debugging is done * * @param null $field * * @return array|mixed * + * @deprecated + * * @throws \Exception */ public function getCrate($field = null) @@ -192,10 +194,21 @@ public function getFilterQueryBuilder() public function __toString() { if (!is_array($this->getParameterValue())) { - return sprintf('table:%s field:%s holder:%s value:%s', $this->getTable(), $this->getField(), $this->getParameterHolder('holder'), $this->getParameterValue()); + return sprintf('table:%s field:%s operator:%s holder:%s value:%s, crate:%s', + $this->getTable(), + $this->getField(), + $this->getOperator(), + $this->getParameterHolder('holder'), + $this->getParameterValue(), + print_r($this->getCrate(), true)); } - return sprintf('table:%s field:%s holder:%s value:%s', $this->getTable(), $this->getField(), print_r($this->getParameterHolder($this->getParameterValue()), true), print_r($this->getParameterValue(), true)); + return sprintf('table:%s field:%s holder:%s value:%s, crate: %s', + $this->getTable(), + $this->getField(), + print_r($this->getParameterHolder($this->getParameterValue()), true), + print_r($this->getParameterValue(), true), + print_r($this->getCrate(), true)); } /** diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php index 0cf25f1d0b6..c68ac6fe1ba 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php @@ -32,6 +32,7 @@ public static function getServiceId() */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { + //@todo look at this, the getCrate method is for debuggin only $parts = explode('_', $filter->getCrate('field')); $channel = 'email'; diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index bd923987611..942daadbe22 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -85,13 +85,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter foreach ($segmentIds as $segmentId) { $ids[] = $segmentId; - $exclusion = ($filter->getOperator() == 'exists'); + dump($filter->getOperator()); + + $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); if ($exclusion) { $leftIds[] = $segmentId; } else { - if (!isset($innerAlias)) { - $innerAlias = $this->generateRandomParameterName(); - } $innerIds[] = $segmentId; } } @@ -103,20 +102,100 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->expr()->in('l.id', $leftIds), $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) ); - $queryBuilder->andWhere($queryBuilder->expr()->isNull($leftAlias.'.lead_id')); + + // do not contact restriction, those who are do no to contact are not considered for exclusion + $dncAlias = $this->generateRandomParameterName(); + + $queryBuilder->leftJoin($leftAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact', $dncAlias, $dncAlias.'.lead_id = '.$leftAlias.'.lead_id'); + + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($dncAlias.'.reason', 1), + $queryBuilder->expr() + ->eq($dncAlias.'.channel', 'email') //@todo I really need to verify that this is the value to use, where is the email coming from? + ); + + $queryBuilder->addJoinCondition($dncAlias, $expression); + +// +// $exprParameter = $this->generateRandomParameterName(); +// $channelParameter = $this->generateRandomParameterName(); +// +// $expression = $queryBuilder->expr()->andX( +// $queryBuilder->expr()->eq($tableAlias.'.reason', ":$exprParameter"), +// $queryBuilder->expr() +// ->eq($tableAlias.'.channel', ":$channelParameter") +// ); +// +// $queryBuilder->addJoinCondition($tableAlias, $expression); +// +// $queryType = $filter->getOperator() === 'eq' ? 'isNull' : 'isNotNull'; +// +// $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); + + $queryBuilder->andWhere( + $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($leftAlias.'.lead_id'), + $queryBuilder->expr()->andX( + $queryBuilder->expr()->isNotNull($leftAlias.'.lead_id'), + $queryBuilder->expr()->isNotNull($dncAlias.'.lead_id') + ) + ) + ); } if (count($innerIds)) { - $leftAlias = $this->generateRandomParameterName(); - $queryBuilder->innerJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $leftAlias, + $innerAlias = $this->generateRandomParameterName(); + $queryBuilder->innerJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $innerAlias, $queryBuilder->expr()->andX( $queryBuilder->expr()->in('l.id', $innerIds), - $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) + $queryBuilder->expr()->eq('l.id', $innerAlias.'.lead_id')) ); } - $queryBuilder->innerJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $innerAlias, 'l.id = '.$innerAlias.'.lead_id'); - return $queryBuilder; } } + +$sql ="ELECT + null + FROM + mautic_leads nlUhHOxv + LEFT JOIN mautic_lead_lists_leads dVzaIsGt ON + dVzaIsGt.lead_id = nlUhHOxv.id + AND dVzaIsGt.leadlist_id = 7 + WHERE + ( + ( + EXISTS( + SELECT + null + FROM + mautic_lead_donotcontact MnuDztmo + WHERE + ( + MnuDztmo.reason = 1 + ) + AND( + MnuDztmo.lead_id = l.id + ) + AND( + MnuDztmo.channel = 'email' + ) + ) + ) + OR( + dVzaIsGt.manually_added = '1' + ) + ) + AND( + nlUhHOxv.id = l.id + ) + AND( + ( + dVzaIsGt.manually_removed IS NULL + ) + OR( + dVzaIsGt.manually_removed = '' + ) + ) + )"; diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 16df746a7b5..8a336624237 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -63,6 +63,7 @@ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegment /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { + dump($filter->__toString()); $queryBuilder = $filter->applyQuery($queryBuilder); } From d153ee1937076a8c5b0628c30923a6d88d5edc75 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 31 Jan 2018 12:30:28 +0100 Subject: [PATCH 099/778] leadlist query builder changes #2 --- .../Segment/Query/Filter/LeadListFilterQueryBuilder.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 942daadbe22..be0abd9a5b0 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -104,16 +104,19 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter ); // do not contact restriction, those who are do no to contact are not considered for exclusion - $dncAlias = $this->generateRandomParameterName(); + $dncAlias = $this->generateRandomParameterName(); + $channelParameter = $this->generateRandomParameterName(); $queryBuilder->leftJoin($leftAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact', $dncAlias, $dncAlias.'.lead_id = '.$leftAlias.'.lead_id'); $expression = $queryBuilder->expr()->andX( $queryBuilder->expr()->eq($dncAlias.'.reason', 1), $queryBuilder->expr() - ->eq($dncAlias.'.channel', 'email') //@todo I really need to verify that this is the value to use, where is the email coming from? + ->eq($dncAlias.'.channel', ':'.$channelParameter) //@todo I really need to verify that this is the value to use, where is the email coming from? ); + $queryBuilder->setParameter($channelParameter, 'email'); + $queryBuilder->addJoinCondition($dncAlias, $expression); // From 48992524aecddbb8933d982d03af6adbb0cf488e Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 31 Jan 2018 13:34:55 +0100 Subject: [PATCH 100/778] leadlist query builder mods --- .../Filter/LeadListFilterQueryBuilder.php | 41 +------------------ .../Segment/Query/LeadSegmentQueryBuilder.php | 1 - 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index be0abd9a5b0..722b1307b1f 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -85,7 +85,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter foreach ($segmentIds as $segmentId) { $ids[] = $segmentId; - dump($filter->getOperator()); $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); if ($exclusion) { @@ -103,46 +102,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) ); - // do not contact restriction, those who are do no to contact are not considered for exclusion - $dncAlias = $this->generateRandomParameterName(); - $channelParameter = $this->generateRandomParameterName(); - - $queryBuilder->leftJoin($leftAlias, MAUTIC_TABLE_PREFIX.'lead_donotcontact', $dncAlias, $dncAlias.'.lead_id = '.$leftAlias.'.lead_id'); - - $expression = $queryBuilder->expr()->andX( - $queryBuilder->expr()->eq($dncAlias.'.reason', 1), - $queryBuilder->expr() - ->eq($dncAlias.'.channel', ':'.$channelParameter) //@todo I really need to verify that this is the value to use, where is the email coming from? - ); - - $queryBuilder->setParameter($channelParameter, 'email'); - - $queryBuilder->addJoinCondition($dncAlias, $expression); - -// -// $exprParameter = $this->generateRandomParameterName(); -// $channelParameter = $this->generateRandomParameterName(); -// -// $expression = $queryBuilder->expr()->andX( -// $queryBuilder->expr()->eq($tableAlias.'.reason', ":$exprParameter"), -// $queryBuilder->expr() -// ->eq($tableAlias.'.channel', ":$channelParameter") -// ); -// -// $queryBuilder->addJoinCondition($tableAlias, $expression); -// -// $queryType = $filter->getOperator() === 'eq' ? 'isNull' : 'isNotNull'; -// -// $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); - $queryBuilder->andWhere( - $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($leftAlias.'.lead_id'), - $queryBuilder->expr()->andX( - $queryBuilder->expr()->isNotNull($leftAlias.'.lead_id'), - $queryBuilder->expr()->isNotNull($dncAlias.'.lead_id') - ) - ) + $queryBuilder->expr()->isNull($leftAlias.'.lead_id') ); } diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 8a336624237..16df746a7b5 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -63,7 +63,6 @@ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegment /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { - dump($filter->__toString()); $queryBuilder = $filter->applyQuery($queryBuilder); } From fd465c9fea59de49adad67ca11d8e0caa4261372 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 1 Feb 2018 13:45:47 +0100 Subject: [PATCH 101/778] Lead list join condition fix --- .../Segment/Query/Filter/LeadListFilterQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index be0abd9a5b0..fe4784d09d9 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -99,7 +99,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $leftAlias = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $leftAlias, $queryBuilder->expr()->andX( - $queryBuilder->expr()->in('l.id', $leftIds), + $queryBuilder->expr()->in($leftAlias.'.leadlist_id', $leftIds), $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) ); From 834bcef888725fdfcb19ab4bb2068c077482e310 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 2 Feb 2018 15:37:01 +0100 Subject: [PATCH 102/778] Dnc filter query builder fix - consider not only operator but parameter value too --- .../LeadBundle/Controller/ListController.php | 2 +- .../LeadBundle/Segment/LeadSegmentService.php | 4 ++++ .../Query/Filter/DncFilterQueryBuilder.php | 23 ++++++++++++++++++- .../Segment/Query/LeadSegmentQueryBuilder.php | 2 +- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 397dae638dd..9496b3cf969 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -562,7 +562,7 @@ public function viewAction($objectId) $listModel = $this->get('mautic.lead.model.list'); $list = $listModel->getEntity($objectId); - //$processed = $listModel->updateLeadList($list); + $processed = $listModel->updateLeadList($list); /** @var \Mautic\LeadBundle\Model\ListModel $model */ $model = $this->getModel('lead.list'); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 2086c432edf..c75d7679376 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -73,6 +73,10 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $segmentFilters, $ $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); + //dump($queryBuilder->getSQL()); + //dump($queryBuilder->getParameters()); + //dump($queryBuilder->execute()); + //exit; return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php index c68ac6fe1ba..d1e6b8fe81d 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php @@ -58,7 +58,28 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addJoinCondition($tableAlias, $expression); - $queryType = $filter->getOperator() === 'eq' ? 'isNull' : 'isNotNull'; + /* + * 1) condition "eq with parameter value YES" and "neq with parameter value NO" are equal + * 2) condition "eq with parameter value NO" and "neq with parameter value YES" are equal + * + * If we want to include unsubscribed people (option 1) - we need IS NOT NULL condition (give me people which exists in table) + * If we do not want to include unsubscribed people (option 2) - we need IS NULL condition + * + * @todo refactor this piece of code + */ + if ($filter->getOperator() === 'eq') { + if ($filter->getParameterValue() === true) { + $queryType = 'isNotNull'; + } else { + $queryType = 'isNull'; + } + } else { + if ($filter->getParameterValue() === true) { + $queryType = 'isNull'; + } else { + $queryType = 'isNotNull'; + } + } $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 8a336624237..49ff5e5cd11 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -63,7 +63,7 @@ public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegment /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { - dump($filter->__toString()); + //dump($filter->__toString()); $queryBuilder = $filter->applyQuery($queryBuilder); } From 34e91ceb2b198a3cc13d238ea059037b5bc05bfc Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 5 Feb 2018 08:46:34 +0100 Subject: [PATCH 103/778] mini SQL fix --- .../Segment/Query/Filter/LeadListFilterQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 722b1307b1f..c1dc446cb13 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -98,7 +98,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $leftAlias = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $leftAlias, $queryBuilder->expr()->andX( - $queryBuilder->expr()->in('l.id', $leftIds), + $queryBuilder->expr()->in($leftAlias.'.leadlist_id', $leftIds), $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) ); From 938f2420b15b9629bd1ceaae81b02b214188a651 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Sun, 11 Feb 2018 17:34:06 +0100 Subject: [PATCH 104/778] enable skipping running old query, remove date restriction from listmodel job, change leadlist generation, check relations between segments (draft for ordered processing), --- .../Command/CheckQueryBuildersCommand.php | 27 ++++++--- app/bundles/LeadBundle/Model/ListModel.php | 5 +- .../LeadBundle/Segment/LeadSegmentFilter.php | 8 +++ .../LeadBundle/Segment/LeadSegmentService.php | 15 +++-- .../Filter/LeadListFilterQueryBuilder.php | 52 +++++++++++++++++ .../Segment/Query/LeadSegmentQueryBuilder.php | 57 ++++++++++++++++--- 6 files changed, 136 insertions(+), 28 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index ca33a2104db..dadc2d869b1 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -23,13 +23,16 @@ class CheckQueryBuildersCommand extends ModeratedCommand /** @var Logger */ private $logger; + /** @var bool */ + private $skipOld = false; + protected function configure() { $this ->setName('mautic:segments:check-builders') ->setDescription('Compare output of query builders for given segments') ->addOption('--segment-id', '-i', InputOption::VALUE_OPTIONAL, 'Set the ID of segment to process') - ; + ->addOption('--skip-old', null, InputOption::VALUE_NONE, 'Skip old query builder'); parent::configure(); } @@ -42,8 +45,9 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ $listModel = $container->get('mautic.lead.model.list'); - $id = $input->getOption('segment-id'); - $verbose = $input->getOption('verbose'); + $id = $input->getOption('segment-id'); + $verbose = $input->getOption('verbose'); + $this->skipOld = $input->getOption('skip-old'); if ($id) { $list = $listModel->getEntity($id); @@ -85,17 +89,22 @@ private function format_period($inputSeconds) private function runSegment($output, $verbose, $l, ListModel $listModel) { - $output->write('Running segment '.$l->getId().'...old...'); + $output->write('Running segment '.$l->getId().'...'); - $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); + if (!$this->skipOld) { + $output->write('old...'); + $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); - $timer1 = microtime(true); - $processed = $listModel->getVersionOld($l); - $timer1 = microtime(true) - $timer1; + $timer1 = microtime(true); + $processed = $listModel->getVersionOld($l); + $timer1 = microtime(true) - $timer1; + } else { + $processed = ['count'=>-1, 'maxId'=>-1]; + $timer1 = 0; + } $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); - $output->write('new...'); $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); $timer2 = microtime(true) - $timer2; diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 2964958edb7..a10cd180fc0 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -831,9 +831,6 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal $batchLimiters = ['dateTime' => $dtHelper->toUtcString()]; $list = ['id' => $leadList->getId(), 'filters' => $leadList->getFilters()]; - //@todo remove this debug line - $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); - $this->dispatcher->dispatch( LeadEvents::LIST_PRE_PROCESS_LIST, new ListPreProcessListEvent($list, false) ); @@ -841,7 +838,7 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal // Get a count of leads to add $newLeadsCount = $this->leadSegmentService->getNewLeadListLeadsCount($leadList, $batchLimiters); - // Ensure the same list is used each batch + // Ensure the same list is used each batch <- would love to know how $batchLimiters['maxId'] = (int) $newLeadsCount[$leadList->getId()]['maxId']; // Number of total leads to process diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 20f199f91b7..412e4a55381 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -220,4 +220,12 @@ public function applyQuery(QueryBuilder $queryBuilder) { return $this->filterQueryBuilder->applyQuery($queryBuilder, $this); } + + /** + * @return bool + */ + public function isContactSegmentReference() + { + return $this->getField() === 'leadlist'; + } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index c75d7679376..f22c94fb002 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -59,18 +59,19 @@ public function __construct( /** * @param LeadList $leadList - * @param $segmentFilters * @param $batchLimiters * * @return Query\QueryBuilder|QueryBuilder */ - private function getNewLeadListLeadsQuery(LeadList $leadList, $segmentFilters, $batchLimiters) + private function getNewLeadListLeadsQuery(LeadList $leadList, $batchLimiters) { if (!is_null($this->preparedQB)) { return $this->preparedQB; } - $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); + $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); + + $queryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); //dump($queryBuilder->getSQL()); @@ -103,7 +104,7 @@ public function getNewLeadListLeadsCount(LeadList $leadList, array $batchLimiter ]; } - $qb = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); + $qb = $this->getNewLeadListLeadsQuery($leadList, $batchLimiters); $qb = $this->leadSegmentQueryBuilder->wrapInCount($qb); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $leadList->getId()]); @@ -124,9 +125,7 @@ public function getNewLeadListLeadsCount(LeadList $leadList, array $batchLimiter */ public function getNewLeadListLeads(LeadList $leadList, array $batchLimiters, $limit = 1000) { - $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); - - $queryBuilder = $this->getNewLeadListLeadsQuery($leadList, $segmentFilters, $batchLimiters); + $queryBuilder = $this->getNewLeadListLeadsQuery($leadList, $batchLimiters); $queryBuilder->select('DISTINCT l.id'); $this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); @@ -164,7 +163,7 @@ private function getOrphanedLeadListLeadsQueryBuilder(LeadList $leadList) { $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); - $queryBuilder = $this->leadSegmentQueryBuilder->getLeadsSegmentQueryBuilder($leadList->getId(), $segmentFilters); + $queryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); $queryBuilder->rightJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp', 'l.id = orp.lead_id and orp.leadlist_id = '.$leadList->getId()); $queryBuilder->andWhere($queryBuilder->expr()->andX( diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index c1dc446cb13..6c7ef267e81 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -71,6 +71,8 @@ public static function getServiceId() * @param LeadSegmentFilter $filter * * @return QueryBuilder + * + * @throws \Mautic\LeadBundle\Segment\Exception\SegmentQueryException */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { @@ -80,6 +82,56 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $segmentIds = [intval($segmentIds)]; } + $ids = []; + $leftIds = []; + $innerIds = []; + + foreach ($segmentIds as $segmentId) { + $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); + + $contactSegments = $this->entityManager->getRepository('MauticLeadBundle:LeadList')->findBy( + ['id' => $segmentId] + ); + + foreach ($contactSegments as $contactSegment) { + $filters = $this->leadSegmentFilterFactory->getLeadListFilters($contactSegment); + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($filters); + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($segmentQueryBuilder, $contactSegment->getId()); + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); + $segmentQueryBuilder->select('l.id'); + + $parameters = $segmentQueryBuilder->getParameters(); + foreach ($parameters as $key=>$value) { + $queryBuilder->setParameter($key, $value); + } + + $segmentAlias = $this->generateRandomParameterName(); + if ($exclusion) { + $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); + $queryBuilder->andWhere($queryBuilder->expr()->isNull($segmentAlias.'.id')); + } else { + $queryBuilder->innerJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); + } + } + } + + return $queryBuilder; + } + + /** + * @param QueryBuilder $queryBuilder + * @param LeadSegmentFilter $filter + * + * @return QueryBuilder + */ + public function applyQueryBak(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + { + $segmentIds = $filter->getParameterValue(); + + if (!is_array($segmentIds)) { + $segmentIds = [intval($segmentIds)]; + } + $leftIds = []; $innerIds = []; diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 49ff5e5cd11..da3c9ad51b6 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -13,6 +13,7 @@ namespace Mautic\LeadBundle\Segment\Query; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilters; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -33,6 +34,9 @@ class LeadSegmentQueryBuilder /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ private $schema; + /** @var array */ + private $relatedSegments = []; + /** * LeadSegmentQueryBuilder constructor. * @@ -54,22 +58,61 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r * * @todo Remove $id? */ - public function getLeadsSegmentQueryBuilder($id, LeadSegmentFilters $leadSegmentFilters) + public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegmentFilters, $backReference = null) { /** @var QueryBuilder $queryBuilder */ $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); $queryBuilder->select('*')->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); + $references = []; + /** @var LeadSegmentFilter $filter */ foreach ($leadSegmentFilters as $filter) { - //dump($filter->__toString()); + $segmentIdArray = is_array($filter->getParameterValue()) ? $filter->getParameterValue() : [$filter->getParameterValue()]; + // We will handle references differently than regular segments + if ($filter->isContactSegmentReference()) { + if (!is_null($backReference) || in_array($backReference, $this->getContactSegmentRelations($segmentIdArray))) { + throw new SegmentQueryException('Circular reference detected.'); + } + $references = $references + $segmentIdArray; + } $queryBuilder = $filter->applyQuery($queryBuilder); } return $queryBuilder; } + /** + * Get the list of segment's related segments. + * + * @param $id array + * + * @return array + */ + private function getContactSegmentRelations(array $id) + { + $referencedContactSegments = $this->entityManager->getRepository('MauticLeadBundle:LeadList')->findBy( + ['id' => $id] + ); + + $relations = []; + foreach ($referencedContactSegments as $segment) { + $filters = $segment->getFilters(); + foreach ($filters as $filter) { + if ($filter['field'] == 'leadlist') { + $relations[] = $filter['filter']; + } + } + } + + if (count($relations)) { + dump('referenced segment(s) has '.count($relations).' relations'); + } + + return $relations; + } + /** * @param QueryBuilder $qb * @@ -153,14 +196,14 @@ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadList 'l.id = '.$tableAlias.'.lead_id and '.$tableAlias.'.leadlist_id = '.intval($leadListId)); $queryBuilder->addJoinCondition($tableAlias, $queryBuilder->expr()->andX( - $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($tableAlias.'.manually_removed'), - $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 0) - ), +// $queryBuilder->expr()->orX( +// $queryBuilder->expr()->isNull($tableAlias.'.manually_removed'), +// $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 0) +// ), $queryBuilder->expr()->eq($tableAlias.'.manually_added', 1) ) ); - $queryBuilder->andWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); + $queryBuilder->orWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); return $queryBuilder; } From 3652045cf6b9490153eac3a2cb9428c82801400f Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 12 Feb 2018 14:39:26 +0100 Subject: [PATCH 105/778] fix segment references for manually subscribed segments --- .../LeadBundle/Segment/LeadSegmentService.php | 18 ++++++++++++++++-- .../Filter/LeadListFilterQueryBuilder.php | 8 +++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index f22c94fb002..dc4d1c60c0d 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -217,6 +217,20 @@ public function getOrphanedLeadListLeads(LeadList $leadList) return [$leadList->getId() => $result]; } + /** + * Formatting helper. + * + * @param $inputSeconds + * + * @return string + */ + private function format_period($inputSeconds) + { + $now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', '')); + + return $now->format('H:i:s.u'); + } + /** * @param QueryBuilder $qb * @param int $segmentId @@ -234,7 +248,7 @@ private function timedFetch(QueryBuilder $qb, $segmentId) $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.number_format(round($end * 100, 2), 3, '.', 's ').'ms. Result count: '.count($result), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.$this->format_period($end).', Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), @@ -261,7 +275,7 @@ private function timedFetchAll(QueryBuilder $qb, $segmentId) $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.round($end * 100, 2).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.$this->format_period($end).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 6c7ef267e81..00053e7ccf9 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -95,8 +95,14 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter foreach ($contactSegments as $contactSegment) { $filters = $this->leadSegmentFilterFactory->getLeadListFilters($contactSegment); + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($filters); - $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($segmentQueryBuilder, $contactSegment->getId()); + + // If the segment contains no filters; it means its for manually subscribed only + if (count($filters)) { + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($segmentQueryBuilder, $contactSegment->getId()); + } + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); $segmentQueryBuilder->select('l.id'); From 33cfa075cca7f1e85633eb1aa8a5d9c78e2727f4 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 12 Feb 2018 21:28:10 +0100 Subject: [PATCH 106/778] fix incossitent prefix usage --- .../Filter/LeadListFilterQueryBuilder.php | 48 ------------------- .../Segment/TableSchemaColumnsCache.php | 2 +- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 00053e7ccf9..1f4ad2a1731 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -82,10 +82,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $segmentIds = [intval($segmentIds)]; } - $ids = []; - $leftIds = []; - $innerIds = []; - foreach ($segmentIds as $segmentId) { $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); @@ -177,47 +173,3 @@ public function applyQueryBak(QueryBuilder $queryBuilder, LeadSegmentFilter $fil return $queryBuilder; } } - -$sql ="ELECT - null - FROM - mautic_leads nlUhHOxv - LEFT JOIN mautic_lead_lists_leads dVzaIsGt ON - dVzaIsGt.lead_id = nlUhHOxv.id - AND dVzaIsGt.leadlist_id = 7 - WHERE - ( - ( - EXISTS( - SELECT - null - FROM - mautic_lead_donotcontact MnuDztmo - WHERE - ( - MnuDztmo.reason = 1 - ) - AND( - MnuDztmo.lead_id = l.id - ) - AND( - MnuDztmo.channel = 'email' - ) - ) - ) - OR( - dVzaIsGt.manually_added = '1' - ) - ) - AND( - nlUhHOxv.id = l.id - ) - AND( - ( - dVzaIsGt.manually_removed IS NULL - ) - OR( - dVzaIsGt.manually_removed = '' - ) - ) - )"; diff --git a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php index 10e41caf33b..ec9d1ca1200 100644 --- a/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php +++ b/app/bundles/LeadBundle/Segment/TableSchemaColumnsCache.php @@ -46,7 +46,7 @@ public function __construct(EntityManager $entityManager) public function getColumns($tableName) { if (!isset($this->cache[$tableName])) { - $columns = $this->entityManager->getConnection()->getSchemaManager()->listTableColumns(MAUTIC_TABLE_PREFIX.$tableName); + $columns = $this->entityManager->getConnection()->getSchemaManager()->listTableColumns($tableName); $this->cache[$tableName] = $columns ?: []; } From 9ee32f0afef1d5a3cfba90deef931a2a7292122a Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 13 Feb 2018 14:23:20 +0100 Subject: [PATCH 107/778] query builder logic grouping added, sort check builder's leads by id for processing, add condition grouping stacking functions to qb, --- .../Command/CheckQueryBuildersCommand.php | 1 + .../Query/Filter/BaseFilterQueryBuilder.php | 18 ++++++++++- .../Segment/Query/LeadSegmentQueryBuilder.php | 5 ++++ .../LeadBundle/Segment/Query/QueryBuilder.php | 30 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index dadc2d869b1..71f3a763d19 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -62,6 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $lists = $listModel->getEntities( [ 'iterator_mode' => true, + 'orderBy' => 'l.id', ] ); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index cb0f3b820f3..ec2c44cc5bb 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -41,6 +41,10 @@ public static function getServiceId() return 'mautic.lead.query.builder.basic'; } + public function getLogicGroupingExpression() + { + } + /** * {@inheritdoc} */ @@ -174,7 +178,19 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } } else { - $queryBuilder->$filterGlueFunc($expression); + if ($filterGlue === 'or') { + if ($queryBuilder->hasLogicStack()) { + $queryBuilder->orWhere($queryBuilder->expr()->andX($queryBuilder->popLogicStack())); + } else { + $queryBuilder->addToLogicStack($expression); + } + } else { + if ($queryBuilder->hasLogicStack()) { + $queryBuilder->addToLogicStack($expression); + } else { + $queryBuilder->$filterGlueFunc($expression); + } + } } $queryBuilder->setParametersPairs($parameters, $filterParameters); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index da3c9ad51b6..e08e1089129 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -16,6 +16,7 @@ use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilters; +use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\RandomParameterName; /** @@ -80,6 +81,10 @@ public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegm $queryBuilder = $filter->applyQuery($queryBuilder); } + if ($queryBuilder->hasLogicStack()) { + $queryBuilder->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack())); + } + return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index d068f502e99..58784dc812f 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -86,6 +86,11 @@ class QueryBuilder 'values' => [], ]; + /** + * @var array + */ + private $logicStack = []; + /** * The complete SQL string for this query. * @@ -1601,4 +1606,29 @@ public function getDebugOutput() return $sql; } + + public function hasLogicStack() + { + return count($this->logicStack); + } + + public function getLogicStack() + { + return $this->logicStack; + } + + public function popLogicStack() + { + $stack = $this->logicStack; + $this->logicStack = []; + + return $stack; + } + + public function addToLogicStack($expression) + { + $this->logicStack[] = $expression; + + return $this; + } } From b4b942fec7635968544ba354399d5e93fcf697d2 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 13 Feb 2018 15:02:18 +0100 Subject: [PATCH 108/778] fix stack operations didn't consider last inserted expression --- .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index ec2c44cc5bb..2679a99a315 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -178,12 +179,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } } else { + // @todo remove stack logic, move it to the query builder if ($filterGlue === 'or') { if ($queryBuilder->hasLogicStack()) { - $queryBuilder->orWhere($queryBuilder->expr()->andX($queryBuilder->popLogicStack())); - } else { - $queryBuilder->addToLogicStack($expression); + $queryBuilder->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack())); } + $queryBuilder->addToLogicStack($expression); } else { if ($queryBuilder->hasLogicStack()) { $queryBuilder->addToLogicStack($expression); From ef32763765e66095f49b13741caa08316c42f6d1 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 14 Feb 2018 15:24:28 +0100 Subject: [PATCH 109/778] fix query logic for 'or' operations on segments with aggregated collumns, rollback changes to DNC QB, transform where conditions to having, add stacked logic methods, --- .../Query/Filter/BaseFilterQueryBuilder.php | 9 ++++- .../Query/Filter/DncFilterQueryBuilder.php | 21 +--------- .../Filter/ForeignFuncFilterQueryBuilder.php | 4 +- .../Segment/Query/LeadSegmentQueryBuilder.php | 6 +-- .../LeadBundle/Segment/Query/QueryBuilder.php | 40 +++++++++++++++++++ 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 2679a99a315..11d0cccbc16 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -174,6 +174,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($queryBuilder->isJoinTable($filter->getTable())) { if ($filterAggr) { + throw new \Exception('should not be used use different query builder'); $queryBuilder->andHaving($expression); } else { $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); @@ -182,7 +183,13 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter // @todo remove stack logic, move it to the query builder if ($filterGlue === 'or') { if ($queryBuilder->hasLogicStack()) { - $queryBuilder->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack())); + if (count($queryBuilder->getLogicStack())) { + $orWhereExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack()); + } else { + $orWhereExpression = $queryBuilder->popLogicStack(); + } + + $queryBuilder->orWhere($orWhereExpression); } $queryBuilder->addToLogicStack($expression); } else { diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php index d1e6b8fe81d..13519696771 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php @@ -58,27 +58,10 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addJoinCondition($tableAlias, $expression); - /* - * 1) condition "eq with parameter value YES" and "neq with parameter value NO" are equal - * 2) condition "eq with parameter value NO" and "neq with parameter value YES" are equal - * - * If we want to include unsubscribed people (option 1) - we need IS NOT NULL condition (give me people which exists in table) - * If we do not want to include unsubscribed people (option 2) - we need IS NULL condition - * - * @todo refactor this piece of code - */ if ($filter->getOperator() === 'eq') { - if ($filter->getParameterValue() === true) { - $queryType = 'isNotNull'; - } else { - $queryType = 'isNull'; - } + $queryType = $filter->getParameterValue() ? 'isNotNull' : 'isNull'; } else { - if ($filter->getParameterValue() === true) { - $queryType = 'isNull'; - } else { - $queryType = 'isNotNull'; - } + $queryType = $filter->getParameterValue() ? 'isNull' : 'isNotNull'; } $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 7bb6617e243..7972572bfe1 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -82,7 +82,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'in': //@todo this logic needs to if ($filterAggr) { - $queryBuilder->innerJoin( + $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, @@ -152,6 +152,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } if ($queryBuilder->isJoinTable($filter->getTable())) { + var_dump($filterAggr); + if ($filterAggr) { $queryBuilder->andHaving($expression); } else { diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index e08e1089129..941a5c1387f 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -16,7 +16,6 @@ use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilters; -use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\RandomParameterName; /** @@ -80,10 +79,7 @@ public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegm } $queryBuilder = $filter->applyQuery($queryBuilder); } - - if ($queryBuilder->hasLogicStack()) { - $queryBuilder->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack())); - } + $queryBuilder->applyStackLogic(); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 58784dc812f..b46594be224 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1631,4 +1631,44 @@ public function addToLogicStack($expression) return $this; } + + public function applyStackLogic() + { + if ($this->hasLogicStack()) { + $parts = $this->getQueryParts(); + + if (!is_null($parts['where']) && !is_null($parts['having'])) { + $where = $parts['where']; + $having = $parts['having']; + + $fields = $this->extractFields($where); + + foreach ($this->getLogicStack() as $expression) { + $fields = array_merge($fields, $this->extractFields($expression)); + } + + $fields = array_diff($fields, $this->extractFields($having)); + + $this->andHaving($where); + + $this->addSelect($fields); + + $this->resetQueryPart('where'); + $this->orHaving(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); + } else { + $this->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); + } + } + } + + public function extractFields($expression) + { + $matches = []; + + preg_match('/([a-zA-Z]+\.[a-zA-Z_]+)/', $expression instanceof CompositeExpression ? (string) $expression : $expression, $matches); + + $matches = array_keys(array_flip($matches)); + + return $matches; + } } From d529e14a25249c444a702ac9bfcde2658549d63d Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 15 Feb 2018 09:15:34 +0100 Subject: [PATCH 110/778] fix logic group for leadlist filters having multiple values --- .../Segment/Query/Filter/LeadListFilterQueryBuilder.php | 7 ++++++- .../LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 1f4ad2a1731..f7e375238a3 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -10,6 +10,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; +use Doctrine\DBAL\Query\Expression\CompositeExpression; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilterFactory; @@ -112,10 +113,14 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); $queryBuilder->andWhere($queryBuilder->expr()->isNull($segmentAlias.'.id')); } else { - $queryBuilder->innerJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); + $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); + $orExpressions[] = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); } } } + if (isset($orExpressions)) { + $queryBuilder->andWhere(new CompositeExpression(CompositeExpression::TYPE_OR, $orExpressions)); + } return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 941a5c1387f..011d007dc93 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -51,12 +51,12 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r } /** - * @param $id * @param LeadSegmentFilters $leadSegmentFilters + * @param null $backReference * * @return QueryBuilder * - * @todo Remove $id? + * @throws SegmentQueryException */ public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegmentFilters, $backReference = null) { From f01ac323250e7d9790d840fb64914a74ef9c8f83 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Feb 2018 11:48:16 +0100 Subject: [PATCH 111/778] Add where option to FilterDescriptor --- .../LeadBundle/Segment/Decorator/BaseDecorator.php | 7 ++++++- .../Segment/Decorator/CustomMappedDecorator.php | 11 +++++++++++ .../Segment/Decorator/FilterDecoratorInterface.php | 2 ++ app/bundles/LeadBundle/Segment/LeadSegmentFilter.php | 8 ++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index bb3a3acfbda..161eece30ae 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -28,7 +28,7 @@ class BaseDecorator implements FilterDecoratorInterface public function __construct( LeadSegmentFilterOperator $leadSegmentFilterOperator ) { - $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; + $this->leadSegmentFilterOperator = $leadSegmentFilterOperator; } public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) @@ -112,4 +112,9 @@ public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return false; } + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return null; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php index 4e299392822..2da99114c45 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -70,4 +70,15 @@ public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; } + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + $originalField = $leadSegmentFilterCrate->getField(); + + if (!isset($this->leadSegmentFilterDescriptor[$originalField]['where'])) { + return parent::getWhere($leadSegmentFilterCrate); + } + + return $this->leadSegmentFilterDescriptor[$originalField]['where']; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index 26152f05a23..97f415ec483 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -28,4 +28,6 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate); public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate); + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php index 412e4a55381..8546498a7bb 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php @@ -120,6 +120,14 @@ public function getParameterValue() return $this->filterDecorator->getParameterValue($this->leadSegmentFilterCrate); } + /** + * @return null|string + */ + public function getWhere() + { + return $this->filterDecorator->getWhere($this->leadSegmentFilterCrate); + } + /** * @return null|string */ From ce4b16d6dd778944675762a203923ee1e524ac94 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 15 Feb 2018 11:51:15 +0100 Subject: [PATCH 112/778] lead email received condition fix, notIn behaviours on foreign tables fix, add new where descriptor (waiting for petr's implementation), add exception for not found table joins if addJoin is requested, add new lead_email_received descriptor --- .../LeadBundle/Entity/LeadListRepository.php | 7 +- .../LeadBundle/Segment/LeadSegmentService.php | 2 + .../Filter/ForeignValueFilterQueryBuilder.php | 55 ++++++-- .../LeadBundle/Segment/Query/QueryBuilder.php | 6 + .../Services/LeadSegmentFilterDescriptor.php | 128 ++++++++++-------- 5 files changed, 127 insertions(+), 71 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 11eb2b1a061..3e016b798ad 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -519,10 +519,13 @@ public function getLeadsByList($lists, $args = [], Logger $logger) $params = $q->getParameters(); $sqlT = $q->getSQL(); foreach ($params as $key=>$val) { - if (!is_int($val) and !is_float($val)) { + if (!is_int($val) and !is_float($val) and !is_array($val)) { $val = "'$val'"; } - $sqlT = str_replace(":{$key}", $val, $sqlT); + if (is_array($val)) { + $val = join(',', $val); + } + $sqlT = str_replace(":{$key}", $val, $sqlT); } $logger->debug(sprintf('Old version SQL: %s', $sqlT)); diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index dc4d1c60c0d..c74cb5fcd79 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -79,6 +79,8 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $batchLimiters) //dump($queryBuilder->execute()); //exit; + dump($queryBuilder->getQueryParts()); + return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index efec7570188..6636877cd8b 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -44,15 +44,44 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); - if (!$tableAlias) { - $tableAlias = $this->generateRandomParameterName(); - - $queryBuilder = $queryBuilder->innerJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), - $filter->getTable(), - $tableAlias, - $tableAlias.'.lead_id = l.id' - ); + switch ($filterOperator) { + case 'notIn': + $tableAlias = $this->generateRandomParameterName(); + $crate = $filter->getCrate(); + + if (is_null($crate['func']) && $crate['aggr']) { + $where = ' AND '.str_replace(str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()).'.', $tableAlias.'.', $crate['aggr']); + } else { + $where = ''; + } + + $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), + $filter->getTable(), + $tableAlias, + $tableAlias.'.lead_id = l.id'.$where + ); + + $expression = $queryBuilder->expr()->in( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + + $queryBuilder->setParametersPairs($filterParametersHolder, $filter->getParameterValue()); + + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + + break; + default: + if (!$tableAlias) { + $tableAlias = $this->generateRandomParameterName(); + } + $queryBuilder = $queryBuilder->innerJoin( + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), + $filter->getTable(), + $tableAlias, + $tableAlias.'.lead_id = l.id' + ); } switch ($filterOperator) { @@ -68,13 +97,21 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias.'.lead_id'); $queryBuilder->andWhere($expression); break; + case 'notIn': + $queryBuilder->addSelect($tableAlias.'.lead_id'); + $expression = $queryBuilder->expr()->isNull( + $tableAlias.'.lead_id'); + $queryBuilder->andWhere($expression); + break; default: $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), $filterParametersHolder ); + dump('xxxx'.$expression); $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); $queryBuilder->setParametersPairs($parameters, $filterParameters); + dump($queryBuilder->getQueryParts()); } return $queryBuilder; diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index b46594be224..c5fc27b1688 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -21,6 +21,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\Expression\CompositeExpression; +use Elastica\Exception\QueryBuilderException; use Mautic\LeadBundle\Segment\Query\Expression\ExpressionBuilder; /** @@ -1421,10 +1422,15 @@ public function addJoinCondition($alias, $expr) foreach ($joins as $key => $join) { if ($join['joinAlias'] == $alias) { $result[$tbl][$key]['joinCondition'] = $join['joinCondition'].' and '.$expr; + $inserted = true; } } } + if (!isset($inserted)) { + throw new QueryBuilderException('Inserting condition to nonexistent join '.$alias); + } + $this->setQueryPart('join', $result); return $this; diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index 8532765259d..b6d953d369a 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -34,6 +34,14 @@ public function __construct() 'field' => 'open_count', ]; + $this->translations['lead_email_received'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table_field' => 'lead_id', + 'foreign_table' => 'email_stats', + 'field' => 'email_id', + 'func' => 'email_stats.is_read = 1', + ]; + $this->translations['hit_url_count'] = [ 'type' => ForeignFuncFilterQueryBuilder::getServiceId(), 'foreign_table' => 'page_hits', @@ -45,138 +53,138 @@ public function __construct() ]; $this->translations['lead_email_read_date'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'email_stats', - 'field' => 'date_read', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'email_stats', + 'field' => 'date_read', ]; $this->translations['hit_url_date'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'field' => 'date_hit', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'field' => 'date_hit', ]; $this->translations['dnc_bounced'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DncFilterQueryBuilder::getServiceId(), ]; $this->translations['dnc_bounced_sms'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DncFilterQueryBuilder::getServiceId(), ]; $this->translations['dnc_unsubscribed'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DncFilterQueryBuilder::getServiceId(), ]; $this->translations['dnc_unsubscribed_sms'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DncFilterQueryBuilder::getServiceId(), ]; $this->translations['leadlist'] = [ - 'type' => LeadListFilterQueryBuilder::getServiceId(), + 'type' => LeadListFilterQueryBuilder::getServiceId(), ]; $this->translations['globalcategory'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'lead_categories', - 'field' => 'category_id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_categories', + 'field' => 'category_id', ]; $this->translations['tags'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'lead_tags_xref', - 'field' => 'tag_id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_tags_xref', + 'field' => 'tag_id', ]; $this->translations['lead_email_sent'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'email_stats', - 'field' => 'email_id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'email_stats', + 'field' => 'email_id', ]; $this->translations['device_type'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'lead_devices', - 'field' => 'device', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device', ]; $this->translations['device_brand'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'lead_devices', - 'field' => 'device_brand', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device_brand', ]; $this->translations['device_os'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'lead_devices', - 'field' => 'device_os_name', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device_os_name', ]; $this->translations['device_model'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'lead_devices', - 'field' => 'device_model', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_devices', + 'field' => 'device_model', ]; $this->translations['stage'] = [ - 'type' => BaseFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'leads', - 'field' => 'stage_id', + 'type' => BaseFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'leads', + 'field' => 'stage_id', ]; $this->translations['notification'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'push_ids', - 'field' => 'id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'push_ids', + 'field' => 'id', ]; $this->translations['page_id'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'foreign_field' => 'page_id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'page_id', ]; $this->translations['email_id'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'foreign_field' => 'email_id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'email_id', ]; $this->translations['redirect_id'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'foreign_field' => 'redirect_id', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'redirect_id', ]; $this->translations['source'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'foreign_field' => 'source', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'foreign_field' => 'source', ]; $this->translations['hit_url'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', - 'field' => 'url', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', + 'field' => 'url', ]; $this->translations['referer'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', ]; $this->translations['source_id'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', ]; $this->translations['url_title'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table' => 'page_hits', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'page_hits', ]; $this->translations['sessions'] = [ - 'type' => SessionsFilterQueryBuilder::getServiceId(), + 'type' => SessionsFilterQueryBuilder::getServiceId(), ]; parent::__construct($this->translations); From a9e7171e36bc90d59f7b7f65397a8bed6f1f2f4f Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 15 Feb 2018 12:00:20 +0100 Subject: [PATCH 113/778] use the decorator methods for *get* --- app/bundles/LeadBundle/Segment/LeadSegmentService.php | 2 -- .../Query/Filter/ForeignValueFilterQueryBuilder.php | 7 ++----- .../Services/LeadSegmentFilterDescriptor.php | 10 +++++----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index c74cb5fcd79..dc4d1c60c0d 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -79,8 +79,6 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $batchLimiters) //dump($queryBuilder->execute()); //exit; - dump($queryBuilder->getQueryParts()); - return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 6636877cd8b..603f7f010a7 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -47,10 +47,9 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter switch ($filterOperator) { case 'notIn': $tableAlias = $this->generateRandomParameterName(); - $crate = $filter->getCrate(); - if (is_null($crate['func']) && $crate['aggr']) { - $where = ' AND '.str_replace(str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()).'.', $tableAlias.'.', $crate['aggr']); + if (!is_null($filter->getWhere())) { + $where = ' AND '.str_replace(str_replace(MAUTIC_TABLE_PREFIX, '', $filter->getTable()).'.', $tableAlias.'.', $filter->getWhere()); } else { $where = ''; } @@ -108,10 +107,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias.'.'.$filter->getField(), $filterParametersHolder ); - dump('xxxx'.$expression); $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); $queryBuilder->setParametersPairs($parameters, $filterParameters); - dump($queryBuilder->getQueryParts()); } return $queryBuilder; diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php index b6d953d369a..428274c0c0e 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php @@ -35,11 +35,11 @@ public function __construct() ]; $this->translations['lead_email_received'] = [ - 'type' => ForeignValueFilterQueryBuilder::getServiceId(), - 'foreign_table_field' => 'lead_id', - 'foreign_table' => 'email_stats', - 'field' => 'email_id', - 'func' => 'email_stats.is_read = 1', + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table_field' => 'lead_id', + 'foreign_table' => 'email_stats', + 'field' => 'email_id', + 'where' => 'email_stats.is_read = 1', ]; $this->translations['hit_url_count'] = [ From 2a6a03f0dc2edb50a09c587921aee6c097558060 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Feb 2018 12:06:37 +0100 Subject: [PATCH 114/778] Dates fix - implement interface --- .../LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 30ee0407771..327f9c49476 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -128,4 +128,9 @@ public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); } + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + } } From 2e1db8a7d5c78abdb7303b13b782ded2c611c519 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Feb 2018 12:09:04 +0100 Subject: [PATCH 115/778] Dates fix - implement interface --- .../Segment/Decorator/Date/Other/DateAnniversary.php | 5 +++++ .../LeadBundle/Segment/Decorator/Date/Other/DateDefault.php | 5 +++++ .../Segment/Decorator/Date/Other/DateRelativeInterval.php | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index 78fce183643..c4c2b08a12b 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -61,4 +61,9 @@ public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); } + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php index 45b41ec3448..63326fcc0c4 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php @@ -71,4 +71,9 @@ public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); } + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php index 06a69e5c911..d3801d07316 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php @@ -87,4 +87,9 @@ public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); } + + public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + { + return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + } } From 3f8beed6eec912e5854d67fd29a458294bbfa977 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 16 Feb 2018 14:56:23 +0100 Subject: [PATCH 116/778] fix joining logic for foreign value, alias joind id tables where possible, replace * in select with l.id ofr basic query, --- .../LeadBundle/Segment/LeadSegmentService.php | 1 + .../Filter/ForeignFuncFilterQueryBuilder.php | 2 - .../Filter/ForeignValueFilterQueryBuilder.php | 7 ++-- .../Filter/LeadListFilterQueryBuilder.php | 1 + .../Segment/Query/LeadSegmentQueryBuilder.php | 9 +---- .../LeadBundle/Segment/Query/QueryBuilder.php | 38 ++++++++----------- 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index dc4d1c60c0d..82130ef1bbe 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -105,6 +105,7 @@ public function getNewLeadListLeadsCount(LeadList $leadList, array $batchLimiter } $qb = $this->getNewLeadListLeadsQuery($leadList, $batchLimiters); + $qb = $this->leadSegmentQueryBuilder->wrapInCount($qb); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $leadList->getId()]); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 7972572bfe1..263c9afad02 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -152,8 +152,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } if ($queryBuilder->isJoinTable($filter->getTable())) { - var_dump($filterAggr); - if ($filterAggr) { $queryBuilder->andHaving($expression); } else { diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 603f7f010a7..0b6813e7dd1 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -42,8 +42,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterParametersHolder = $filter->getParameterHolder($parameters); - $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); - switch ($filterOperator) { case 'notIn': $tableAlias = $this->generateRandomParameterName(); @@ -72,15 +70,18 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter break; default: + $tableAlias = $queryBuilder->getTableAlias($filter->getTable(), 'inner'); + if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); } - $queryBuilder = $queryBuilder->innerJoin( + $queryBuilder = $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, $tableAlias.'.lead_id = l.id' ); + $queryBuilder->andWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); } switch ($filterOperator) { diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index f7e375238a3..9f0ed12660b 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -116,6 +116,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); $orExpressions[] = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); } + $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); } } if (isset($orExpressions)) { diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 011d007dc93..26b6d2af8cf 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -63,7 +63,7 @@ public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegm /** @var QueryBuilder $queryBuilder */ $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); - $queryBuilder->select('*')->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); + $queryBuilder->select('l.id')->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); $references = []; @@ -107,10 +107,6 @@ private function getContactSegmentRelations(array $id) } } - if (count($relations)) { - dump('referenced segment(s) has '.count($relations).' relations'); - } - return $relations; } @@ -134,7 +130,6 @@ public function wrapInCount(QueryBuilder $qb) } $qb->select('DISTINCT '.$primary.' as leadIdPrimary'); - foreach ($currentSelects as $select) { $qb->addSelect($select); } @@ -157,7 +152,7 @@ public function wrapInCount(QueryBuilder $qb) */ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) { - $queryBuilder->select('l.id'); + //$queryBuilder->select('l.id'); $parts = $queryBuilder->getQueryParts(); $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index c5fc27b1688..2ea1957a086 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1492,41 +1492,27 @@ public function getTableAlias($table, $joinType = null) $tableJoins = $this->getTableJoins($table); - if (!$tableJoins) { - return false; - } - - $result = []; - foreach ($tableJoins as $tableJoin) { if ($tableJoin['joinType'] == $joinType) { - $result[] = $tableJoin['joinAlias']; + return $tableJoin['joinAlias']; } } - if (!count($result)) { - throw new QueryException(sprintf('No alias found for %s, available tables: [%s]', $table, join(', ', $this->getTableAliases()))); - } - - return count($result) == 1 ? array_shift($result) : $result; + return false; } - /** - * @param $tableName - * - * @return bool - */ public function getTableJoins($tableName) { + $found = []; foreach ($this->getQueryParts()['join'] as $join) { foreach ($join as $joinPart) { - if ($tableName == $joinPart['joinAlias']) { - return $joinPart; + if ($tableName == $joinPart['joinTable']) { + $found[] = $joinPart; } } } - return false; + return count($found) ? $found : []; } /** @@ -1655,12 +1641,18 @@ public function applyStackLogic() $fields = array_diff($fields, $this->extractFields($having)); - $this->andHaving($where); + $select = array_unique(array_merge($this->getQueryPart('select'), $fields)); + + $whereConditionAlias = 'wh_'.substr(md5($where->__toString()), 0, 10); + $selectCondition = sprintf('(%s) AS %s', $where->__toString(), $whereConditionAlias); + + $this->orHaving($whereConditionAlias); - $this->addSelect($fields); + $this->addSelect($selectCondition); $this->resetQueryPart('where'); - $this->orHaving(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); + //$this->orHaving(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); + //$this->orHaving($this->popLogicStack()); } else { $this->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); } From bbbc9f045270a11cfb76427a2a9ea5e398153089 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 16 Feb 2018 15:14:33 +0100 Subject: [PATCH 117/778] fix companies handling, alias joined table selects and fix 'having' for them --- .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 2 +- .../LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 11d0cccbc16..f0108b903f5 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -113,7 +113,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); } else { - if ($filter->getTable() == 'companies') { + if ($filter->getTable() == MAUTIC_TABLE_PREFIX.'companies') { $relTable = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php index 26b6d2af8cf..7e12fe5b3c6 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php @@ -159,20 +159,20 @@ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $tableAlias = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id'); - $queryBuilder->addSelect($tableAlias.'.lead_id'); + $queryBuilder->addSelect($tableAlias.'.lead_id AS '.$tableAlias.'_lead_id'); $expression = $queryBuilder->expr()->andX( $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $leadListId), $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$whatever['dateTime']."'") ); - $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'.lead_id'); - $queryBuilder->addJoinCondition($tableAlias, $expression); if ($setHaving) { + $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'_lead_id'); $queryBuilder->andHaving($restrictionExpression); } else { + $restrictionExpression = $queryBuilder->expr()->isNull($tableAlias.'.lead_id'); $queryBuilder->andWhere($restrictionExpression); } From 535426c0b9e8603fae6250976441ad5ca7a1ec23 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 16 Feb 2018 16:01:25 +0100 Subject: [PATCH 118/778] Integration tests - fix dependencies --- .../DataFixtures/ORM/LoadLeadListData.php | 2 +- .../DataFixtures/ORM/LoadSegmentsData.php | 2 +- .../Tests/Model/ListModelFunctionalTest.php | 30 +++++++++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php b/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php index 51ede36cb7f..73d8d32d995 100644 --- a/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php +++ b/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php @@ -63,7 +63,7 @@ public function load(ObjectManager $manager) $manager->persist($list); $manager->flush(); - $this->container->get('mautic.lead.model.list')->rebuildListLeads($list); + $this->container->get('mautic.lead.model.list')->updateLeadList($list); } /** diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index 85dd5511ad0..f529c650206 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -510,7 +510,7 @@ protected function createSegment($listConfig, ObjectManager $manager) $manager->flush(); if ($listConfig['populate']) { - $this->container->get('mautic.lead.model.list')->rebuildListLeads($list); + $this->container->get('mautic.lead.model.list')->updateLeadList($list); } if (!empty($listConfig['manually_add'])) { diff --git a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php index e5da2eef9b1..21cf14003cb 100644 --- a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php @@ -4,11 +4,16 @@ use Mautic\CoreBundle\Test\MauticWebTestCase; use Mautic\LeadBundle\Entity\LeadList; +use Mautic\LeadBundle\Entity\LeadListRepository; +use Monolog\Logger; class ListModelFunctionalTest extends MauticWebTestCase { public function testSegmentCountIsCorrect() { + /** + * @var LeadListRepository + */ $repo = $this->em->getRepository(LeadList::class); $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); @@ -35,6 +40,10 @@ public function testSegmentCountIsCorrect() $segmentCompanyFields = $this->fixtures->getReference('segment-company-only-fields'); $segmentMembershipCompanyOnlyFields = $this->fixtures->getReference('segment-including-segment-with-company-only-fields'); + $logger = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + // These expect filters to be part of the $lists passed to getLeadsByList so pass the entity $segmentContacts = $repo->getLeadsByList( [ @@ -61,7 +70,8 @@ public function testSegmentCountIsCorrect() $segmentCompanyFields, $segmentMembershipCompanyOnlyFields, ], - ['countOnly' => true] + ['countOnly' => true], + $logger ); $this->assertEquals( @@ -199,6 +209,9 @@ public function testSegmentCountIsCorrect() public function testPublicSegmentsInContactPreferences() { + /** + * @var LeadListRepository + */ $repo = $this->em->getRepository(LeadList::class); $lists = $repo->getGlobalLists(); @@ -214,6 +227,9 @@ public function testPublicSegmentsInContactPreferences() public function testSegmentRebuildCommand() { + /** + * @var LeadListRepository + */ $repo = $this->em->getRepository(LeadList::class); $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); @@ -222,9 +238,13 @@ public function testSegmentRebuildCommand() '--env' => 'test', ]); + $logger = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + $segmentContacts = $repo->getLeadsByList([ $segmentTest3Ref, - ], ['countOnly' => true]); + ], ['countOnly' => true], $logger); $this->assertEquals( 24, @@ -240,9 +260,13 @@ public function testSegmentRebuildCommand() '--env' => 'test', ]); + $logger = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + $segmentContacts = $repo->getLeadsByList([ $segmentTest3Ref, - ], ['countOnly' => true]); + ], ['countOnly' => true], $logger); $this->assertEquals( 0, From 0c505dcff1a482a5b3be3af72f01463d38f4cb33 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 20 Feb 2018 12:40:56 +0100 Subject: [PATCH 119/778] add better command output, add notNull negation logic for boolean values 'neq' operations, create logic processing and move query builders logic to these functions(10% done), fix incorrect or/and logic for sub queries, add many debug links, --- .../Command/CheckQueryBuildersCommand.php | 34 +++++++-- .../LeadBundle/Segment/LeadSegmentService.php | 3 + .../Query/Filter/BaseFilterQueryBuilder.php | 50 ++++++++----- .../Filter/ForeignValueFilterQueryBuilder.php | 24 ++++++- .../Filter/LeadListFilterQueryBuilder.php | 31 ++++++-- .../LeadBundle/Segment/Query/QueryBuilder.php | 72 +++++++++++++++++++ 6 files changed, 183 insertions(+), 31 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 71f3a763d19..40ae17f660c 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -49,7 +49,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $verbose = $input->getOption('verbose'); $this->skipOld = $input->getOption('skip-old'); - if ($id) { + $failed = $ok = 0; + + if ($id && substr($id, strlen($id) - 1, 1) != '+') { $list = $listModel->getEntity($id); if (!$list) { @@ -57,7 +59,12 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } - $this->runSegment($output, $verbose, $list, $listModel); + $response = $this->runSegment($output, $verbose, $list, $listModel); + if ($response) { + ++$ok; + } else { + ++$failed; + } } else { $lists = $listModel->getEntities( [ @@ -70,7 +77,15 @@ protected function execute(InputInterface $input, OutputInterface $output) // Get first item; using reset as the key will be the ID and not 0 $l = reset($l); - $this->runSegment($output, $verbose, $l, $listModel); + if (substr($id, strlen($id) - 1, 1) == '+' and $l->getId() < intval(trim($id, '+'))) { + continue; + } + $response = $this->runSegment($output, $verbose, $l, $listModel); + if (!$response) { + ++$failed; + } else { + ++$ok; + } } unset($l); @@ -78,7 +93,11 @@ protected function execute(InputInterface $input, OutputInterface $output) unset($lists); } - return 0; + $total = $ok + $failed; + $output->writeln(''); + $output->writeln(sprintf('Total success rate: %d%%, %d succeeded: and %s%s failed... ', round(($ok / $total) * 100), $ok, ($failed ? $failed : ''), (!$failed ? $failed : ''))); + + return $failed ? 1 : 0; } private function format_period($inputSeconds) @@ -93,7 +112,6 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $output->write('Running segment '.$l->getId().'...'); if (!$this->skipOld) { - $output->write('old...'); $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); $timer1 = microtime(true); @@ -113,9 +131,9 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $processed2 = array_shift($processed2); if ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))) { - $output->write(''); + $output->write('FAILED - '); } else { - $output->write(''); + $output->write('OK - '); } $output->write( @@ -134,5 +152,7 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) } else { $output->writeln(''); } + + return !((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); } } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/LeadSegmentService.php index 82130ef1bbe..7b827c36e36 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/LeadSegmentService.php @@ -72,8 +72,11 @@ private function getNewLeadListLeadsQuery(LeadList $leadList, $batchLimiters) $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); $queryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + //dump($queryBuilder->getQueryPart('where')); $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); + //dump($queryBuilder->getQueryPart('where')); $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); + //dump($queryBuilder->getQueryPart('where')); //dump($queryBuilder->getSQL()); //dump($queryBuilder->getParameters()); //dump($queryBuilder->execute()); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index f0108b903f5..5e00abfc2cc 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -140,11 +140,21 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter case 'notEmpty': $expression = $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()); break; + case 'neq': + if ($filter->getCrate()['type'] === 'boolean' && $filter->getParameterValue() == 1) { + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->$filterOperator( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ) + ); + break; + } case 'startsWith': case 'endsWith': case 'gt': case 'eq': - case 'neq': case 'gte': case 'like': case 'notLike': @@ -181,24 +191,26 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter } } else { // @todo remove stack logic, move it to the query builder - if ($filterGlue === 'or') { - if ($queryBuilder->hasLogicStack()) { - if (count($queryBuilder->getLogicStack())) { - $orWhereExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack()); - } else { - $orWhereExpression = $queryBuilder->popLogicStack(); - } - - $queryBuilder->orWhere($orWhereExpression); - } - $queryBuilder->addToLogicStack($expression); - } else { - if ($queryBuilder->hasLogicStack()) { - $queryBuilder->addToLogicStack($expression); - } else { - $queryBuilder->$filterGlueFunc($expression); - } - } + $queryBuilder->addLogic($expression, $filterGlue); + +// if ($filterGlue === 'or') { +// if ($queryBuilder->hasLogicStack()) { +// if ($queryBuilder->hasLogicStack()) { +// $orWhereExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack()); +// } else { +// $orWhereExpression = $queryBuilder->popLogicStack(); +// } +// +// $queryBuilder->orWhere($orWhereExpression); +// } +// $queryBuilder->addToLogicStack($expression); +// } else { +// if ($queryBuilder->hasLogicStack()) { +// $queryBuilder->addToLogicStack($expression); +// } else { +// $queryBuilder->$filterGlueFunc($expression); +// } +// } } $queryBuilder->setParametersPairs($parameters, $filterParameters); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 0b6813e7dd1..a581e600de5 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -27,6 +27,7 @@ public static function getServiceId() /** {@inheritdoc} */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { + dump(get_class($this)); $filterOperator = $filter->getOperator(); $filterParameters = $filter->getParameterValue(); @@ -68,6 +69,17 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + break; + case 'neq': + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), + $filter->getTable(), + $tableAlias, + $tableAlias.'.lead_id = l.id' + ); + + $queryBuilder->addLogic($queryBuilder->expr()->isNull($tableAlias.'.lead_id'), 'and'); break; default: $tableAlias = $queryBuilder->getTableAlias($filter->getTable(), 'inner'); @@ -81,7 +93,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias, $tableAlias.'.lead_id = l.id' ); - $queryBuilder->andWhere($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id')); + $queryBuilder->addLogic($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id'), 'and'); } switch ($filterOperator) { @@ -89,6 +101,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addSelect($tableAlias.'.lead_id'); $expression = $queryBuilder->expr()->isNull( $tableAlias.'.lead_id'); + $queryBuilder->addLogic($expression, 'and'); $queryBuilder->andWhere($expression); break; case 'notEmpty': @@ -103,6 +116,15 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $tableAlias.'.lead_id'); $queryBuilder->andWhere($expression); break; + case 'neq': + $expression = $queryBuilder->expr()->eq( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + + $queryBuilder->addJoinCondition($tableAlias, $expression); + $queryBuilder->setParametersPairs($parameters, $filterParameters); + break; default: $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index 9f0ed12660b..edbef703df9 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -111,7 +111,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $segmentAlias = $this->generateRandomParameterName(); if ($exclusion) { $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); - $queryBuilder->andWhere($queryBuilder->expr()->isNull($segmentAlias.'.id')); + $expression = $queryBuilder->expr()->isNull($segmentAlias.'.id'); } else { $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); $orExpressions[] = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); @@ -119,9 +119,32 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); } } - if (isset($orExpressions)) { - $queryBuilder->andWhere(new CompositeExpression(CompositeExpression::TYPE_OR, $orExpressions)); - } + + $expression = isset($orExpressions) ? '('.join(' OR ', $orExpressions).')' : $expression; + + dump("====================================================================== EXPRESSION:\n"); + dump($expression); + + $queryBuilder->addLogic($expression, $filter->getGlue()); + +// if ($queryBuilder->hasLogicStack() && $filter->getGlue()=='AND') { +// $queryBuilder->addToLogicStack($expression); +// } else if ($queryBuilder->hasLogicStack()) { + //// $logic = $queryBuilder->popLogicStack(); + //// /** @var CompositeExpression $standingLogic */ + //// $standingLogic = $queryBuilder->getQueryPart('where'); + //// dump($logic); + //// $queryBuilder->orWhere("(" . join(' AND ', $logic) . ")"); +// $queryBuilder->applyStackLogic(); +// $queryBuilder->addToLogicStack($expression); +// } else { +// $queryBuilder->addToLogicStack($expression); +// } + + dump("====================================================================== WHERE:\n"); + dump($queryBuilder->getQueryPart('where')); + dump("====================================================================== STACK:\n"); + dump($queryBuilder->getLogicStack()); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 2ea1957a086..ae7a5398f5d 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1436,6 +1436,36 @@ public function addJoinCondition($alias, $expr) return $this; } + /** + * @todo I need to rewrite it, it's no longer necessary like this, we have direct access to query parts + * + * @param $alias + * @param $expr + * + * @return $this + */ + public function addOrJoinCondition($alias, $expr) + { + $result = $parts = $this->getQueryPart('join'); + + foreach ($parts as $tbl => $joins) { + foreach ($joins as $key => $join) { + if ($join['joinAlias'] == $alias) { + $result[$tbl][$key]['joinCondition'] = $this->expr()->orX($join['joinCondition'], $expr); + $inserted = true; + } + } + } + + if (!isset($inserted)) { + throw new QueryBuilderException('Inserting condition to nonexistent join '.$alias); + } + + $this->setQueryPart('join', $result); + + return $this; + } + /** * @param $alias * @param $expr @@ -1624,6 +1654,38 @@ public function addToLogicStack($expression) return $this; } + public function addLogic($expression, $glue) + { + dump('adding logic "'.$glue.'":'.$expression.' stack:'.count($this->getLogicStack())); + if ($this->hasLogicStack() && $glue == 'and') { + dump('and where:'.$expression); + $this->addToLogicStack($expression); + } elseif ($this->hasLogicStack()) { + // $logic = $queryBuilder->popLogicStack(); + // /** @var CompositeExpression $standingLogic */ + // $standingLogic = $queryBuilder->getQueryPart('where'); + // dump($logic); + // $queryBuilder->orWhere("(" . join(' AND ', $logic) . ")"); + //$this->applyStackLogic(); + if ($glue == 'or') { + $this->applyStackLogic(); + } + $this->addToLogicStack($expression); + } elseif ($glue == 'or') { + dump('or to stack: '.$expression); + $this->addToLogicStack($expression); + } else { +// if (!is_null($this->lastAndWhere)) { +// $this->andWhere($this->lastAndWhere); +// $this->lastAndWhere = null; +// } else { +// $this->lastAndWhere = $expression; +// } + $this->andWhere($expression); + dump('and where '.$expression); + } + } + public function applyStackLogic() { if ($this->hasLogicStack()) { @@ -1654,8 +1716,18 @@ public function applyStackLogic() //$this->orHaving(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); //$this->orHaving($this->popLogicStack()); } else { + dump('-------- or stack:'); + dump($stack = $this->getLogicStack()); + /* @var CompositeExpression $where */ + dump('where:'); + dump($where = $this->getQueryPart('where')); + // Stack need to be added to the last composite of type 'or' + $this->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); } + dump('------- applied logic:'.(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack()))->__toString()); + dump('where:'); + dump($this->getQueryPart('where')); } } From 55a0bcead09934f47340bb753c524a22a9c33528 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 20 Feb 2018 12:46:27 +0100 Subject: [PATCH 120/778] move QB logic to QB from the Foreign QB, remove debug output --- .../Filter/ForeignValueFilterQueryBuilder.php | 6 +- .../Filter/LeadListFilterQueryBuilder.php | 82 +------------------ .../LeadBundle/Segment/Query/QueryBuilder.php | 22 ++--- 3 files changed, 16 insertions(+), 94 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index a581e600de5..84004e28ec8 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -27,7 +27,6 @@ public static function getServiceId() /** {@inheritdoc} */ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) { - dump(get_class($this)); $filterOperator = $filter->getOperator(); $filterParameters = $filter->getParameterValue(); @@ -102,19 +101,18 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $expression = $queryBuilder->expr()->isNull( $tableAlias.'.lead_id'); $queryBuilder->addLogic($expression, 'and'); - $queryBuilder->andWhere($expression); break; case 'notEmpty': $queryBuilder->addSelect($tableAlias.'.lead_id'); $expression = $queryBuilder->expr()->isNull( $tableAlias.'.lead_id'); - $queryBuilder->andWhere($expression); + $queryBuilder->addLogic($expression, 'and'); break; case 'notIn': $queryBuilder->addSelect($tableAlias.'.lead_id'); $expression = $queryBuilder->expr()->isNull( $tableAlias.'.lead_id'); - $queryBuilder->andWhere($expression); + $queryBuilder->addLogic($expression, 'and'); break; case 'neq': $expression = $queryBuilder->expr()->eq( diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php index edbef703df9..d4d59a17cd7 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php @@ -10,7 +10,6 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Doctrine\DBAL\Query\Expression\CompositeExpression; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\LeadSegmentFilter; use Mautic\LeadBundle\Segment\LeadSegmentFilterFactory; @@ -87,11 +86,11 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); $contactSegments = $this->entityManager->getRepository('MauticLeadBundle:LeadList')->findBy( - ['id' => $segmentId] + ['id' => $segmentId] ); foreach ($contactSegments as $contactSegment) { - $filters = $this->leadSegmentFilterFactory->getLeadListFilters($contactSegment); + $filters = $this->leadSegmentFilterFactory->getLeadListFilters($contactSegment); $segmentQueryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($filters); @@ -104,7 +103,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $segmentQueryBuilder->select('l.id'); $parameters = $segmentQueryBuilder->getParameters(); - foreach ($parameters as $key=>$value) { + foreach ($parameters as $key => $value) { $queryBuilder->setParameter($key, $value); } @@ -122,83 +121,8 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $expression = isset($orExpressions) ? '('.join(' OR ', $orExpressions).')' : $expression; - dump("====================================================================== EXPRESSION:\n"); - dump($expression); - $queryBuilder->addLogic($expression, $filter->getGlue()); -// if ($queryBuilder->hasLogicStack() && $filter->getGlue()=='AND') { -// $queryBuilder->addToLogicStack($expression); -// } else if ($queryBuilder->hasLogicStack()) { - //// $logic = $queryBuilder->popLogicStack(); - //// /** @var CompositeExpression $standingLogic */ - //// $standingLogic = $queryBuilder->getQueryPart('where'); - //// dump($logic); - //// $queryBuilder->orWhere("(" . join(' AND ', $logic) . ")"); -// $queryBuilder->applyStackLogic(); -// $queryBuilder->addToLogicStack($expression); -// } else { -// $queryBuilder->addToLogicStack($expression); -// } - - dump("====================================================================== WHERE:\n"); - dump($queryBuilder->getQueryPart('where')); - dump("====================================================================== STACK:\n"); - dump($queryBuilder->getLogicStack()); - - return $queryBuilder; - } - - /** - * @param QueryBuilder $queryBuilder - * @param LeadSegmentFilter $filter - * - * @return QueryBuilder - */ - public function applyQueryBak(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) - { - $segmentIds = $filter->getParameterValue(); - - if (!is_array($segmentIds)) { - $segmentIds = [intval($segmentIds)]; - } - - $leftIds = []; - $innerIds = []; - - foreach ($segmentIds as $segmentId) { - $ids[] = $segmentId; - - $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); - if ($exclusion) { - $leftIds[] = $segmentId; - } else { - $innerIds[] = $segmentId; - } - } - - if (count($leftIds)) { - $leftAlias = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $leftAlias, - $queryBuilder->expr()->andX( - $queryBuilder->expr()->in($leftAlias.'.leadlist_id', $leftIds), - $queryBuilder->expr()->eq('l.id', $leftAlias.'.lead_id')) - ); - - $queryBuilder->andWhere( - $queryBuilder->expr()->isNull($leftAlias.'.lead_id') - ); - } - - if (count($innerIds)) { - $innerAlias = $this->generateRandomParameterName(); - $queryBuilder->innerJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $innerAlias, - $queryBuilder->expr()->andX( - $queryBuilder->expr()->in('l.id', $innerIds), - $queryBuilder->expr()->eq('l.id', $innerAlias.'.lead_id')) - ); - } - return $queryBuilder; } } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index ae7a5398f5d..698bdcf17b4 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1656,9 +1656,9 @@ public function addToLogicStack($expression) public function addLogic($expression, $glue) { - dump('adding logic "'.$glue.'":'.$expression.' stack:'.count($this->getLogicStack())); + //dump('adding logic "'.$glue.'":'.$expression.' stack:'.count($this->getLogicStack())); if ($this->hasLogicStack() && $glue == 'and') { - dump('and where:'.$expression); + //dump('and where:'.$expression); $this->addToLogicStack($expression); } elseif ($this->hasLogicStack()) { // $logic = $queryBuilder->popLogicStack(); @@ -1672,7 +1672,7 @@ public function addLogic($expression, $glue) } $this->addToLogicStack($expression); } elseif ($glue == 'or') { - dump('or to stack: '.$expression); + //dump('or to stack: '.$expression); $this->addToLogicStack($expression); } else { // if (!is_null($this->lastAndWhere)) { @@ -1682,7 +1682,7 @@ public function addLogic($expression, $glue) // $this->lastAndWhere = $expression; // } $this->andWhere($expression); - dump('and where '.$expression); + //dump('and where '.$expression); } } @@ -1716,18 +1716,18 @@ public function applyStackLogic() //$this->orHaving(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); //$this->orHaving($this->popLogicStack()); } else { - dump('-------- or stack:'); - dump($stack = $this->getLogicStack()); + //dump('-------- or stack:'); +// dump($stack = $this->getLogicStack()); /* @var CompositeExpression $where */ - dump('where:'); - dump($where = $this->getQueryPart('where')); +// dump('where:'); +// dump($where = $this->getQueryPart('where')); // Stack need to be added to the last composite of type 'or' $this->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); } - dump('------- applied logic:'.(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack()))->__toString()); - dump('where:'); - dump($this->getQueryPart('where')); +// dump('------- applied logic:'.(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack()))->__toString()); +// dump('where:'); +// dump($this->getQueryPart('where')); } } From e33478cc516e2dac588b35a63ef3552103d6499f Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Tue, 20 Feb 2018 16:15:35 +0100 Subject: [PATCH 121/778] Add Disable trackable urls to SMS integration --- app/bundles/SmsBundle/Config/config.php | 1 + .../SmsBundle/EventListener/SmsSubscriber.php | 16 +++++++++- app/bundles/SmsBundle/Helper/SmsHelper.php | 30 ++++++++++++++----- .../Integration/TwilioIntegration.php | 16 +++++++++- .../SmsBundle/Translations/en_US/messages.ini | 2 ++ 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/app/bundles/SmsBundle/Config/config.php b/app/bundles/SmsBundle/Config/config.php index e652b43fa03..128e2cac9dc 100644 --- a/app/bundles/SmsBundle/Config/config.php +++ b/app/bundles/SmsBundle/Config/config.php @@ -26,6 +26,7 @@ 'mautic.page.model.trackable', 'mautic.page.helper.token', 'mautic.asset.helper.token', + 'mautic.helper.sms', ], ], 'mautic.sms.channel.subscriber' => [ diff --git a/app/bundles/SmsBundle/EventListener/SmsSubscriber.php b/app/bundles/SmsBundle/EventListener/SmsSubscriber.php index 65f1c386097..aaac89b579f 100644 --- a/app/bundles/SmsBundle/EventListener/SmsSubscriber.php +++ b/app/bundles/SmsBundle/EventListener/SmsSubscriber.php @@ -21,6 +21,7 @@ use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper; use Mautic\PageBundle\Model\TrackableModel; use Mautic\SmsBundle\Event\SmsEvent; +use Mautic\SmsBundle\Helper\SmsHelper; use Mautic\SmsBundle\SmsEvents; /** @@ -48,6 +49,11 @@ class SmsSubscriber extends CommonSubscriber */ protected $assetTokenHelper; + /** + * @var SmsHelper + */ + protected $smsHelper; + /** * DynamicContentSubscriber constructor. * @@ -55,17 +61,20 @@ class SmsSubscriber extends CommonSubscriber * @param TrackableModel $trackableModel * @param PageTokenHelper $pageTokenHelper * @param AssetTokenHelper $assetTokenHelper + * @param SmsHelper $smsHelper */ public function __construct( AuditLogModel $auditLogModel, TrackableModel $trackableModel, PageTokenHelper $pageTokenHelper, - AssetTokenHelper $assetTokenHelper + AssetTokenHelper $assetTokenHelper, + SmsHelper $smsHelper ) { $this->auditLogModel = $auditLogModel; $this->trackableModel = $trackableModel; $this->pageTokenHelper = $pageTokenHelper; $this->assetTokenHelper = $assetTokenHelper; + $this->smsHelper = $smsHelper; } /** @@ -123,6 +132,11 @@ public function onDelete(SmsEvent $event) */ public function onTokenReplacement(TokenReplacementEvent $event) { + // Disable trackable urls + if ($this->smsHelper->getDisableTrackableUrls()) { + return; + } + /** @var Lead $lead */ $lead = $event->getLead(); $content = $event->getContent(); diff --git a/app/bundles/SmsBundle/Helper/SmsHelper.php b/app/bundles/SmsBundle/Helper/SmsHelper.php index 0bb4cf7d94a..36ee9b89c41 100644 --- a/app/bundles/SmsBundle/Helper/SmsHelper.php +++ b/app/bundles/SmsBundle/Helper/SmsHelper.php @@ -46,6 +46,11 @@ class SmsHelper */ protected $integrationHelper; + /** + * @var bool + */ + private $disableTrackableUrls; + /** * SmsHelper constructor. * @@ -57,14 +62,15 @@ class SmsHelper */ public function __construct(EntityManager $em, LeadModel $leadModel, PhoneNumberHelper $phoneNumberHelper, SmsModel $smsModel, IntegrationHelper $integrationHelper) { - $this->em = $em; - $this->leadModel = $leadModel; - $this->phoneNumberHelper = $phoneNumberHelper; - $this->smsModel = $smsModel; - $this->integrationHelper = $integrationHelper; - $integration = $integrationHelper->getIntegrationObject('Twilio'); - $settings = $integration->getIntegrationSettings()->getFeatureSettings(); - $this->smsFrequencyNumber = $settings['frequency_number']; + $this->em = $em; + $this->leadModel = $leadModel; + $this->phoneNumberHelper = $phoneNumberHelper; + $this->smsModel = $smsModel; + $this->integrationHelper = $integrationHelper; + $integration = $integrationHelper->getIntegrationObject('Twilio'); + $settings = $integration->getIntegrationSettings()->getFeatureSettings(); + $this->smsFrequencyNumber = $settings['frequency_number']; + $this->disableTrackableUrls = !empty($settings['disable_trackable_urls']) ? true : false; } public function unsubscribe($number) @@ -105,4 +111,12 @@ public function unsubscribe($number) return $this->leadModel->addDncForLead($lead, 'sms', null, DoNotContact::UNSUBSCRIBED); } + + /** + * @return bool + */ + public function getDisableTrackableUrls() + { + return $this->disableTrackableUrls; + } } diff --git a/app/bundles/SmsBundle/Integration/TwilioIntegration.php b/app/bundles/SmsBundle/Integration/TwilioIntegration.php index d783ce70ee8..ffe94dcd1a5 100644 --- a/app/bundles/SmsBundle/Integration/TwilioIntegration.php +++ b/app/bundles/SmsBundle/Integration/TwilioIntegration.php @@ -94,7 +94,8 @@ public function appendToForm(&$builder, $data, $formArea) 'label_attr' => ['class' => 'control-label'], 'required' => false, 'attr' => [ - 'class' => 'form-control', + 'class' => 'form-control', + 'tooltip' => 'mautic.sms.config.form.sms.sending_phone_number.tooltip', ], ] ); @@ -108,6 +109,7 @@ public function appendToForm(&$builder, $data, $formArea) 'class' => 'form-control frequency', ], ]); + $builder->add('frequency_time', 'choice', [ 'choices' => [ @@ -123,6 +125,18 @@ public function appendToForm(&$builder, $data, $formArea) 'class' => 'form-control frequency', ], ]); + + $builder->add( + 'disable_trackable_urls', + 'yesno_button_group', + [ + 'label' => 'mautic.sms.config.form.sms.disable_trackable_urls', + 'attr' => [ + 'tooltip' => 'mautic.sms.config.form.sms.disable_trackable_urls.tooltip', + ], + 'data'=> !empty($data['disable_trackable_urls']) ? true : false, + ] + ); } } } diff --git a/app/bundles/SmsBundle/Translations/en_US/messages.ini b/app/bundles/SmsBundle/Translations/en_US/messages.ini index ed3b2f43386..b9e35b113f8 100644 --- a/app/bundles/SmsBundle/Translations/en_US/messages.ini +++ b/app/bundles/SmsBundle/Translations/en_US/messages.ini @@ -13,6 +13,8 @@ mautic.sms.config.form.sms.password="Auth Token" mautic.sms.config.form.sms.password.tooltip="Twilio Auth Token" mautic.sms.config.form.sms.sending_phone_number="Sending Phone Number" mautic.sms.config.form.sms.sending_phone_number.tooltip="The phone number given by your provider that you use to send and receive Text Message messages." +mautic.sms.config.form.sms.disable_trackable_urls="Disable trackable Urls" +mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option disable trackable Urls and your SMS click stats will not work." mautic.sms.sms="Text Message" mautic.sms.smses="Text Messages" From 7df53d6ecf525991f11ee6b1a84124136b83568f Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Tue, 20 Feb 2018 16:20:23 +0100 Subject: [PATCH 122/778] Minor --- app/bundles/SmsBundle/Integration/TwilioIntegration.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/bundles/SmsBundle/Integration/TwilioIntegration.php b/app/bundles/SmsBundle/Integration/TwilioIntegration.php index ffe94dcd1a5..38948e74c4a 100644 --- a/app/bundles/SmsBundle/Integration/TwilioIntegration.php +++ b/app/bundles/SmsBundle/Integration/TwilioIntegration.php @@ -109,7 +109,6 @@ public function appendToForm(&$builder, $data, $formArea) 'class' => 'form-control frequency', ], ]); - $builder->add('frequency_time', 'choice', [ 'choices' => [ From 4c82567796a2c259e459119ec78b71e30aa5cc29 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 21 Feb 2018 11:36:36 +0100 Subject: [PATCH 123/778] semantic refactoring, lead becomes contact and leadlist becomes segment --- app/bundles/LeadBundle/Config/config.php | 14 +- .../Entity/LeadListSegmentRepository.php | 24 +- app/bundles/LeadBundle/Model/ListModel.php | 6 +- ...entFilter.php => ContactSegmentFilter.php} | 82 ++-- ...rate.php => ContactSegmentFilterCrate.php} | 21 +- ...ry.php => ContactSegmentFilterFactory.php} | 41 +- ...r.php => ContactSegmentFilterOperator.php} | 16 +- ...tFilters.php => ContactSegmentFilters.php} | 38 +- ...tService.php => ContactSegmentService.php} | 136 +++--- .../Segment/Decorator/BaseDecorator.php | 91 +++- .../Decorator/CustomMappedDecorator.php | 100 ++-- .../Decorator/Date/DateOptionAbstract.php | 38 +- .../Decorator/Date/DateOptionFactory.php | 6 +- .../Decorator/Date/DateOptionParameters.php | 14 +- .../Decorator/Date/Day/DateDayAbstract.php | 4 +- .../Date/Month/DateMonthAbstract.php | 4 +- .../Decorator/Date/Other/DateAnniversary.php | 30 +- .../Decorator/Date/Other/DateDefault.php | 32 +- .../Date/Other/DateRelativeInterval.php | 38 +- .../Decorator/Date/Week/DateWeekAbstract.php | 4 +- .../Decorator/Date/Year/DateYearAbstract.php | 4 +- .../Segment/Decorator/DateDecorator.php | 11 +- .../Segment/Decorator/DecoratorFactory.php | 41 +- .../Decorator/FilterDecoratorInterface.php | 25 +- .../Exception/SegmentQueryException.php | 8 +- .../Segment/LeadSegmentFilterOld.php | 432 ------------------ ...der.php => ContactSegmentQueryBuilder.php} | 39 +- .../Query/Expression/CompositeExpression.php | 7 +- .../Query/Filter/BaseFilterQueryBuilder.php | 35 +- ...php => DoNotContactFilterQueryBuilder.php} | 8 +- .../Filter/FilterQueryBuilderInterface.php | 8 +- .../Filter/ForeignFuncFilterQueryBuilder.php | 4 +- .../Filter/ForeignValueFilterQueryBuilder.php | 4 +- ...=> SegmentReferenceFilterQueryBuilder.php} | 36 +- .../Filter/SessionsFilterQueryBuilder.php | 4 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 101 ++-- .../Segment/RandomParameterName.php | 3 + ...php => ContactSegmentFilterDictionary.php} | 22 +- 38 files changed, 606 insertions(+), 925 deletions(-) rename app/bundles/LeadBundle/Segment/{LeadSegmentFilter.php => ContactSegmentFilter.php} (64%) rename app/bundles/LeadBundle/Segment/{LeadSegmentFilterCrate.php => ContactSegmentFilterCrate.php} (88%) rename app/bundles/LeadBundle/Segment/{LeadSegmentFilterFactory.php => ContactSegmentFilterFactory.php} (54%) rename app/bundles/LeadBundle/Segment/{LeadSegmentFilterOperator.php => ContactSegmentFilterOperator.php} (78%) rename app/bundles/LeadBundle/Segment/{LeadSegmentFilters.php => ContactSegmentFilters.php} (66%) rename app/bundles/LeadBundle/Segment/{LeadSegmentService.php => ContactSegmentService.php} (55%) delete mode 100644 app/bundles/LeadBundle/Segment/LeadSegmentFilterOld.php rename app/bundles/LeadBundle/Segment/Query/{LeadSegmentQueryBuilder.php => ContactSegmentQueryBuilder.php} (89%) rename app/bundles/LeadBundle/Segment/Query/Filter/{DncFilterQueryBuilder.php => DoNotContactFilterQueryBuilder.php} (89%) rename app/bundles/LeadBundle/Segment/Query/Filter/{LeadListFilterQueryBuilder.php => SegmentReferenceFilterQueryBuilder.php} (76%) rename app/bundles/LeadBundle/Services/{LeadSegmentFilterDescriptor.php => ContactSegmentFilterDictionary.php} (88%) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 01dba4bf7ed..e9b4ac7b043 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -730,7 +730,7 @@ 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.dnc' => [ - 'class' => \Mautic\LeadBundle\Segment\Query\Filter\DncFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\DoNotContactFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.sessions' => [ @@ -738,7 +738,7 @@ 'arguments' => ['mautic.lead.model.random_parameter_name'], ], 'mautic.lead.query.builder.special.leadlist' => [ - 'class' => \Mautic\LeadBundle\Segment\Query\Filter\LeadListFilterQueryBuilder::class, + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\SegmentReferenceFilterQueryBuilder::class, 'arguments' => [ 'mautic.lead.model.random_parameter_name', 'mautic.lead.repository.lead_segment_query_builder', @@ -788,18 +788,18 @@ ], ], 'mautic.lead.repository.lead_segment_filter_descriptor' => [ - 'class' => \Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor::class, + 'class' => \Mautic\LeadBundle\Services\ContactSegmentFilterDictionary::class, 'arguments' => [], ], 'mautic.lead.repository.lead_segment_query_builder' => [ - 'class' => Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder::class, + 'class' => Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder::class, 'arguments' => [ 'doctrine.orm.entity_manager', 'mautic.lead.model.random_parameter_name', ], ], 'mautic.lead.model.lead_segment_service' => [ - 'class' => \Mautic\LeadBundle\Segment\LeadSegmentService::class, + 'class' => \Mautic\LeadBundle\Segment\ContactSegmentService::class, 'arguments' => [ 'mautic.lead.model.lead_segment_filter_factory', 'mautic.lead.repository.lead_segment_query_builder', @@ -807,7 +807,7 @@ ], ], 'mautic.lead.model.lead_segment_filter_factory' => [ - 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterFactory::class, + 'class' => \Mautic\LeadBundle\Segment\ContactSegmentFilterFactory::class, 'arguments' => [ 'mautic.lead.model.lead_segment_schema_cache', '@service_container', @@ -827,7 +827,7 @@ ], ], 'mautic.lead.model.lead_segment_filter_operator' => [ - 'class' => \Mautic\LeadBundle\Segment\LeadSegmentFilterOperator::class, + 'class' => \Mautic\LeadBundle\Segment\ContactSegmentFilterOperator::class, 'arguments' => [ 'translator', 'event_dispatcher', diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php index 3cb9195950d..a690a4fad03 100644 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php @@ -16,8 +16,8 @@ use Mautic\CoreBundle\Helper\InputHelper; use Mautic\LeadBundle\Event\LeadListFilteringEvent; use Mautic\LeadBundle\LeadEvents; -use Mautic\LeadBundle\Segment\LeadSegmentFilterOld; -use Mautic\LeadBundle\Segment\LeadSegmentFilters; +use Mautic\LeadBundle\Segment\ContactSegmentFilterOld; +use Mautic\LeadBundle\Segment\ContactSegmentFilters; use Mautic\LeadBundle\Segment\RandomParameterName; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -48,7 +48,7 @@ public function __construct( $this->randomParameterName = $randomParameterName; } - public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilters, array $batchLimiters) + public function getNewLeadsByListCount($id, ContactSegmentFilters $leadSegmentFilters, array $batchLimiters) { //TODO $withMinId = false; @@ -145,7 +145,7 @@ public function getNewLeadsByListCount($id, LeadSegmentFilters $leadSegmentFilte return $leads; } - private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) + private function generateSegmentExpression(ContactSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) { $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); @@ -157,13 +157,13 @@ private function generateSegmentExpression(LeadSegmentFilters $leadSegmentFilter } /** - * @param LeadSegmentFilters $leadSegmentFilters - * @param QueryBuilder $q - * @param int $listId + * @param ContactSegmentFilters $leadSegmentFilters + * @param QueryBuilder $q + * @param int $listId * * @return \Doctrine\DBAL\Query\Expression\CompositeExpression|mixed */ - private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId) + private function getListFilterExpr(ContactSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId) { $parameters = []; @@ -176,7 +176,7 @@ private function getListFilterExpr(LeadSegmentFilters $leadSegmentFilters, Query $groupExpr = $q->expr()->andX(); foreach ($leadSegmentFilters as $k => $leadSegmentFilter) { - $leadSegmentFilter = new LeadSegmentFilterOld((array) $leadSegmentFilter->leadSegmentFilterCrate); + $leadSegmentFilter = new ContactSegmentFilterOld((array) $leadSegmentFilter->contactSegmentFilterCrate); //$object = $leadSegmentFilter->getObject(); $column = false; @@ -1141,10 +1141,10 @@ protected function createFilterExpressionSubQuery($table, $alias, $column, $valu * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. * - * @param QueryBuilder $q - * @param LeadSegmentFilters $leadSegmentFilters + * @param QueryBuilder $q + * @param ContactSegmentFilters $leadSegmentFilters */ - private function applyCompanyFieldFilters(QueryBuilder $q, LeadSegmentFilters $leadSegmentFilters) + private function applyCompanyFieldFilters(QueryBuilder $q, ContactSegmentFilters $leadSegmentFilters) { $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; // Join company tables for query optimization diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index a10cd180fc0..d4df4bf3ff9 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -29,7 +29,7 @@ use Mautic\LeadBundle\Event\ListPreProcessListEvent; use Mautic\LeadBundle\Helper\FormFieldHelper; use Mautic\LeadBundle\LeadEvents; -use Mautic\LeadBundle\Segment\LeadSegmentService; +use Mautic\LeadBundle\Segment\ContactSegmentService; use Monolog\Logger; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; @@ -49,7 +49,7 @@ class ListModel extends FormModel protected $coreParametersHelper; /** - * @var LeadSegmentService + * @var ContactSegmentService */ private $leadSegmentService; @@ -58,7 +58,7 @@ class ListModel extends FormModel * * @param CoreParametersHelper $coreParametersHelper */ - public function __construct(CoreParametersHelper $coreParametersHelper, LeadSegmentService $leadSegment) + public function __construct(CoreParametersHelper $coreParametersHelper, ContactSegmentService $leadSegment) { $this->coreParametersHelper = $coreParametersHelper; $this->leadSegmentService = $leadSegment; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php similarity index 64% rename from app/bundles/LeadBundle/Segment/LeadSegmentFilter.php rename to app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 8546498a7bb..87f731ce538 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -1,7 +1,6 @@ leadSegmentFilterCrate = $leadSegmentFilterCrate; - $this->filterDecorator = $filterDecorator; - $this->schemaCache = $cache; - $this->filterQueryBuilder = $filterQueryBuilder; + ) + { + $this->contactSegmentFilterCrate = $contactSegmentFilterCrate; + $this->filterDecorator = $filterDecorator; + $this->schemaCache = $cache; + $this->filterQueryBuilder = $filterQueryBuilder; } /** @@ -75,7 +88,7 @@ public function getColumn() */ public function getQueryType() { - return $this->filterDecorator->getQueryType($this->leadSegmentFilterCrate); + return $this->filterDecorator->getQueryType($this->contactSegmentFilterCrate); } /** @@ -83,7 +96,7 @@ public function getQueryType() */ public function getOperator() { - return $this->filterDecorator->getOperator($this->leadSegmentFilterCrate); + return $this->filterDecorator->getOperator($this->contactSegmentFilterCrate); } /** @@ -91,7 +104,7 @@ public function getOperator() */ public function getField() { - return $this->filterDecorator->getField($this->leadSegmentFilterCrate); + return $this->filterDecorator->getField($this->contactSegmentFilterCrate); } /** @@ -99,7 +112,7 @@ public function getField() */ public function getTable() { - return $this->filterDecorator->getTable($this->leadSegmentFilterCrate); + return $this->filterDecorator->getTable($this->contactSegmentFilterCrate); } /** @@ -109,7 +122,7 @@ public function getTable() */ public function getParameterHolder($argument) { - return $this->filterDecorator->getParameterHolder($this->leadSegmentFilterCrate, $argument); + return $this->filterDecorator->getParameterHolder($this->contactSegmentFilterCrate, $argument); } /** @@ -117,7 +130,7 @@ public function getParameterHolder($argument) */ public function getParameterValue() { - return $this->filterDecorator->getParameterValue($this->leadSegmentFilterCrate); + return $this->filterDecorator->getParameterValue($this->contactSegmentFilterCrate); } /** @@ -125,7 +138,7 @@ public function getParameterValue() */ public function getWhere() { - return $this->filterDecorator->getWhere($this->leadSegmentFilterCrate); + return $this->filterDecorator->getWhere($this->contactSegmentFilterCrate); } /** @@ -133,7 +146,7 @@ public function getWhere() */ public function getGlue() { - return $this->leadSegmentFilterCrate->getGlue(); + return $this->contactSegmentFilterCrate->getGlue(); } /** @@ -141,11 +154,11 @@ public function getGlue() */ public function getAggregateFunction() { - return $this->filterDecorator->getAggregateFunc($this->leadSegmentFilterCrate); + return $this->filterDecorator->getAggregateFunc($this->contactSegmentFilterCrate); } /** - * @todo check where necessary and remove after debugging is done + * @todo remove this, create functions to replace need for this * * @param null $field * @@ -157,7 +170,7 @@ public function getAggregateFunction() */ public function getCrate($field = null) { - $fields = (array) $this->toArray(); + $fields = (array)$this->toArray(); if (is_null($field)) { return $fields; @@ -167,7 +180,7 @@ public function getCrate($field = null) return $fields[$field]; } - throw new \Exception('Unknown crate field "'.$field."'"); + throw new \Exception('Unknown crate field "' . $field . "'"); } /** @@ -176,14 +189,14 @@ public function getCrate($field = null) public function toArray() { return [ - 'glue' => $this->leadSegmentFilterCrate->getGlue(), - 'field' => $this->leadSegmentFilterCrate->getField(), - 'object' => $this->leadSegmentFilterCrate->getObject(), - 'type' => $this->leadSegmentFilterCrate->getType(), - 'filter' => $this->leadSegmentFilterCrate->getFilter(), - 'display' => $this->leadSegmentFilterCrate->getDisplay(), - 'operator' => $this->leadSegmentFilterCrate->getOperator(), - 'func' => $this->leadSegmentFilterCrate->getFunc(), + 'glue' => $this->contactSegmentFilterCrate->getGlue(), + 'field' => $this->contactSegmentFilterCrate->getField(), + 'object' => $this->contactSegmentFilterCrate->getObject(), + 'type' => $this->contactSegmentFilterCrate->getType(), + 'filter' => $this->contactSegmentFilterCrate->getFilter(), + 'display' => $this->contactSegmentFilterCrate->getDisplay(), + 'operator' => $this->contactSegmentFilterCrate->getOperator(), + 'func' => $this->contactSegmentFilterCrate->getFunc(), 'aggr' => $this->getAggregateFunction(), ]; } @@ -196,8 +209,12 @@ public function getFilterQueryBuilder() return $this->filterQueryBuilder; } + /** + * String representation of the object + * * @return string + * @throws \Exception */ public function __toString() { @@ -230,6 +247,9 @@ public function applyQuery(QueryBuilder $queryBuilder) } /** + * Whether the filter references another ContactSegment + * + * @todo replace if not used * @return bool */ public function isContactSegmentReference() diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php similarity index 88% rename from app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php rename to app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 92cc21f42b1..11cd5350f0b 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -11,9 +11,9 @@ namespace Mautic\LeadBundle\Segment; -class LeadSegmentFilterCrate +class ContactSegmentFilterCrate { - const LEAD_OBJECT = 'lead'; + const CONTACT_OBJECT = 'lead'; const COMPANY_OBJECT = 'company'; /** @@ -56,11 +56,16 @@ class LeadSegmentFilterCrate */ private $func; + /** + * ContactSegmentFilterCrate constructor. + * + * @param array $filter + */ public function __construct(array $filter) { $this->glue = isset($filter['glue']) ? $filter['glue'] : null; $this->field = isset($filter['field']) ? $filter['field'] : null; - $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; + $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; $this->display = isset($filter['display']) ? $filter['display'] : null; $this->func = isset($filter['func']) ? $filter['func'] : null; @@ -95,9 +100,9 @@ public function getObject() /** * @return bool */ - public function isLeadType() + public function isContactType() { - return $this->object === self::LEAD_OBJECT; + return $this->object === self::CONTACT_OBJECT; } /** @@ -148,11 +153,17 @@ public function getFunc() return $this->func; } + /** + * @return bool + */ public function isDateType() { return $this->getType() === 'date' || $this->hasTimeParts(); } + /** + * @return bool + */ public function hasTimeParts() { return $this->getType() === 'datetime'; diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php similarity index 54% rename from app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php rename to app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php index 54202357e01..b8739e5a28a 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php @@ -17,7 +17,7 @@ use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface; use Symfony\Component\DependencyInjection\Container; -class LeadSegmentFilterFactory +class ContactSegmentFilterFactory { /** * @var TableSchemaColumnsCache @@ -34,6 +34,13 @@ class LeadSegmentFilterFactory */ private $decoratorFactory; + /** + * ContactSegmentFilterFactory constructor. + * + * @param TableSchemaColumnsCache $schemaCache + * @param Container $container + * @param DecoratorFactory $decoratorFactory + */ public function __construct( TableSchemaColumnsCache $schemaCache, Container $container, @@ -47,38 +54,40 @@ public function __construct( /** * @param LeadList $leadList * - * @return LeadSegmentFilters + * @return ContactSegmentFilters + * @throws \Exception */ - public function getLeadListFilters(LeadList $leadList) + public function getSegmentFilters(LeadList $leadList) { - $leadSegmentFilters = new LeadSegmentFilters(); + $contactSegmentFilters = new ContactSegmentFilters(); $filters = $leadList->getFilters(); foreach ($filters as $filter) { - // LeadSegmentFilterCrate is for accessing $filter as an object - $leadSegmentFilterCrate = new LeadSegmentFilterCrate($filter); + // ContactSegmentFilterCrate is for accessing $filter as an object + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $decorator = $this->decoratorFactory->getDecoratorForFilter($leadSegmentFilterCrate); + $decorator = $this->decoratorFactory->getDecoratorForFilter($contactSegmentFilterCrate); - $filterQueryBuilder = $this->getQueryBuilderForFilter($decorator, $leadSegmentFilterCrate); + $filterQueryBuilder = $this->getQueryBuilderForFilter($decorator, $contactSegmentFilterCrate); - $leadSegmentFilter = new LeadSegmentFilter($leadSegmentFilterCrate, $decorator, $this->schemaCache, $filterQueryBuilder); + $contactSegmentFilter = new ContactSegmentFilter($contactSegmentFilterCrate, $decorator, $this->schemaCache, $filterQueryBuilder); - $leadSegmentFilters->addLeadSegmentFilter($leadSegmentFilter); + $contactSegmentFilters->addContactSegmentFilter($contactSegmentFilter); } - return $leadSegmentFilters; + return $contactSegmentFilters; } /** - * @param FilterDecoratorInterface $decorator - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * @param FilterDecoratorInterface $decorator + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * - * @return FilterQueryBuilderInterface + * @return object + * @throws \Exception */ - private function getQueryBuilderForFilter(FilterDecoratorInterface $decorator, LeadSegmentFilterCrate $leadSegmentFilterCrate) + private function getQueryBuilderForFilter(FilterDecoratorInterface $decorator, ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $qbServiceId = $decorator->getQueryType($leadSegmentFilterCrate); + $qbServiceId = $decorator->getQueryType($contactSegmentFilterCrate); return $this->container->get($qbServiceId); } diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php similarity index 78% rename from app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php rename to app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php index 8b25b1aec45..1c60bcdcc03 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentFilterOperator.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php @@ -1,7 +1,7 @@ leadSegmentFilters[] = $leadSegmentFilter; -// if ($leadSegmentFilter->isCompanyType()) { -// $this->hasCompanyFilter = true; -// // Must tell getLeadsByList how to best handle the relationship with the companies table -// if (!in_array($leadSegmentFilter->getFunc(), ['empty', 'neq', 'notIn', 'notLike'], true)) { -// $this->listFiltersInnerJoinCompany = true; -// } -// } + $this->contactSegmentFilters[] = $contactSegmentFilter; + return $this; } /** @@ -50,11 +54,11 @@ public function addLeadSegmentFilter(LeadSegmentFilter $leadSegmentFilter) * * @see http://php.net/manual/en/iterator.current.php * - * @return LeadSegmentFilter + * @return ContactSegmentFilter */ public function current() { - return $this->leadSegmentFilters[$this->position]; + return $this->contactSegmentFilters[$this->position]; } /** @@ -88,7 +92,7 @@ public function key() */ public function valid() { - return isset($this->leadSegmentFilters[$this->position]); + return isset($this->contactSegmentFilters[$this->position]); } /** @@ -110,7 +114,7 @@ public function rewind() */ public function count() { - return count($this->leadSegmentFilters); + return count($this->contactSegmentFilters); } /** diff --git a/app/bundles/LeadBundle/Segment/LeadSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php similarity index 55% rename from app/bundles/LeadBundle/Segment/LeadSegmentService.php rename to app/bundles/LeadBundle/Segment/ContactSegmentService.php index 7b827c36e36..7cbb5f591cd 100644 --- a/app/bundles/LeadBundle/Segment/LeadSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -13,21 +13,21 @@ use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListSegmentRepository; -use Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder; +use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Symfony\Bridge\Monolog\Logger; -class LeadSegmentService +class ContactSegmentService { /** - * @var LeadSegmentFilterFactory + * @var ContactSegmentFilterFactory */ - private $leadSegmentFilterFactory; + private $contactSegmentFilterFactory; /** - * @var LeadSegmentQueryBuilder + * @var ContactSegmentQueryBuilder */ - private $leadSegmentQueryBuilder; + private $contactSegmentQueryBuilder; /** * @var Logger @@ -39,87 +39,75 @@ class LeadSegmentService */ private $preparedQB; - /** - * LeadSegmentService constructor. - * - * @param LeadSegmentFilterFactory $leadSegmentFilterFactory - * @param LeadListSegmentRepository $leadListSegmentRepository - * @param LeadSegmentQueryBuilder $queryBuilder - * @param Logger $logger - */ + public function __construct( - LeadSegmentFilterFactory $leadSegmentFilterFactory, - LeadSegmentQueryBuilder $queryBuilder, + ContactSegmentFilterFactory $contactSegmentFilterFactory, + ContactSegmentQueryBuilder $queryBuilder, Logger $logger ) { - $this->leadSegmentFilterFactory = $leadSegmentFilterFactory; - $this->leadSegmentQueryBuilder = $queryBuilder; - $this->logger = $logger; + $this->contactSegmentFilterFactory = $contactSegmentFilterFactory; + $this->contactSegmentQueryBuilder = $queryBuilder; + $this->logger = $logger; } /** - * @param LeadList $leadList + * @param LeadList $segment * @param $batchLimiters * - * @return Query\QueryBuilder|QueryBuilder + * @return QueryBuilder + * @throws Exception\SegmentQueryException + * @throws \Exception */ - private function getNewLeadListLeadsQuery(LeadList $leadList, $batchLimiters) + private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) { if (!is_null($this->preparedQB)) { return $this->preparedQB; } - $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); - $queryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); - //dump($queryBuilder->getQueryPart('where')); - $queryBuilder = $this->leadSegmentQueryBuilder->addNewLeadsRestrictions($queryBuilder, $leadList->getId(), $batchLimiters); - //dump($queryBuilder->getQueryPart('where')); - $queryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $leadList->getId()); - //dump($queryBuilder->getQueryPart('where')); - //dump($queryBuilder->getSQL()); - //dump($queryBuilder->getParameters()); - //dump($queryBuilder->execute()); - //exit; + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $segment->getId()); return $queryBuilder; } /** - * @param LeadList $leadList + * @param LeadList $segment * @param array $batchLimiters * * @return array * * @throws \Exception */ - public function getNewLeadListLeadsCount(LeadList $leadList, array $batchLimiters) + public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters) { - $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); if (!count($segmentFilters)) { - $this->logger->debug('Segment QB: Segment has no filters', ['segmentId' => $leadList->getId()]); + $this->logger->debug('Segment QB: Segment has no filters', ['segmentId' => $segment->getId()]); - return [$leadList->getId() => [ + return [$segment->getId() => [ 'count' => '0', 'maxId' => '0', ], ]; } - $qb = $this->getNewLeadListLeadsQuery($leadList, $batchLimiters); + $qb = $this->getNewSegmentContactsQuery($segment, $batchLimiters); - $qb = $this->leadSegmentQueryBuilder->wrapInCount($qb); + $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); - $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $leadList->getId()]); + $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); - $result = $this->timedFetch($qb, $leadList->getId()); + $result = $this->timedFetch($qb, $segment->getId()); - return [$leadList->getId() => $result]; + return [$segment->getId() => $result]; } /** - * @param LeadList $leadList + * @param LeadList $segment * @param array $batchLimiters * @param int $limit * @@ -127,12 +115,12 @@ public function getNewLeadListLeadsCount(LeadList $leadList, array $batchLimiter * * @throws \Exception */ - public function getNewLeadListLeads(LeadList $leadList, array $batchLimiters, $limit = 1000) + public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $limit = 1000) { - $queryBuilder = $this->getNewLeadListLeadsQuery($leadList, $batchLimiters); + $queryBuilder = $this->getNewSegmentContactsQuery($segment, $batchLimiters); $queryBuilder->select('DISTINCT l.id'); - $this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); + $this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); $queryBuilder->setMaxResults($limit); @@ -153,72 +141,70 @@ public function getNewLeadListLeads(LeadList $leadList, array $batchLimiters, $l ); } - $result = $this->timedFetchAll($queryBuilder, $leadList->getId()); + $result = $this->timedFetchAll($queryBuilder, $segment->getId()); - return [$leadList->getId() => $result]; + return [$segment->getId() => $result]; } /** - * @param LeadList $leadList + * @param LeadList $segment * * @return QueryBuilder + * @throws Exception\SegmentQueryException + * @throws \Exception */ - private function getOrphanedLeadListLeadsQueryBuilder(LeadList $leadList) + private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) { - $segmentFilters = $this->leadSegmentFilterFactory->getLeadListFilters($leadList); + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); - $queryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); - $queryBuilder->rightJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp', 'l.id = orp.lead_id and orp.leadlist_id = '.$leadList->getId()); + $queryBuilder->rightJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp', 'l.id = orp.lead_id and orp.leadlist_id = '.$segment->getId()); $queryBuilder->andWhere($queryBuilder->expr()->andX( $queryBuilder->expr()->isNull('l.id'), - $queryBuilder->expr()->eq('orp.leadlist_id', $leadList->getId()) + $queryBuilder->expr()->eq('orp.leadlist_id', $segment->getId()) )); - $queryBuilder->select($queryBuilder->guessPrimaryLeadIdColumn().' as id'); + $queryBuilder->select($queryBuilder->guessPrimaryLeadContactIdColumn().' as id'); return $queryBuilder; } /** - * @param LeadList $leadList - * @param array $batchLimiters - * @param int $limit + * @param LeadList $segment * * @return array - * + * @throws Exception\SegmentQueryException * @throws \Exception */ - public function getOrphanedLeadListLeadsCount(LeadList $leadList) + public function getOrphanedLeadListLeadsCount(LeadList $segment) { - $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($leadList); - $queryBuilder = $this->leadSegmentQueryBuilder->wrapInCount($queryBuilder); + $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment); + $queryBuilder = $this->contactSegmentQueryBuilder->wrapInCount($queryBuilder); - $this->logger->debug('Segment QB: Orphan Leads Count SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); + $this->logger->debug('Segment QB: Orphan Leads Count SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); - $result = $this->timedFetch($queryBuilder, $leadList->getId()); + $result = $this->timedFetch($queryBuilder, $segment->getId()); - return [$leadList->getId() => $result]; + return [$segment->getId() => $result]; } /** - * @param LeadList $leadList - * @param array $batchLimiters - * @param int $limit + * @param LeadList $segment * * @return array - * + * @throws Exception\SegmentQueryException * @throws \Exception */ - public function getOrphanedLeadListLeads(LeadList $leadList) + public function getOrphanedLeadListLeads(LeadList $segment) { - $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($leadList); + $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment); - $this->logger->debug('Segment QB: Orphan Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $leadList->getId()]); + $this->logger->debug('Segment QB: Orphan Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); - $result = $this->timedFetchAll($queryBuilder, $leadList->getId()); + $result = $this->timedFetchAll($queryBuilder, $segment->getId()); - return [$leadList->getId() => $result]; + return [$segment->getId() => $result]; } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 161eece30ae..33ad5fba2f7 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -1,7 +1,7 @@ leadSegmentFilterOperator = $leadSegmentFilterOperator; + $this->contactSegmentFilterOperator = $contactSegmentFilterOperator; } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|string + */ + public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $leadSegmentFilterCrate->getField(); + return $contactSegmentFilterCrate->getField(); } - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ + public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - if ($leadSegmentFilterCrate->isLeadType()) { + if ($contactSegmentFilterCrate->isContactType()) { return MAUTIC_TABLE_PREFIX.'leads'; } return MAUTIC_TABLE_PREFIX.'companies'; } - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ + public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $operator = $this->leadSegmentFilterOperator->fixOperator($leadSegmentFilterCrate->getOperator()); + $operator = $this->contactSegmentFilterOperator->fixOperator($contactSegmentFilterCrate->getOperator()); switch ($operator) { case 'startsWith': @@ -60,17 +83,28 @@ public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) return $operator; } - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ + public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return BaseFilterQueryBuilder::getServiceId(); } - public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * @param $argument + * + * @return array|string + */ + public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { if (is_array($argument)) { $result = []; foreach ($argument as $arg) { - $result[] = $this->getParameterHolder($leadSegmentFilterCrate, $arg); + $result[] = $this->getParameterHolder($contactSegmentFilterCrate, $arg); } return $result; @@ -79,18 +113,23 @@ public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrat return ':'.$argument; } - public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return array|bool|float|mixed|null|string + */ + public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $filter = $leadSegmentFilterCrate->getFilter(); + $filter = $contactSegmentFilterCrate->getFilter(); - switch ($leadSegmentFilterCrate->getType()) { + switch ($contactSegmentFilterCrate->getType()) { case 'number': return (float) $filter; case 'boolean': return (bool) $filter; } - switch ($this->getOperator($leadSegmentFilterCrate)) { + switch ($this->getOperator($contactSegmentFilterCrate)) { case 'like': case 'notLike': return strpos($filter, '%') === false ? '%'.$filter.'%' : $filter; @@ -108,12 +147,20 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate return $filter; } - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return bool + */ + public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return false; } - public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + */ + public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return null; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php index 2da99114c45..a2ed78adbe6 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -1,7 +1,7 @@ leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; + parent::__construct($contactSegmentFilterOperator); + $this->dictionary = $contactSegmentFilterDictionary; } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|string + */ + public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); + $originalField = $contactSegmentFilterCrate->getField(); - if (empty($this->leadSegmentFilterDescriptor[$originalField]['field'])) { - return parent::getField($leadSegmentFilterCrate); + if (empty($this->dictionary[$originalField]['field'])) { + return parent::getField($contactSegmentFilterCrate); } - return $this->leadSegmentFilterDescriptor[$originalField]['field']; + return $this->dictionary[$originalField]['field']; } - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ + public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); + $originalField = $contactSegmentFilterCrate->getField(); - if (empty($this->leadSegmentFilterDescriptor[$originalField]['foreign_table'])) { - return parent::getTable($leadSegmentFilterCrate); + if (empty($this->dictionary[$originalField]['foreign_table'])) { + return parent::getTable($contactSegmentFilterCrate); } - return MAUTIC_TABLE_PREFIX.$this->leadSegmentFilterDescriptor[$originalField]['foreign_table']; + return MAUTIC_TABLE_PREFIX.$this->dictionary[$originalField]['foreign_table']; } - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ + public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); + $originalField = $contactSegmentFilterCrate->getField(); - if (!isset($this->leadSegmentFilterDescriptor[$originalField]['type'])) { - return parent::getQueryType($leadSegmentFilterCrate); + if (!isset($this->dictionary[$originalField]['type'])) { + return parent::getQueryType($contactSegmentFilterCrate); } - return $this->leadSegmentFilterDescriptor[$originalField]['type']; + return $this->dictionary[$originalField]['type']; } - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return bool + */ + public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); + $originalField = $contactSegmentFilterCrate->getField(); - return isset($this->leadSegmentFilterDescriptor[$originalField]['func']) ? - $this->leadSegmentFilterDescriptor[$originalField]['func'] : false; + return isset($this->dictionary[$originalField]['func']) ? + $this->dictionary[$originalField]['func'] : false; } - public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + */ + public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $originalField = $leadSegmentFilterCrate->getField(); + $originalField = $contactSegmentFilterCrate->getField(); - if (!isset($this->leadSegmentFilterDescriptor[$originalField]['where'])) { - return parent::getWhere($leadSegmentFilterCrate); + if (!isset($this->dictionary[$originalField]['where'])) { + return parent::getWhere($contactSegmentFilterCrate); } - return $this->leadSegmentFilterDescriptor[$originalField]['where']; + return $this->dictionary[$originalField]['where']; } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 327f9c49476..00181db83d2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -12,9 +12,9 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; use Mautic\CoreBundle\Helper\DateTimeHelper; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; abstract class DateOptionAbstract implements FilterDecoratorInterface { @@ -70,37 +70,37 @@ abstract protected function getValueForBetweenRange(); /** * This function returns an operator if between range is needed. Could return like or between. * - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * @param ContactSegmentFilterCrate $leadSegmentFilterCrate * * @return string */ - abstract protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate); + abstract protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate); - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getField($leadSegmentFilterCrate); + return $this->dateDecorator->getField($contactSegmentFilterCrate); } - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getTable($leadSegmentFilterCrate); + return $this->dateDecorator->getTable($contactSegmentFilterCrate); } - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { if ($this->dateOptionParameters->isBetweenRequired()) { - return $this->getOperatorForBetweenRange($leadSegmentFilterCrate); + return $this->getOperatorForBetweenRange($contactSegmentFilterCrate); } - return $this->dateDecorator->getOperator($leadSegmentFilterCrate); + return $this->dateDecorator->getOperator($contactSegmentFilterCrate); } - public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { - return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } - public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { $this->modifyBaseDate(); @@ -119,18 +119,18 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate return $this->dateTimeHelper->toUtcString($dateFormat); } - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } - public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + return $this->dateDecorator->getWhere($contactSegmentFilterCrate); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index 054dd3eadc3..32da13c3ad7 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -12,6 +12,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; use Mautic\CoreBundle\Helper\DateTimeHelper; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday; @@ -29,7 +30,6 @@ use Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; use Mautic\LeadBundle\Segment\RelativeDate; class DateOptionFactory @@ -53,11 +53,11 @@ public function __construct( } /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * @param ContactSegmentFilterCrate $leadSegmentFilterCrate * * @return FilterDecoratorInterface */ - public function getDateOption(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) { $originalValue = $leadSegmentFilterCrate->getFilter(); $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php index 7bac94f7efb..ee2331778a3 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; class DateOptionParameters { @@ -36,10 +36,10 @@ class DateOptionParameters private $includeMidnigh; /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * @param array $relativeDateStrings + * @param ContactSegmentFilterCrate $leadSegmentFilterCrate + * @param array $relativeDateStrings */ - public function __construct(LeadSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) + public function __construct(ContactSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) { $this->hasTimePart = $leadSegmentFilterCrate->hasTimeParts(); $this->timeframe = $this->parseTimeFrame($leadSegmentFilterCrate, $relativeDateStrings); @@ -80,12 +80,12 @@ public function shouldIncludeMidnigh() } /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate - * @param array $relativeDateStrings + * @param ContactSegmentFilterCrate $leadSegmentFilterCrate + * @param array $relativeDateStrings * * @return string */ - private function parseTimeFrame(LeadSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) + private function parseTimeFrame(ContactSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) { $key = array_search($leadSegmentFilterCrate->getFilter(), $relativeDateStrings, true); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php index 41015610007..ac596545f2a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php @@ -11,8 +11,8 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; abstract class DateDayAbstract extends DateOptionAbstract { @@ -35,7 +35,7 @@ protected function getValueForBetweenRange() /** * {@inheritdoc} */ - protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate) { return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notLike' : 'like'; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php index 354f4cbdcaa..57f2be42cc4 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php @@ -11,8 +11,8 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; abstract class DateMonthAbstract extends DateOptionAbstract { @@ -35,7 +35,7 @@ protected function getValueForBetweenRange() /** * {@inheritdoc} */ - protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate) { return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notLike' : 'like'; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index c4c2b08a12b..f79341786d1 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -11,9 +11,9 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; class DateAnniversary implements FilterDecoratorInterface { @@ -27,43 +27,43 @@ public function __construct(DateDecorator $dateDecorator) $this->dateDecorator = $dateDecorator; } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getField($leadSegmentFilterCrate); + return $this->dateDecorator->getField($contactSegmentFilterCrate); } - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getTable($leadSegmentFilterCrate); + return $this->dateDecorator->getTable($contactSegmentFilterCrate); } - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return 'like'; } - public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { - return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } - public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return '%'.date('-m-d'); } - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } - public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + return $this->dateDecorator->getWhere($contactSegmentFilterCrate); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php index 63326fcc0c4..5fb866c71a8 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php @@ -11,9 +11,9 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; class DateDefault implements FilterDecoratorInterface { @@ -37,43 +37,43 @@ public function __construct(DateDecorator $dateDecorator, $originalValue) $this->originalValue = $originalValue; } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getField($leadSegmentFilterCrate); + return $this->dateDecorator->getField($contactSegmentFilterCrate); } - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getTable($leadSegmentFilterCrate); + return $this->dateDecorator->getTable($contactSegmentFilterCrate); } - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getOperator($leadSegmentFilterCrate); + return $this->dateDecorator->getOperator($contactSegmentFilterCrate); } - public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { - return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } - public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->originalValue; } - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } - public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + return $this->dateDecorator->getWhere($contactSegmentFilterCrate); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php index d3801d07316..f4264979a19 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php @@ -11,9 +11,9 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Other; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\DateDecorator; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; class DateRelativeInterval implements FilterDecoratorInterface { @@ -37,39 +37,39 @@ public function __construct(DateDecorator $dateDecorator, $originalValue) $this->originalValue = $originalValue; } - public function getField(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getField($leadSegmentFilterCrate); + return $this->dateDecorator->getField($contactSegmentFilterCrate); } - public function getTable(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getTable($leadSegmentFilterCrate); + return $this->dateDecorator->getTable($contactSegmentFilterCrate); } - public function getOperator(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - if ($leadSegmentFilterCrate->getOperator() === '=') { + if ($contactSegmentFilterCrate->getOperator() === '=') { return 'like'; } - if ($leadSegmentFilterCrate->getOperator() === '!=') { + if ($contactSegmentFilterCrate->getOperator() === '!=') { return 'notLike'; } - return $this->dateDecorator->getOperator($leadSegmentFilterCrate); + return $this->dateDecorator->getOperator($contactSegmentFilterCrate); } - public function getParameterHolder(LeadSegmentFilterCrate $leadSegmentFilterCrate, $argument) + public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { - return $this->dateDecorator->getParameterHolder($leadSegmentFilterCrate, $argument); + return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } - public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { $date = new \DateTime('now'); $date->modify($this->originalValue); - $operator = $this->getOperator($leadSegmentFilterCrate); + $operator = $this->getOperator($contactSegmentFilterCrate); $format = 'Y-m-d'; if ($operator === 'like' || $operator === 'notLike') { $format .= '%'; @@ -78,18 +78,18 @@ public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate return $date->format($format); } - public function getQueryType(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getQueryType($leadSegmentFilterCrate); + return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } - public function getAggregateFunc(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getAggregateFunc($leadSegmentFilterCrate); + return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } - public function getWhere(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - return $this->dateDecorator->getWhere($leadSegmentFilterCrate); + return $this->dateDecorator->getWhere($contactSegmentFilterCrate); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php index 8693b117313..3d11e72b699 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php @@ -11,8 +11,8 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; abstract class DateWeekAbstract extends DateOptionAbstract { @@ -42,7 +42,7 @@ protected function getValueForBetweenRange() /** * {@inheritdoc} */ - protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate) { return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notBetween' : 'between'; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php index 83d0b6da7bc..89737319319 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php @@ -11,8 +11,8 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; abstract class DateYearAbstract extends DateOptionAbstract { @@ -35,7 +35,7 @@ protected function getValueForBetweenRange() /** * {@inheritdoc} */ - protected function getOperatorForBetweenRange(LeadSegmentFilterCrate $leadSegmentFilterCrate) + protected function getOperatorForBetweenRange(ContactSegmentFilterCrate $leadSegmentFilterCrate) { return $leadSegmentFilterCrate->getOperator() === '!=' ? 'notLike' : 'like'; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index d259af2ddd2..9e4d3cf9487 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -11,16 +11,21 @@ namespace Mautic\LeadBundle\Segment\Decorator; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; +/** + * Class DateDecorator. + */ class DateDecorator extends CustomMappedDecorator { /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @todo @petr please check this method * * @throws \Exception */ - public function getParameterValue(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { throw new \Exception('Instance of Date option need to implement this function'); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php b/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php index c07ddc6742d..601d732bfed 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DecoratorFactory.php @@ -11,16 +11,19 @@ namespace Mautic\LeadBundle\Segment\Decorator; +use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory; -use Mautic\LeadBundle\Segment\LeadSegmentFilterCrate; -use Mautic\LeadBundle\Services\LeadSegmentFilterDescriptor; +use Mautic\LeadBundle\Services\ContactSegmentFilterDictionary; +/** + * Class DecoratorFactory. + */ class DecoratorFactory { /** - * @var LeadSegmentFilterDescriptor + * @var ContactSegmentFilterDictionary */ - private $leadSegmentFilterDescriptor; + private $contactSegmentFilterDictionary; /** * @var BaseDecorator @@ -37,32 +40,40 @@ class DecoratorFactory */ private $dateOptionFactory; + /** + * DecoratorFactory constructor. + * + * @param ContactSegmentFilterDictionary $contactSegmentFilterDictionary + * @param BaseDecorator $baseDecorator + * @param CustomMappedDecorator $customMappedDecorator + * @param DateOptionFactory $dateOptionFactory + */ public function __construct( - LeadSegmentFilterDescriptor $leadSegmentFilterDescriptor, + ContactSegmentFilterDictionary $contactSegmentFilterDictionary, BaseDecorator $baseDecorator, CustomMappedDecorator $customMappedDecorator, DateOptionFactory $dateOptionFactory ) { - $this->baseDecorator = $baseDecorator; - $this->customMappedDecorator = $customMappedDecorator; - $this->dateOptionFactory = $dateOptionFactory; - $this->leadSegmentFilterDescriptor = $leadSegmentFilterDescriptor; + $this->baseDecorator = $baseDecorator; + $this->customMappedDecorator = $customMappedDecorator; + $this->dateOptionFactory = $dateOptionFactory; + $this->contactSegmentFilterDictionary = $contactSegmentFilterDictionary; } /** - * @param LeadSegmentFilterCrate $leadSegmentFilterCrate + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * * @return FilterDecoratorInterface */ - public function getDecoratorForFilter(LeadSegmentFilterCrate $leadSegmentFilterCrate) + public function getDecoratorForFilter(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - if ($leadSegmentFilterCrate->isDateType()) { - return $this->dateOptionFactory->getDateOption($leadSegmentFilterCrate); + if ($contactSegmentFilterCrate->isDateType()) { + return $this->dateOptionFactory->getDateOption($contactSegmentFilterCrate); } - $originalField = $leadSegmentFilterCrate->getField(); + $originalField = $contactSegmentFilterCrate->getField(); - if (empty($this->leadSegmentFilterDescriptor[$originalField])) { + if (empty($this->contactSegmentFilterDictionary[$originalField])) { return $this->baseDecorator; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index 97f415ec483..814e3881fae 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -1,7 +1,7 @@ glue = isset($filter['glue']) ? $filter['glue'] : null; - $this->field = isset($filter['field']) ? $filter['field'] : null; - $this->object = isset($filter['object']) ? $filter['object'] : self::LEAD_OBJECT; - $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->display = isset($filter['display']) ? $filter['display'] : null; - $this->func = isset($filter['func']) ? $filter['func'] : null; - $operatorValue = isset($filter['operator']) ? $filter['operator'] : null; - $this->setOperator($operatorValue); - - $filterValue = isset($filter['filter']) ? $filter['filter'] : null; - $this->setFilter($filterValue); - $this->em = $em; - if (!is_null($dictionary)) { - $this->translateQueryDescription($dictionary); - } - } - - /** - * @return string - * - * @throws \Exception - */ - public function getSQLOperator() - { - switch ($this->getOperator()) { - case 'gt': - return '>'; - case 'eq': - return '='; - case 'gt': - return '>'; - case 'gte': - return '>='; - case 'lt': - return '<'; - case 'lte': - return '<='; - } - throw new \Exception(sprintf('Unknown operator \'%s\'.', $this->getOperator())); - } - - public function getFilterConditionValue($argument = null) - { - switch ($this->getDBColumn()->getType()->getName()) { - case 'number': - case 'integer': - case 'float': - return ':'.$argument; - case 'datetime': - case 'date': - return sprintf('":%s"', $argument); - case 'text': - case 'string': - switch ($this->getFunc()) { - case 'eq': - case 'ne': - case 'neq': - return sprintf("':%s'", $argument); - default: - throw new \Exception('Unknown operator '.$this->getFunc()); - } - default: - var_dump($this->getDBColumn()->getType()->getName()); - } - throw new \Exception(sprintf('Unknown value type \'%s\'.', $filter->getName())); - } - - public function createQuery(QueryBuilder $queryBuilder, $alias = false) - { - dump('creating query:'.$this->getObject()); - $glueFunc = $this->getGlue().'Where'; - - $parameterName = $this->generateRandomParameterName(); - - $queryBuilder = $this->createExpression($queryBuilder, $parameterName, $this->getFunc()); - - $queryBuilder->setParameter($parameterName, $this->getFilter()); - - dump($queryBuilder->getSQL()); - - return $queryBuilder; - } - - public function createExpression(QueryBuilder $queryBuilder, $parameterName, $func = null) - { - dump('creating query:'.$this->getField()); - $func = is_null($func) ? $this->getFunc() : $func; - $alias = $this->getTableAlias($this->getEntityName(), $queryBuilder); - $desc = $this->getQueryDescription(); - if (!$alias) { - if ($desc['func']) { - $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($desc['func'].'('.$alias.'.'.$this->getDBColumn()->getName().')', $this->getFilterConditionValue($parameterName)); - $queryBuilder = $queryBuilder->andHaving($expr); - } else { - if ($alias != 'l') { - $queryBuilder = $this->createJoin($queryBuilder, $this->getEntityName(), $alias = $this->generateRandomParameterName()); - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); - } else { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); - die(); - $queryBuilder = $queryBuilder->andWhere($expr); - } - } - } else { - if ($alias != 'l') { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $this->AddJoinCondition($queryBuilder, $alias, $expr); - } else { - $expr = $queryBuilder->expr()->$func($alias.'.'.$this->getDBColumn()->getName(), $this->getFilterConditionValue($parameterName)); - $queryBuilder = $queryBuilder->andWhere($expr); - } - } - - return $queryBuilder; - } - - public function getDBTable() - { - //@todo cache metadata - try { - $tableName = $this->em->getClassMetadata($this->getEntityName())->getTableName(); - } catch (MappingException $e) { - return $this->getObject(); - } - - return $tableName; - } - - public function getEntityName() - { - $converter = new CamelCaseToSnakeCaseNameConverter(); - if ($this->getQueryDescription()) { - $table = $this->queryDescription['foreign_table']; - } else { - $table = $this->getObject(); - } - - $entity = sprintf('MauticLeadBundle:%s', ucfirst($converter->denormalize($table))); - - return $entity; - } - - /** - * @return Column - * - * @throws \Exception - */ - public function getDBColumn() - { - if (is_null($this->dbColumn)) { - if ($descr = $this->getQueryDescription()) { - $this->dbColumn = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->queryDescription['foreign_table'])[$this->queryDescription['field']]; - } else { - $dbTableColumns = $this->em->getConnection()->getSchemaManager()->listTableColumns($this->getDBTable()); - if (!$dbTableColumns) { - throw new \Exception('Unknown database table and no translation provided for type "'.$this->getType().'"'); - } - if (!isset($dbTableColumns[$this->getField()])) { - throw new \Exception('Unknown database column and no translation provided for type "'.$this->getType().'"'); - } - $this->dbColumn = $dbTableColumns[$this->getField()]; - } - } - - return $this->dbColumn; - } - - /** - * @return string|null - */ - public function getGlue() - { - return $this->glue; - } - - /** - * @return string|null - */ - public function getField() - { - return $this->field; - } - - /** - * @return string|null - */ - public function getObject() - { - return $this->object; - } - - /** - * @return bool - */ - public function isLeadType() - { - return $this->object === self::LEAD_OBJECT; - } - - /** - * @return bool - */ - public function isCompanyType() - { - return $this->object === self::COMPANY_OBJECT; - } - - /** - * @return string|null - */ - public function getType() - { - return $this->type; - } - - /** - * @return string|array|null - */ - public function getFilter() - { - return $this->filter; - } - - /** - * @return string|null - */ - public function getDisplay() - { - return $this->display; - } - - /** - * @return string|null - */ - public function getOperator() - { - return $this->operator; - } - - /** - * @param string|null $operator - */ - public function setOperator($operator) - { - $this->operator = $operator; - } - - /** - * @param string|array|bool|float|null $filter - */ - public function setFilter($filter) - { - $filter = $this->sanitizeFilter($filter); - - $this->filter = $filter; - } - - /** - * @return string - */ - public function getFunc() - { - return $this->func; - } - - /** - * @param string $func - */ - public function setFunc($func) - { - $this->func = $func; - } - - /** - * @return array - */ - public function toArray() - { - return [ - 'glue' => $this->getGlue(), - 'field' => $this->getField(), - 'object' => $this->getObject(), - 'type' => $this->getType(), - 'filter' => $this->getFilter(), - 'display' => $this->getDisplay(), - 'operator' => $this->getOperator(), - 'func' => $this->getFunc(), - ]; - } - - /** - * @param string|array|bool|float|null $filter - * - * @return string|array|bool|float|null - */ - private function sanitizeFilter($filter) - { - if ($filter === null || is_array($filter) || !$this->getType()) { - return $filter; - } - - switch ($this->getType()) { - case 'number': - $filter = (float) $filter; - break; - - case 'boolean': - $filter = (bool) $filter; - break; - } - - return $filter; - } - - /** - * @return array - */ - public function getQueryDescription($dictionary = null) - { - if (is_null($this->queryDescription)) { - $this->translateQueryDescription($dictionary); - } - - return $this->queryDescription; - } - - /** - * @param array $queryDescription - * - * @return LeadSegmentFilter - */ - public function setQueryDescription($queryDescription) - { - $this->queryDescription = $queryDescription; - - return $this; - } - - /** - * @return $this - */ - public function translateQueryDescription(\ArrayIterator $dictionary = null) - { - $this->queryDescription = isset($dictionary[$this->getField()]) - ? $dictionary[$this->getField()] - : false; - - return $this; - } -} diff --git a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php similarity index 89% rename from app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 7e12fe5b3c6..c2eada9faa8 100644 --- a/app/bundles/LeadBundle/Segment/Query/LeadSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -3,7 +3,6 @@ /* * @copyright 2014-2018 Mautic Contributors. All rights reserved * @author Mautic - * @author Jan Kozak * * @link http://mautic.org * @@ -14,16 +13,16 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; -use Mautic\LeadBundle\Segment\LeadSegmentFilters; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilters; use Mautic\LeadBundle\Segment\RandomParameterName; /** - * Class LeadSegmentQueryBuilder is responsible for building queries for segments. + * Class ContactSegmentQueryBuilder is responsible for building queries for segments. * - * @todo add exceptions + * @todo add exceptions, remove related segments */ -class LeadSegmentQueryBuilder +class ContactSegmentQueryBuilder { /** @var EntityManager */ private $entityManager; @@ -38,7 +37,7 @@ class LeadSegmentQueryBuilder private $relatedSegments = []; /** - * LeadSegmentQueryBuilder constructor. + * ContactSegmentQueryBuilder constructor. * * @param EntityManager $entityManager * @param RandomParameterName $randomParameterName @@ -51,14 +50,14 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r } /** - * @param LeadSegmentFilters $leadSegmentFilters - * @param null $backReference + * @param ContactSegmentFilters $contactSegmentFilters + * @param null $backReference * * @return QueryBuilder * * @throws SegmentQueryException */ - public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegmentFilters, $backReference = null) + public function assembleContactsSegmentQueryBuilder(ContactSegmentFilters $contactSegmentFilters, $backReference = null) { /** @var QueryBuilder $queryBuilder */ $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); @@ -67,9 +66,10 @@ public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegm $references = []; - /** @var LeadSegmentFilter $filter */ - foreach ($leadSegmentFilters as $filter) { + /** @var ContactSegmentFilter $filter */ + foreach ($contactSegmentFilters as $filter) { $segmentIdArray = is_array($filter->getParameterValue()) ? $filter->getParameterValue() : [$filter->getParameterValue()]; + // We will handle references differently than regular segments if ($filter->isContactSegmentReference()) { if (!is_null($backReference) || in_array($backReference, $this->getContactSegmentRelations($segmentIdArray))) { @@ -79,6 +79,7 @@ public function assembleContactsSegmentQueryBuilder(LeadSegmentFilters $leadSegm } $queryBuilder = $filter->applyQuery($queryBuilder); } + $queryBuilder->applyStackLogic(); return $queryBuilder; @@ -120,7 +121,7 @@ public function wrapInCount(QueryBuilder $qb) // Add count functions to the query $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); // If there is any right join in the query we need to select its it - $primary = $qb->guessPrimaryLeadIdColumn(); + $primary = $qb->guessPrimaryLeadContactIdColumn(); $currentSelects = []; foreach ($qb->getQueryParts()['select'] as $select) { @@ -145,15 +146,13 @@ public function wrapInCount(QueryBuilder $qb) * Restrict the query to NEW members of segment. * * @param QueryBuilder $queryBuilder - * @param $leadListId + * @param $segmentId * @param $whatever @todo document this field * * @return QueryBuilder */ - public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $whatever) + public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentId, $whatever) { - //$queryBuilder->select('l.id'); - $parts = $queryBuilder->getQueryParts(); $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); @@ -162,7 +161,7 @@ public function addNewLeadsRestrictions(QueryBuilder $queryBuilder, $leadListId, $queryBuilder->addSelect($tableAlias.'.lead_id AS '.$tableAlias.'_lead_id'); $expression = $queryBuilder->expr()->andX( - $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $leadListId), + $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId), $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$whatever['dateTime']."'") ); @@ -244,7 +243,7 @@ public function getTranslator() /** * @param LeadSegmentFilterDescriptor $translator * - * @return LeadSegmentQueryBuilder + * @return ContactSegmentQueryBuilder * * @todo Remove this function */ @@ -268,7 +267,7 @@ public function getSchema() /** * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema * - * @return LeadSegmentQueryBuilder + * @return ContactSegmentQueryBuilder * * @todo Remove this function */ diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php b/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php index a717322e203..256f7552468 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/CompositeExpression.php @@ -20,13 +20,14 @@ namespace Mautic\LeadBundle\Segment\Query\Expression; /** - * Composite expression is responsible to build a group of similar expression. + * Composite expression is responsible to build a group of similar expression. Mautic MOD. * * @see www.doctrine-project.org * @since 2.1 * * @author Guilherme Blanco * @author Benjamin Eberlei + * @author Jan Kozak */ class CompositeExpression implements \Countable { @@ -72,7 +73,7 @@ public function __construct($type, array $parts = []) * * @param array $parts * - * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + * @return self */ public function addMultiple(array $parts = []) { @@ -88,7 +89,7 @@ public function addMultiple(array $parts = []) * * @param mixed $part * - * @return \Doctrine\DBAL\Query\Expression\CompositeExpression + * @return self */ public function add($part) { diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 5e00abfc2cc..09962ecdfd3 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -10,8 +10,8 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; -use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; +use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -42,14 +42,10 @@ public static function getServiceId() return 'mautic.lead.query.builder.basic'; } - public function getLogicGroupingExpression() - { - } - /** * {@inheritdoc} */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { $filterOperator = $filter->getOperator(); $filterGlue = $filter->getGlue(); @@ -75,8 +71,6 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter $filterParametersHolder = $filter->getParameterHolder($parameters); - $filterGlueFunc = $filterGlue.'Where'; - $tableAlias = $queryBuilder->getTableAlias($filter->getTable()); // for aggregate function we need to create new alias and not reuse the old one @@ -184,33 +178,12 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter if ($queryBuilder->isJoinTable($filter->getTable())) { if ($filterAggr) { - throw new \Exception('should not be used use different query builder'); - $queryBuilder->andHaving($expression); + throw new SegmentQueryException('aggregate functions should not be used in basic filters. use different query builder'); } else { $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } } else { - // @todo remove stack logic, move it to the query builder $queryBuilder->addLogic($expression, $filterGlue); - -// if ($filterGlue === 'or') { -// if ($queryBuilder->hasLogicStack()) { -// if ($queryBuilder->hasLogicStack()) { -// $orWhereExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $queryBuilder->popLogicStack()); -// } else { -// $orWhereExpression = $queryBuilder->popLogicStack(); -// } -// -// $queryBuilder->orWhere($orWhereExpression); -// } -// $queryBuilder->addToLogicStack($expression); -// } else { -// if ($queryBuilder->hasLogicStack()) { -// $queryBuilder->addToLogicStack($expression); -// } else { -// $queryBuilder->$filterGlueFunc($expression); -// } -// } } $queryBuilder->setParametersPairs($parameters, $filterParameters); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php similarity index 89% rename from app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index 13519696771..29acd742ce2 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -11,13 +11,13 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Entity\DoNotContact; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; /** - * Class DncFilterQueryBuilder. + * Class DoNotContactFilterQueryBuilder. */ -class DncFilterQueryBuilder extends BaseFilterQueryBuilder +class DoNotContactFilterQueryBuilder extends BaseFilterQueryBuilder { /** * {@inheritdoc} @@ -30,7 +30,7 @@ public static function getServiceId() /** * {@inheritdoc} */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { //@todo look at this, the getCrate method is for debuggin only $parts = explode('_', $filter->getCrate('field')); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php b/app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php index 27cdf071b78..7d7bc29dbaa 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/FilterQueryBuilderInterface.php @@ -10,7 +10,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; /** @@ -19,12 +19,12 @@ interface FilterQueryBuilderInterface { /** - * @param QueryBuilder $queryBuilder - * @param LeadSegmentFilter $filter + * @param QueryBuilder $queryBuilder + * @param ContactSegmentFilter $filter * * @return QueryBuilder */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter); + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter); /** * @return string returns the service id in the DIC container diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 263c9afad02..2cd0ca3acc0 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -10,7 +10,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; @@ -30,7 +30,7 @@ public static function getServiceId() /** * {@inheritdoc} */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { $filterOperator = $filter->getOperator(); $filterGlue = $filter->getGlue(); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 84004e28ec8..c6a902fbf21 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -10,7 +10,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; /** @@ -25,7 +25,7 @@ public static function getServiceId() } /** {@inheritdoc} */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { $filterOperator = $filter->getOperator(); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php similarity index 76% rename from app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php rename to app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index d4d59a17cd7..11f939c208b 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/LeadListFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -11,24 +11,24 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; use Doctrine\ORM\EntityManager; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; -use Mautic\LeadBundle\Segment\LeadSegmentFilterFactory; -use Mautic\LeadBundle\Segment\Query\LeadSegmentQueryBuilder; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilterFactory; +use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\RandomParameterName; /** - * Class LeadListFilterQueryBuilder. + * Class SegmentReferenceFilterQueryBuilder. */ -class LeadListFilterQueryBuilder extends BaseFilterQueryBuilder +class SegmentReferenceFilterQueryBuilder extends BaseFilterQueryBuilder { /** - * @var LeadSegmentQueryBuilder + * @var ContactSegmentQueryBuilder */ private $leadSegmentQueryBuilder; /** - * @var LeadSegmentFilterFactory + * @var ContactSegmentFilterFactory */ private $leadSegmentFilterFactory; @@ -38,18 +38,18 @@ class LeadListFilterQueryBuilder extends BaseFilterQueryBuilder private $entityManager; /** - * LeadListFilterQueryBuilder constructor. + * SegmentReferenceFilterQueryBuilder constructor. * - * @param RandomParameterName $randomParameterNameService - * @param LeadSegmentQueryBuilder $leadSegmentQueryBuilder - * @param EntityManager $entityManager - * @param LeadSegmentFilterFactory $leadSegmentFilterFactory + * @param RandomParameterName $randomParameterNameService + * @param ContactSegmentQueryBuilder $leadSegmentQueryBuilder + * @param EntityManager $entityManager + * @param ContactSegmentFilterFactory $leadSegmentFilterFactory */ public function __construct( RandomParameterName $randomParameterNameService, - LeadSegmentQueryBuilder $leadSegmentQueryBuilder, + ContactSegmentQueryBuilder $leadSegmentQueryBuilder, EntityManager $entityManager, - LeadSegmentFilterFactory $leadSegmentFilterFactory + ContactSegmentFilterFactory $leadSegmentFilterFactory ) { parent::__construct($randomParameterNameService); @@ -67,14 +67,14 @@ public static function getServiceId() } /** - * @param QueryBuilder $queryBuilder - * @param LeadSegmentFilter $filter + * @param QueryBuilder $queryBuilder + * @param ContactSegmentFilter $filter * * @return QueryBuilder * * @throws \Mautic\LeadBundle\Segment\Exception\SegmentQueryException */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { $segmentIds = $filter->getParameterValue(); @@ -90,7 +90,7 @@ public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter ); foreach ($contactSegments as $contactSegment) { - $filters = $this->leadSegmentFilterFactory->getLeadListFilters($contactSegment); + $filters = $this->leadSegmentFilterFactory->getSegmentFilters($contactSegment); $segmentQueryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($filters); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php index aa70320cb3b..3d17af651fb 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SessionsFilterQueryBuilder.php @@ -10,7 +10,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Mautic\LeadBundle\Segment\LeadSegmentFilter; +use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; /** @@ -25,7 +25,7 @@ public static function getServiceId() } /** {@inheritdoc} */ - public function applyQuery(QueryBuilder $queryBuilder, LeadSegmentFilter $filter) + public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { $filterOperator = $filter->getOperator(); diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 698bdcf17b4..242192301ec 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -43,6 +43,8 @@ * @author Guilherme Blanco * @author Benjamin Eberlei * @author Jan Kozak + * + * @todo extend standing class instead of redefining everything */ class QueryBuilder { @@ -88,6 +90,8 @@ class QueryBuilder ]; /** + * Unprocessed logic for segment processing. + * * @var array */ private $logicStack = []; @@ -1550,7 +1554,7 @@ public function getTableJoins($tableName) * * @return string */ - public function guessPrimaryLeadIdColumn() + public function guessPrimaryLeadContactIdColumn() { $parts = $this->getQueryParts(); $leadTable = $parts['from'][0]['alias']; @@ -1592,8 +1596,7 @@ public function getTableAliases() } /** - * @param $table - * @param QueryBuilder $queryBuilder + * @param $table * * @return bool */ @@ -1629,16 +1632,25 @@ public function getDebugOutput() return $sql; } + /** + * @return bool + */ public function hasLogicStack() { - return count($this->logicStack); + return count($this->logicStack) > 0; } + /** + * @return array + */ public function getLogicStack() { return $this->logicStack; } + /** + * @return array + */ public function popLogicStack() { $stack = $this->logicStack; @@ -1647,45 +1659,52 @@ public function popLogicStack() return $stack; } - public function addToLogicStack($expression) + /** + * @param $expression + * + * @return $this + */ + public function addLogicStack($expression) { $this->logicStack[] = $expression; return $this; } + /** + * This function assembles correct logic for segment processing, this is to replace andWhere and orWhere (virtualy + * as they need to be kept). + * + * @todo make this readable and explain + * + * @param $expression + * @param $glue + * + * @return $this + */ public function addLogic($expression, $glue) { - //dump('adding logic "'.$glue.'":'.$expression.' stack:'.count($this->getLogicStack())); if ($this->hasLogicStack() && $glue == 'and') { - //dump('and where:'.$expression); - $this->addToLogicStack($expression); + $this->addLogicStack($expression); } elseif ($this->hasLogicStack()) { - // $logic = $queryBuilder->popLogicStack(); - // /** @var CompositeExpression $standingLogic */ - // $standingLogic = $queryBuilder->getQueryPart('where'); - // dump($logic); - // $queryBuilder->orWhere("(" . join(' AND ', $logic) . ")"); - //$this->applyStackLogic(); if ($glue == 'or') { $this->applyStackLogic(); } - $this->addToLogicStack($expression); + $this->addLogicStack($expression); } elseif ($glue == 'or') { - //dump('or to stack: '.$expression); - $this->addToLogicStack($expression); + $this->addLogicStack($expression); } else { -// if (!is_null($this->lastAndWhere)) { -// $this->andWhere($this->lastAndWhere); -// $this->lastAndWhere = null; -// } else { -// $this->lastAndWhere = $expression; -// } $this->andWhere($expression); - //dump('and where '.$expression); } + + return $this; } + /** + * Convert stored logic into regular where condition. + * + * @return $this + */ public function applyStackLogic() { if ($this->hasLogicStack()) { @@ -1693,17 +1712,6 @@ public function applyStackLogic() if (!is_null($parts['where']) && !is_null($parts['having'])) { $where = $parts['where']; - $having = $parts['having']; - - $fields = $this->extractFields($where); - - foreach ($this->getLogicStack() as $expression) { - $fields = array_merge($fields, $this->extractFields($expression)); - } - - $fields = array_diff($fields, $this->extractFields($having)); - - $select = array_unique(array_merge($this->getQueryPart('select'), $fields)); $whereConditionAlias = 'wh_'.substr(md5($where->__toString()), 0, 10); $selectCondition = sprintf('(%s) AS %s', $where->__toString(), $whereConditionAlias); @@ -1713,32 +1721,11 @@ public function applyStackLogic() $this->addSelect($selectCondition); $this->resetQueryPart('where'); - //$this->orHaving(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); - //$this->orHaving($this->popLogicStack()); } else { - //dump('-------- or stack:'); -// dump($stack = $this->getLogicStack()); - /* @var CompositeExpression $where */ -// dump('where:'); -// dump($where = $this->getQueryPart('where')); - // Stack need to be added to the last composite of type 'or' - $this->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); } -// dump('------- applied logic:'.(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack()))->__toString()); -// dump('where:'); -// dump($this->getQueryPart('where')); } - } - public function extractFields($expression) - { - $matches = []; - - preg_match('/([a-zA-Z]+\.[a-zA-Z_]+)/', $expression instanceof CompositeExpression ? (string) $expression : $expression, $matches); - - $matches = array_keys(array_flip($matches)); - - return $matches; + return $this; } } diff --git a/app/bundles/LeadBundle/Segment/RandomParameterName.php b/app/bundles/LeadBundle/Segment/RandomParameterName.php index 2ca68a02d94..84442c23945 100644 --- a/app/bundles/LeadBundle/Segment/RandomParameterName.php +++ b/app/bundles/LeadBundle/Segment/RandomParameterName.php @@ -11,6 +11,9 @@ namespace Mautic\LeadBundle\Segment; +/** + * Class RandomParameterName. + */ class RandomParameterName { /** diff --git a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php similarity index 88% rename from app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php rename to app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index 428274c0c0e..7cadabfc79b 100644 --- a/app/bundles/LeadBundle/Services/LeadSegmentFilterDescriptor.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -12,13 +12,19 @@ namespace Mautic\LeadBundle\Services; use Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder; -use Mautic\LeadBundle\Segment\Query\Filter\DncFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\DoNotContactFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\ForeignFuncFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\ForeignValueFilterQueryBuilder; -use Mautic\LeadBundle\Segment\Query\Filter\LeadListFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\SegmentReferenceFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder; -class LeadSegmentFilterDescriptor extends \ArrayIterator +/** + * Class ContactSegmentFilterDictionary + * + * @package Mautic\LeadBundle\Services + * @todo @petr Já jsem to myslím předělával už. Chtěl jsem z toho pak udělat i objekt, aby se člověk nemusel ptát na klíče v poli, ale pak jsme na to nesahali, protože to nebylo komplet + */ +class ContactSegmentFilterDictionary extends \ArrayIterator { private $translations; @@ -65,23 +71,23 @@ public function __construct() ]; $this->translations['dnc_bounced'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DoNotContactFilterQueryBuilder::getServiceId(), ]; $this->translations['dnc_bounced_sms'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DoNotContactFilterQueryBuilder::getServiceId(), ]; $this->translations['dnc_unsubscribed'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DoNotContactFilterQueryBuilder::getServiceId(), ]; $this->translations['dnc_unsubscribed_sms'] = [ - 'type' => DncFilterQueryBuilder::getServiceId(), + 'type' => DoNotContactFilterQueryBuilder::getServiceId(), ]; $this->translations['leadlist'] = [ - 'type' => LeadListFilterQueryBuilder::getServiceId(), + 'type' => SegmentReferenceFilterQueryBuilder::getServiceId(), ]; $this->translations['globalcategory'] = [ From a2e0982fb04d81eca6b41458bb6f5f97c692d170 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 21 Feb 2018 15:14:08 +0100 Subject: [PATCH 124/778] fix incorrect addLogic implementation in ReferencedSegment's QB --- .../Segment/ContactSegmentFilter.php | 22 ++++----- .../Segment/ContactSegmentService.php | 9 +++- .../Segment/Decorator/BaseDecorator.php | 3 ++ .../Decorator/CustomMappedDecorator.php | 2 + .../Segment/Decorator/DateDecorator.php | 2 +- .../Decorator/FilterDecoratorInterface.php | 2 +- .../Exception/SegmentQueryException.php | 4 +- .../LeadBundle/Segment/OperatorOptions.php | 4 +- .../Query/ContactSegmentQueryBuilder.php | 17 ++++--- .../Query/Filter/BaseFilterQueryBuilder.php | 4 +- .../Filter/DoNotContactFilterQueryBuilder.php | 2 +- .../Filter/ForeignFuncFilterQueryBuilder.php | 2 +- .../SegmentReferenceFilterQueryBuilder.php | 9 ++-- .../LeadBundle/Segment/Query/QueryBuilder.php | 48 ++++++++++--------- .../Segment/Query/QueryException.php | 12 ++--- 15 files changed, 78 insertions(+), 64 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 87f731ce538..0f6467cd68e 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -16,9 +16,7 @@ use Mautic\LeadBundle\Segment\Query\QueryException; /** - * Class ContactSegmentFilter - * - * @package Mautic\LeadBundle\Segment + * Class ContactSegmentFilter. */ class ContactSegmentFilter { @@ -55,8 +53,7 @@ public function __construct( FilterDecoratorInterface $filterDecorator, TableSchemaColumnsCache $cache, FilterQueryBuilderInterface $filterQueryBuilder - ) - { + ) { $this->contactSegmentFilterCrate = $contactSegmentFilterCrate; $this->filterDecorator = $filterDecorator; $this->schemaCache = $cache; @@ -158,7 +155,7 @@ public function getAggregateFunction() } /** - * @todo remove this, create functions to replace need for this + * @TODO remove this, create functions to replace need for this * * @param null $field * @@ -170,7 +167,7 @@ public function getAggregateFunction() */ public function getCrate($field = null) { - $fields = (array)$this->toArray(); + $fields = (array) $this->toArray(); if (is_null($field)) { return $fields; @@ -180,7 +177,7 @@ public function getCrate($field = null) return $fields[$field]; } - throw new \Exception('Unknown crate field "' . $field . "'"); + throw new \Exception('Unknown crate field "'.$field."'"); } /** @@ -209,11 +206,11 @@ public function getFilterQueryBuilder() return $this->filterQueryBuilder; } - /** - * String representation of the object + * String representation of the object. * * @return string + * * @throws \Exception */ public function __toString() @@ -247,9 +244,10 @@ public function applyQuery(QueryBuilder $queryBuilder) } /** - * Whether the filter references another ContactSegment + * Whether the filter references another ContactSegment. + * + * @TODO replace if not used * - * @todo replace if not used * @return bool */ public function isContactSegmentReference() diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 7cbb5f591cd..30ba02dacda 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -12,7 +12,6 @@ namespace Mautic\LeadBundle\Segment; use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Entity\LeadListSegmentRepository; use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Symfony\Bridge\Monolog\Logger; @@ -39,7 +38,6 @@ class ContactSegmentService */ private $preparedQB; - public function __construct( ContactSegmentFilterFactory $contactSegmentFilterFactory, ContactSegmentQueryBuilder $queryBuilder, @@ -55,6 +53,7 @@ public function __construct( * @param $batchLimiters * * @return QueryBuilder + * * @throws Exception\SegmentQueryException * @throws \Exception */ @@ -67,6 +66,7 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + dump($queryBuilder->getLogicStack()); $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $segment->getId()); @@ -99,6 +99,8 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); + dump($qb->getLogicStack()); + $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); $result = $this->timedFetch($qb, $segment->getId()); @@ -150,6 +152,7 @@ public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $li * @param LeadList $segment * * @return QueryBuilder + * * @throws Exception\SegmentQueryException * @throws \Exception */ @@ -174,6 +177,7 @@ private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) * @param LeadList $segment * * @return array + * * @throws Exception\SegmentQueryException * @throws \Exception */ @@ -193,6 +197,7 @@ public function getOrphanedLeadListLeadsCount(LeadList $segment) * @param LeadList $segment * * @return array + * * @throws Exception\SegmentQueryException * @throws \Exception */ diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 33ad5fba2f7..159ed9828d4 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -14,6 +14,7 @@ use Mautic\LeadBundle\Entity\RegexTrait; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\ContactSegmentFilterOperator; +use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\Query\Filter\BaseFilterQueryBuilder; /** @@ -159,6 +160,8 @@ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilter /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|CompositeExpression|string */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php index a2ed78adbe6..486c7b37f29 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -102,6 +102,8 @@ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilter /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression|null|string */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index 9e4d3cf9487..28fe533ebb2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -21,7 +21,7 @@ class DateDecorator extends CustomMappedDecorator /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * - * @todo @petr please check this method + * @TODO @petr please check this method * * @throws \Exception */ diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index 814e3881fae..ba08565dd62 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -16,7 +16,7 @@ /** * Interface FilterDecoratorInterface. * - * @todo @petr document these functions please with phpdoc and meaningful description + * @TODO @petr document these functions please with phpdoc and meaningful description */ interface FilterDecoratorInterface { diff --git a/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php b/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php index f9f4ebab93f..5c08033ca00 100644 --- a/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php +++ b/app/bundles/LeadBundle/Segment/Exception/SegmentQueryException.php @@ -10,9 +10,11 @@ namespace Mautic\LeadBundle\Segment\Exception; +use Doctrine\DBAL\Query\QueryException; + /** * Class SegmentQueryException. */ -class SegmentQueryException extends \Exception +class SegmentQueryException extends QueryException { } diff --git a/app/bundles/LeadBundle/Segment/OperatorOptions.php b/app/bundles/LeadBundle/Segment/OperatorOptions.php index adb96e8da4b..e5e7db3e4d3 100644 --- a/app/bundles/LeadBundle/Segment/OperatorOptions.php +++ b/app/bundles/LeadBundle/Segment/OperatorOptions.php @@ -68,14 +68,14 @@ class OperatorOptions 'label' => 'mautic.lead.list.form.operator.between', 'expr' => 'between', //special case 'negate_expr' => 'notBetween', - // @todo implement in list UI + // @TODO implement in list UI 'hide' => true, ], '!between' => [ 'label' => 'mautic.lead.list.form.operator.notbetween', 'expr' => 'notBetween', //special case 'negate_expr' => 'between', - // @todo implement in list UI + // @TODO implement in list UI 'hide' => true, ], 'in' => [ diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index c2eada9faa8..849fa82e013 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -12,15 +12,15 @@ namespace Mautic\LeadBundle\Segment\Query; use Doctrine\ORM\EntityManager; -use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\ContactSegmentFilters; +use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\RandomParameterName; /** * Class ContactSegmentQueryBuilder is responsible for building queries for segments. * - * @todo add exceptions, remove related segments + * @TODO add exceptions, remove related segments */ class ContactSegmentQueryBuilder { @@ -68,6 +68,7 @@ public function assembleContactsSegmentQueryBuilder(ContactSegmentFilters $conta /** @var ContactSegmentFilter $filter */ foreach ($contactSegmentFilters as $filter) { + dump($filter->__toString()); $segmentIdArray = is_array($filter->getParameterValue()) ? $filter->getParameterValue() : [$filter->getParameterValue()]; // We will handle references differently than regular segments @@ -115,6 +116,8 @@ private function getContactSegmentRelations(array $id) * @param QueryBuilder $qb * * @return QueryBuilder + * + * @throws \Doctrine\DBAL\DBALException */ public function wrapInCount(QueryBuilder $qb) { @@ -147,7 +150,7 @@ public function wrapInCount(QueryBuilder $qb) * * @param QueryBuilder $queryBuilder * @param $segmentId - * @param $whatever @todo document this field + * @param $whatever @TODO document this field * * @return QueryBuilder */ @@ -233,7 +236,7 @@ private function generateRandomParameterName() /** * @return LeadSegmentFilterDescriptor * - * @todo Remove this function + * @TODO Remove this function */ public function getTranslator() { @@ -245,7 +248,7 @@ public function getTranslator() * * @return ContactSegmentQueryBuilder * - * @todo Remove this function + * @TODO Remove this function */ public function setTranslator($translator) { @@ -257,7 +260,7 @@ public function setTranslator($translator) /** * @return \Doctrine\DBAL\Schema\AbstractSchemaManager * - * @todo Remove this function + * @TODO Remove this function */ public function getSchema() { @@ -269,7 +272,7 @@ public function getSchema() * * @return ContactSegmentQueryBuilder * - * @todo Remove this function + * @TODO Remove this function */ public function setSchema($schema) { diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 09962ecdfd3..1da458bd79e 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -98,7 +98,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'in': case 'regexp': case 'notRegexp': - //@todo this logic needs to + //@TODO this logic needs to if ($filterAggr) { $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), @@ -143,7 +143,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $filterParametersHolder ) ); - break; + break; // Break will be performed only if the condition above matches } case 'startsWith': case 'endsWith': diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index 29acd742ce2..32a9a14bf85 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -32,7 +32,7 @@ public static function getServiceId() */ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { - //@todo look at this, the getCrate method is for debuggin only + //@TODO look at this, the getCrate method is for debuggin only $parts = explode('_', $filter->getCrate('field')); $channel = 'email'; diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 2cd0ca3acc0..2e06fd6de18 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -80,7 +80,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'lt': case 'lte': case 'in': - //@todo this logic needs to + //@TODO this logic needs to if ($filterAggr) { $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index 11f939c208b..2989c6903ed 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -72,6 +72,8 @@ public static function getServiceId() * * @return QueryBuilder * + * @throws \Doctrine\DBAL\Query\QueryException + * @throws \Exception * @throws \Mautic\LeadBundle\Segment\Exception\SegmentQueryException */ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) @@ -113,16 +115,13 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $expression = $queryBuilder->expr()->isNull($segmentAlias.'.id'); } else { $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); - $orExpressions[] = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); + $expression = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); } $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); + $queryBuilder->addLogic($expression, $filter->getGlue()); } } - $expression = isset($orExpressions) ? '('.join(' OR ', $orExpressions).')' : $expression; - - $queryBuilder->addLogic($expression, $filter->getGlue()); - return $queryBuilder; } } diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 242192301ec..e1ebd7eb04f 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -20,8 +20,7 @@ namespace Mautic\LeadBundle\Segment\Query; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Query\Expression\CompositeExpression; -use Elastica\Exception\QueryBuilderException; +use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\Query\Expression\ExpressionBuilder; /** @@ -35,16 +34,11 @@ * even if some vendors such as MySQL support it. * * @see www.doctrine-project.org - * - * @todo rework this to extend the original Query Builder instead of writing new one - * * @since 2.1 * * @author Guilherme Blanco * @author Benjamin Eberlei * @author Jan Kozak - * - * @todo extend standing class instead of redefining everything */ class QueryBuilder { @@ -226,6 +220,8 @@ public function getState() * for insert, update and delete statements. * * @return \Doctrine\DBAL\Driver\Statement|int + * + * @throws \Doctrine\DBAL\DBALException */ public function execute() { @@ -247,6 +243,8 @@ public function execute() * * * @return string the SQL query string + * + * @throws \Doctrine\DBAL\DBALException */ public function getSQL() { @@ -1110,12 +1108,10 @@ public function resetQueryParts($queryPartNames = null) } /** - * Sets SQL parts. + * @param $queryPartName + * @param $value * - * @param array $queryPartNames - * @param array $value - * - * @return $this this QueryBuilder instance + * @return $this */ public function setQueryPart($queryPartName, $value) { @@ -1145,7 +1141,7 @@ public function resetQueryPart($queryPartName) /** * @return string * - * @throws \Doctrine\DBAL\Query\QueryException + * @throws \Doctrine\DBAL\DBALException */ private function getSQLForSelect() { @@ -1169,7 +1165,9 @@ private function getSQLForSelect() } /** - * @return string[] + * @return array + * + * @throws QueryException */ private function getFromClauses() { @@ -1259,10 +1257,9 @@ private function getSQLForDelete() } /** - * Gets a string representation of this QueryBuilder which corresponds to - * the final SQL query being constructed. + * @return string * - * @return string the string representation of this QueryBuilder + * @throws \Doctrine\DBAL\DBALException */ public function __toString() { @@ -1341,10 +1338,12 @@ public function createPositionalParameter($value, $type = \PDO::PARAM_STR) } /** - * @param string $fromAlias - * @param array $knownAliases + * @param $fromAlias + * @param array $knownAliases * * @return string + * + * @throws QueryException */ private function getSQLForJoins($fromAlias, array &$knownAliases) { @@ -1411,7 +1410,7 @@ public function getJoinCondition($alias) } /** - * @todo I need to rewrite it, it's no longer necessary like this, we have direct access to query parts + * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts * * @param $alias * @param $expr @@ -1441,7 +1440,7 @@ public function addJoinCondition($alias, $expr) } /** - * @todo I need to rewrite it, it's no longer necessary like this, we have direct access to query parts + * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts * * @param $alias * @param $expr @@ -1616,7 +1615,9 @@ public function isJoinTable($table) } /** - * @return mixed + * @return mixed|string + * + * @throws \Doctrine\DBAL\DBALException */ public function getDebugOutput() { @@ -1675,7 +1676,7 @@ public function addLogicStack($expression) * This function assembles correct logic for segment processing, this is to replace andWhere and orWhere (virtualy * as they need to be kept). * - * @todo make this readable and explain + * @TODO make this readable and explain * * @param $expression * @param $glue @@ -1684,6 +1685,7 @@ public function addLogicStack($expression) */ public function addLogic($expression, $glue) { + dump('add logic: '.$expression.', glue: '.$glue); if ($this->hasLogicStack() && $glue == 'and') { $this->addLogicStack($expression); } elseif ($this->hasLogicStack()) { diff --git a/app/bundles/LeadBundle/Segment/Query/QueryException.php b/app/bundles/LeadBundle/Segment/Query/QueryException.php index 9063c6d0ad3..e14c9c325df 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryException.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryException.php @@ -27,10 +27,10 @@ class QueryException extends DBALException { /** - * @param string $alias - * @param array $registeredAliases + * @param $alias + * @param $registeredAliases * - * @return \Doctrine\DBAL\Query\QueryException + * @return QueryException */ public static function unknownAlias($alias, $registeredAliases) { @@ -40,10 +40,10 @@ public static function unknownAlias($alias, $registeredAliases) } /** - * @param string $alias - * @param array $registeredAliases + * @param $alias + * @param $registeredAliases * - * @return \Doctrine\DBAL\Query\QueryException + * @return QueryException */ public static function nonUniqueAlias($alias, $registeredAliases) { From 7b95e9822222eafaca322f98d465b9c7d838f228 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 21 Feb 2018 15:19:48 +0100 Subject: [PATCH 125/778] fix not including unsubscribed, this commit should be reverted. --- app/bundles/LeadBundle/Segment/ContactSegmentService.php | 3 +-- .../LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php | 1 - app/bundles/LeadBundle/Segment/Query/QueryBuilder.php | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 30ba02dacda..2955fe43fd9 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -66,9 +66,8 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); - dump($queryBuilder->getLogicStack()); $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); - $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $segment->getId()); + //$queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $segment->getId()); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 849fa82e013..d16633185e6 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -68,7 +68,6 @@ public function assembleContactsSegmentQueryBuilder(ContactSegmentFilters $conta /** @var ContactSegmentFilter $filter */ foreach ($contactSegmentFilters as $filter) { - dump($filter->__toString()); $segmentIdArray = is_array($filter->getParameterValue()) ? $filter->getParameterValue() : [$filter->getParameterValue()]; // We will handle references differently than regular segments diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index e1ebd7eb04f..c130836cd57 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1685,7 +1685,6 @@ public function addLogicStack($expression) */ public function addLogic($expression, $glue) { - dump('add logic: '.$expression.', glue: '.$glue); if ($this->hasLogicStack() && $glue == 'and') { $this->addLogicStack($expression); } elseif ($this->hasLogicStack()) { From c6433f2c49baccb1b39cc17aef9bed6ef55b7642 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 21 Feb 2018 15:55:02 +0100 Subject: [PATCH 126/778] Typo for tests From 8ce03e0eb26a05f1adeec76dce18a60ffbad6931 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 21 Feb 2018 15:55:32 +0100 Subject: [PATCH 127/778] Get rid of using Crate directly for boolean check --- app/bundles/LeadBundle/Segment/ContactSegmentFilter.php | 8 ++++++++ .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 0f6467cd68e..4f679d5a71d 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -254,4 +254,12 @@ public function isContactSegmentReference() { return $this->getField() === 'leadlist'; } + + /** + * @return bool + */ + public function isColumnTypeBoolean() + { + return $this->contactSegmentFilterCrate->getType() === 'boolean'; + } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 1da458bd79e..9a1ebfb1f88 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -135,7 +135,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $expression = $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()); break; case 'neq': - if ($filter->getCrate()['type'] === 'boolean' && $filter->getParameterValue() == 1) { + if ($filter->isColumnTypeBoolean() && $filter->getParameterValue() == 1) { $expression = $queryBuilder->expr()->orX( $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), $queryBuilder->expr()->$filterOperator( From 3ea43c1a0337477de7c69e56a5334a349d3a7d04 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 21 Feb 2018 17:17:29 +0100 Subject: [PATCH 128/778] DoNotContactQB - move logic of getting parameter to separate class --- .../Segment/ContactSegmentFilter.php | 11 +++- .../DoNotContact/DoNotContactParts.php | 52 +++++++++++++++++++ .../Filter/DoNotContactFilterQueryBuilder.php | 13 ++--- 3 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 4f679d5a71d..ce76cc115d5 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment; use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; +use Mautic\LeadBundle\Segment\DoNotContact\DoNotContactParts; use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; @@ -41,8 +42,6 @@ class ContactSegmentFilter private $schemaCache; /** - * ContactSegmentFilter constructor. - * * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * @param FilterDecoratorInterface $filterDecorator * @param TableSchemaColumnsCache $cache @@ -262,4 +261,12 @@ public function isColumnTypeBoolean() { return $this->contactSegmentFilterCrate->getType() === 'boolean'; } + + /** + * @return DoNotContactParts + */ + public function getDoNotContactParts() + { + return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); + } } diff --git a/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php b/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php new file mode 100644 index 00000000000..c5c9951c153 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/DoNotContact/DoNotContactParts.php @@ -0,0 +1,52 @@ +type = $parts[1]; + $this->channel = count($parts) === 3 ? $parts[2] : 'email'; + } + + /** + * @return string + */ + public function getChannel() + { + return $this->channel; + } + + /** + * @return int + */ + public function getParameterType() + { + return $this->type === 'bounced' ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; + } +} diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index 32a9a14bf85..fc5f4018411 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -10,7 +10,6 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; -use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Query\QueryBuilder; @@ -32,13 +31,7 @@ public static function getServiceId() */ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $filter) { - //@TODO look at this, the getCrate method is for debuggin only - $parts = explode('_', $filter->getCrate('field')); - $channel = 'email'; - - if (count($parts) === 3) { - $channel = $parts[2]; - } + $doNotContactParts = $filter->getDoNotContactParts(); $tableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'left'); @@ -66,8 +59,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); - $queryBuilder->setParameter($exprParameter, ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED); - $queryBuilder->setParameter($channelParameter, $channel); + $queryBuilder->setParameter($exprParameter, $doNotContactParts->getParameterType()); + $queryBuilder->setParameter($channelParameter, $doNotContactParts->getChannel()); return $queryBuilder; } From 7ea09e11d5ca05f21b0d9c2ab7b867485f2f79c7 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 21 Feb 2018 17:41:36 +0100 Subject: [PATCH 129/778] Remove unnused methods from Filter --- .../Segment/ContactSegmentFilter.php | 71 ------------------- .../Segment/ContactSegmentFilterFactory.php | 4 +- 2 files changed, 3 insertions(+), 72 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index ce76cc115d5..1bcf1a35748 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -153,50 +153,6 @@ public function getAggregateFunction() return $this->filterDecorator->getAggregateFunc($this->contactSegmentFilterCrate); } - /** - * @TODO remove this, create functions to replace need for this - * - * @param null $field - * - * @return array|mixed - * - * @deprecated - * - * @throws \Exception - */ - public function getCrate($field = null) - { - $fields = (array) $this->toArray(); - - if (is_null($field)) { - return $fields; - } - - if (isset($fields[$field])) { - return $fields[$field]; - } - - throw new \Exception('Unknown crate field "'.$field."'"); - } - - /** - * @return array - */ - public function toArray() - { - return [ - 'glue' => $this->contactSegmentFilterCrate->getGlue(), - 'field' => $this->contactSegmentFilterCrate->getField(), - 'object' => $this->contactSegmentFilterCrate->getObject(), - 'type' => $this->contactSegmentFilterCrate->getType(), - 'filter' => $this->contactSegmentFilterCrate->getFilter(), - 'display' => $this->contactSegmentFilterCrate->getDisplay(), - 'operator' => $this->contactSegmentFilterCrate->getOperator(), - 'func' => $this->contactSegmentFilterCrate->getFunc(), - 'aggr' => $this->getAggregateFunction(), - ]; - } - /** * @return FilterQueryBuilderInterface */ @@ -205,33 +161,6 @@ public function getFilterQueryBuilder() return $this->filterQueryBuilder; } - /** - * String representation of the object. - * - * @return string - * - * @throws \Exception - */ - public function __toString() - { - if (!is_array($this->getParameterValue())) { - return sprintf('table:%s field:%s operator:%s holder:%s value:%s, crate:%s', - $this->getTable(), - $this->getField(), - $this->getOperator(), - $this->getParameterHolder('holder'), - $this->getParameterValue(), - print_r($this->getCrate(), true)); - } - - return sprintf('table:%s field:%s holder:%s value:%s, crate: %s', - $this->getTable(), - $this->getField(), - print_r($this->getParameterHolder($this->getParameterValue()), true), - print_r($this->getParameterValue(), true), - print_r($this->getCrate(), true)); - } - /** * @param QueryBuilder $queryBuilder * diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php index b8739e5a28a..6b43ddbb3c9 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php @@ -55,6 +55,7 @@ public function __construct( * @param LeadList $leadList * * @return ContactSegmentFilters + * * @throws \Exception */ public function getSegmentFilters(LeadList $leadList) @@ -82,7 +83,8 @@ public function getSegmentFilters(LeadList $leadList) * @param FilterDecoratorInterface $decorator * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * - * @return object + * @return FilterQueryBuilderInterface + * * @throws \Exception */ private function getQueryBuilderForFilter(FilterDecoratorInterface $decorator, ContactSegmentFilterCrate $contactSegmentFilterCrate) From 986d98f9354590d162a3ba685bdc5513ffc25d94 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 21 Feb 2018 17:49:13 +0100 Subject: [PATCH 130/778] Class comment --- app/bundles/LeadBundle/Segment/ContactSegmentFilter.php | 2 +- app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 1bcf1a35748..3fb98040d9b 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -17,7 +17,7 @@ use Mautic\LeadBundle\Segment\Query\QueryException; /** - * Class ContactSegmentFilter. + * Class ContactSegmentFilter is used for accessing $filter as an object and to keep logic in an object. */ class ContactSegmentFilter { diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php index 6b43ddbb3c9..c543db8a83f 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterFactory.php @@ -64,7 +64,6 @@ public function getSegmentFilters(LeadList $leadList) $filters = $leadList->getFilters(); foreach ($filters as $filter) { - // ContactSegmentFilterCrate is for accessing $filter as an object $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); $decorator = $this->decoratorFactory->getDecoratorForFilter($contactSegmentFilterCrate); From 43dc97d76c6aaad635701cddf76c0f04d7521244 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 22 Feb 2018 14:37:01 +0100 Subject: [PATCH 131/778] New functional test for new segments - WIP --- .../Segment/ContactSegmentService.php | 2 +- .../Tests/Model/ListModelFunctionalTest.php | 2 +- .../ContactSegmentServiceFunctionalTest.php | 297 ++++++++++++++++++ 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 2955fe43fd9..976b90f7951 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -98,7 +98,7 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); - dump($qb->getLogicStack()); + //dump($qb->getLogicStack()); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); diff --git a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php index 21cf14003cb..56d5425e8c0 100644 --- a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php @@ -73,7 +73,7 @@ public function testSegmentCountIsCorrect() ['countOnly' => true], $logger ); - + dump($segmentContacts); $this->assertEquals( 1, $segmentContacts[$segmentTest1Ref->getId()]['count'], diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php new file mode 100644 index 00000000000..c716c6ee8da --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php @@ -0,0 +1,297 @@ +container->get('mautic.lead.model.lead_segment_service'); + + $dtHelper = new DateTimeHelper(); + $batchLimiters = ['dateTime' => $dtHelper->toUtcString()]; + + $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); + $segmentContacts = $contactSegmentService->getNewLeadListLeadsCount($segmentTest1Ref, $batchLimiters); + + $this->assertEquals( + 1, + $segmentContacts[$segmentTest1Ref->getId()]['count'], + 'There should be 1 contacts in the segment-test-1 segment.' + ); + + return; + + /** + * @var LeadListRepository + */ + $repo = $this->em->getRepository(LeadList::class); + + $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); + $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); + $segmentTest4Ref = $this->fixtures->getReference('segment-test-4'); + $segmentTest5Ref = $this->fixtures->getReference('segment-test-5'); + $likePercentEndRef = $this->fixtures->getReference('like-percent-end'); + $segmentTestWithoutFiltersRef = $this->fixtures->getReference('segment-test-without-filters'); + $segmentTestIncludeMembershipWithFiltersRef = $this->fixtures->getReference('segment-test-include-segment-with-filters'); + $segmentTestExcludeMembershipWithFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-with-filters'); + $segmentTestIncludeMembershipWithoutFiltersRef = $this->fixtures->getReference('segment-test-include-segment-without-filters'); + $segmentTestExcludeMembershipWithoutFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-without-filters'); + $segmentTestIncludeMembershipMixedFiltersRef = $this->fixtures->getReference('segment-test-include-segment-mixed-filters'); + $segmentTestExcludeMembershipMixedFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-mixed-filters'); + $segmentTestMixedIncludeExcludeRef = $this->fixtures->getReference('segment-test-mixed-include-exclude-filters'); + $segmentTestManualMembership = $this->fixtures->getReference('segment-test-manual-membership'); + $segmentTestIncludeMembershipManualMembersRef = $this->fixtures->getReference('segment-test-include-segment-manual-members'); + $segmentTestExcludeMembershipManualMembersRef = $this->fixtures->getReference('segment-test-exclude-segment-manual-members'); + $segmentTestExcludeMembershipWithoutOtherFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-without-other-filters'); + $segmentTestIncludeWithUnrelatedManualRemovalRef = $this->fixtures->getReference( + 'segment-test-include-segment-with-unrelated-segment-manual-removal' + ); + $segmentMembershipRegex = $this->fixtures->getReference('segment-membership-regexp'); + $segmentCompanyFields = $this->fixtures->getReference('segment-company-only-fields'); + $segmentMembershipCompanyOnlyFields = $this->fixtures->getReference('segment-including-segment-with-company-only-fields'); + + $logger = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + + // These expect filters to be part of the $lists passed to getLeadsByList so pass the entity + $segmentContacts = $repo->getLeadsByList( + [ + $segmentTest1Ref, + $segmentTest2Ref, + $segmentTest3Ref, + $segmentTest4Ref, + $segmentTest5Ref, + $likePercentEndRef, + $segmentTestWithoutFiltersRef, + $segmentTestIncludeMembershipWithFiltersRef, + $segmentTestExcludeMembershipWithFiltersRef, + $segmentTestIncludeMembershipWithoutFiltersRef, + $segmentTestExcludeMembershipWithoutFiltersRef, + $segmentTestIncludeMembershipMixedFiltersRef, + $segmentTestExcludeMembershipMixedFiltersRef, + $segmentTestMixedIncludeExcludeRef, + $segmentTestManualMembership, + $segmentTestIncludeMembershipManualMembersRef, + $segmentTestExcludeMembershipManualMembersRef, + $segmentTestExcludeMembershipWithoutOtherFiltersRef, + $segmentTestIncludeWithUnrelatedManualRemovalRef, + $segmentMembershipRegex, + $segmentCompanyFields, + $segmentMembershipCompanyOnlyFields, + ], + ['countOnly' => true], + $logger + ); + + $this->assertEquals( + 4, + $segmentContacts[$segmentTest2Ref->getId()]['count'], + 'There should be 4 contacts in the segment-test-2 segment.' + ); + + $this->assertEquals( + 24, + $segmentContacts[$segmentTest3Ref->getId()]['count'], + 'There should be 24 contacts in the segment-test-3 segment' + ); + + $this->assertEquals( + 1, + $segmentContacts[$segmentTest4Ref->getId()]['count'], + 'There should be 1 contacts in the segment-test-4 segment.' + ); + + $this->assertEquals( + 53, + $segmentContacts[$segmentTest5Ref->getId()]['count'], + 'There should be 53 contacts in the segment-test-5 segment.' + ); + + $this->assertEquals( + 32, + $segmentContacts[$likePercentEndRef->getId()]['count'], + 'There should be 32 contacts in the like-percent-end segment.' + ); + + $this->assertEquals( + 0, + $segmentContacts[$segmentTestWithoutFiltersRef->getId()]['count'], + 'There should be 0 contacts in the segment-test-without-filters segment.' + ); + + $this->assertEquals( + 26, + $segmentContacts[$segmentTestIncludeMembershipWithFiltersRef->getId()]['count'], + 'There should be 26 contacts in the segment-test-include-segment-with-filters segment. 24 from segment-test-3 that was not added yet plus 4 from segment-test-2 minus 2 for being in both = 26.' + ); + + $this->assertEquals( + 7, + $segmentContacts[$segmentTestExcludeMembershipWithFiltersRef->getId()]['count'], + 'There should be 7 contacts in the segment-test-exclude-segment-with-filters segment. 8 that are in the US minus 1 that is in segment-test-3.' + ); + + $this->assertEquals( + 0, + $segmentContacts[$segmentTestIncludeMembershipWithoutFiltersRef->getId()]['count'], + 'There should be 0 contacts as there is no one in segment-test-without-filters' + ); + + $this->assertEquals( + 11, + $segmentContacts[$segmentTestExcludeMembershipWithoutFiltersRef->getId()]['count'], + 'There should be 11 contacts in the United Kingdom and 0 from segment-test-without-filters.' + ); + + $this->assertEquals( + 24, + $segmentContacts[$segmentTestIncludeMembershipMixedFiltersRef->getId()]['count'], + 'There should be 24 contacts. 0 from segment-test-without-filters and 24 from segment-test-3.' + ); + + $this->assertEquals( + 30, + $segmentContacts[$segmentTestExcludeMembershipMixedFiltersRef->getId()]['count'], + 'There should be 30 contacts. 0 from segment-test-without-filters and 30 from segment-test-3.' + ); + + $this->assertEquals( + 8, + $segmentContacts[$segmentTestMixedIncludeExcludeRef->getId()]['count'], + 'There should be 8 contacts. 32 from like-percent-end minus 24 from segment-test-3.' + ); + + $this->assertEquals( + 12, + $segmentContacts[$segmentTestManualMembership->getId()]['count'], + 'There should be 12 contacts. 11 in the United Kingdom plus 3 manually added minus 2 manually removed.' + ); + + $this->assertEquals( + 12, + $segmentContacts[$segmentTestIncludeMembershipManualMembersRef->getId()]['count'], + 'There should be 12 contacts in the included segment-test-include-segment-manual-members segment' + ); + + $this->assertEquals( + 25, + $segmentContacts[$segmentTestExcludeMembershipManualMembersRef->getId()]['count'], + 'There should be 25 contacts in the segment-test-exclude-segment-manual-members segment' + ); + + $this->assertEquals( + 42, + $segmentContacts[$segmentTestExcludeMembershipWithoutOtherFiltersRef->getId()]['count'], + 'There should be 42 contacts in the included segment-test-exclude-segment-without-other-filters segment' + ); + + $this->assertEquals( + 11, + $segmentContacts[$segmentTestIncludeWithUnrelatedManualRemovalRef->getId()]['count'], + 'There should be 11 contacts in the segment-test-include-segment-with-unrelated-segment-manual-removal segment where a contact has been manually removed form another list' + ); + + $this->assertEquals( + 11, + $segmentContacts[$segmentMembershipRegex->getId()]['count'], + 'There should be 11 contacts that match the regex with dayrep.com in it' + ); + + $this->assertEquals( + 6, + $segmentContacts[$segmentCompanyFields->getId()]['count'], + 'There should only be 6 in this segment (6 contacts belong to HostGator based in Houston)' + ); + + $this->assertEquals( + 14, + $segmentContacts[$segmentMembershipCompanyOnlyFields->getId()]['count'], + 'There should be 14 in this segment.' + ); + } + +// +// public function testPublicSegmentsInContactPreferences() +// { +// /** +// * @var LeadListRepository $repo +// */ +// $repo = $this->em->getRepository(LeadList::class); +// +// $lists = $repo->getGlobalLists(); +// +// $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); +// +// $this->assertArrayNotHasKey( +// $segmentTest2Ref->getId(), +// $lists, +// 'Non-public lists should not be returned by the `getGlobalLists()` method.' +// ); +// } +// +// public function testSegmentRebuildCommand() +// { +// /** +// * @var LeadListRepository $repo +// */ +// $repo = $this->em->getRepository(LeadList::class); +// $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); +// +// $this->runCommand('mautic:segments:update', [ +// '-i' => $segmentTest3Ref->getId(), +// '--env' => 'test', +// ]); +// +// $logger = $this->getMockBuilder(Logger::class) +// ->disableOriginalConstructor() +// ->getMock(); +// +// $segmentContacts = $repo->getLeadsByList([ +// $segmentTest3Ref, +// ], ['countOnly' => true], $logger); +// +// $this->assertEquals( +// 24, +// $segmentContacts[$segmentTest3Ref->getId()]['count'], +// 'There should be 24 contacts in the segment-test-3 segment after rebuilding from the command line.' +// ); +// +// // Remove the title from all contacts, rebuild the list, and check that list is updated +// $this->em->getConnection()->query(sprintf('UPDATE %sleads SET title = NULL;', MAUTIC_TABLE_PREFIX)); +// +// $this->runCommand('mautic:segments:update', [ +// '-i' => $segmentTest3Ref->getId(), +// '--env' => 'test', +// ]); +// +// $logger = $this->getMockBuilder(Logger::class) +// ->disableOriginalConstructor() +// ->getMock(); +// +// $segmentContacts = $repo->getLeadsByList([ +// $segmentTest3Ref, +// ], ['countOnly' => true], $logger); +// +// $this->assertEquals( +// 0, +// $segmentContacts[$segmentTest3Ref->getId()]['count'], +// 'There should be no contacts in the segment-test-3 segment after removing contact titles and rebuilding from the command line.' +// ); +// } +} From d0e86829d682e5f145538d75208968558ab90072 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Thu, 22 Feb 2018 16:35:27 +0100 Subject: [PATCH 132/778] Move the logic --- .../Integration/TwilioIntegration.php | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/bundles/SmsBundle/Integration/TwilioIntegration.php b/app/bundles/SmsBundle/Integration/TwilioIntegration.php index 38948e74c4a..e100b1b9789 100644 --- a/app/bundles/SmsBundle/Integration/TwilioIntegration.php +++ b/app/bundles/SmsBundle/Integration/TwilioIntegration.php @@ -99,6 +99,17 @@ public function appendToForm(&$builder, $data, $formArea) ], ] ); + $builder->add( + 'disable_trackable_urls', + 'yesno_button_group', + [ + 'label' => 'mautic.sms.config.form.sms.disable_trackable_urls', + 'attr' => [ + 'tooltip' => 'mautic.sms.config.form.sms.disable_trackable_urls.tooltip', + ], + 'data'=> !empty($data['disable_trackable_urls']) ? true : false, + ] + ); $builder->add('frequency_number', 'number', [ 'precision' => 0, @@ -124,18 +135,6 @@ public function appendToForm(&$builder, $data, $formArea) 'class' => 'form-control frequency', ], ]); - - $builder->add( - 'disable_trackable_urls', - 'yesno_button_group', - [ - 'label' => 'mautic.sms.config.form.sms.disable_trackable_urls', - 'attr' => [ - 'tooltip' => 'mautic.sms.config.form.sms.disable_trackable_urls.tooltip', - ], - 'data'=> !empty($data['disable_trackable_urls']) ? true : false, - ] - ); } } } From 589213c137eceb47298adf3af7c2e6d40b2dc5ca Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Thu, 22 Feb 2018 16:36:56 +0100 Subject: [PATCH 133/778] Trans --- app/bundles/SmsBundle/Translations/en_US/messages.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/SmsBundle/Translations/en_US/messages.ini b/app/bundles/SmsBundle/Translations/en_US/messages.ini index b9e35b113f8..c38c82bc346 100644 --- a/app/bundles/SmsBundle/Translations/en_US/messages.ini +++ b/app/bundles/SmsBundle/Translations/en_US/messages.ini @@ -13,7 +13,7 @@ mautic.sms.config.form.sms.password="Auth Token" mautic.sms.config.form.sms.password.tooltip="Twilio Auth Token" mautic.sms.config.form.sms.sending_phone_number="Sending Phone Number" mautic.sms.config.form.sms.sending_phone_number.tooltip="The phone number given by your provider that you use to send and receive Text Message messages." -mautic.sms.config.form.sms.disable_trackable_urls="Disable trackable Urls" +mautic.sms.config.form.sms.disable_trackable_urls="Disable trackable urls" mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option disable trackable Urls and your SMS click stats will not work." mautic.sms.sms="Text Message" From 8dea5db82aa2b2ca08b828aee68013b13a9a149f Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 22 Feb 2018 18:07:19 +0100 Subject: [PATCH 134/778] Segment functional tests - some of tests work. Still WIP --- .../Segment/ContactSegmentService.php | 58 ++++++ .../Query/ContactSegmentQueryBuilder.php | 2 +- .../SegmentReferenceFilterQueryBuilder.php | 2 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 3 + .../ContactSegmentServiceFunctionalTest.php | 166 ++++++++---------- 5 files changed, 132 insertions(+), 99 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 976b90f7951..25d272e2e41 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -72,6 +72,29 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) return $queryBuilder; } + /** + * @param LeadList $segment + * + * @return QueryBuilder + * + * @throws Exception\SegmentQueryException + * @throws \Exception + */ + private function getTotalSegmentContactsQuery(LeadList $segment) + { + if (!is_null($this->preparedQB)) { + return $this->preparedQB; + } + + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); + + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $segment->getId()); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId()); + + return $queryBuilder; + } + /** * @param LeadList $segment * @param array $batchLimiters @@ -107,6 +130,41 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters return [$segment->getId() => $result]; } + /** + * @param LeadList $segment + * + * @return array + * + * @throws \Exception + * + * @todo This is almost copy of getNewLeadListLeadsCount method. Only difference is that it calls getTotalSegmentContactsQuery + */ + public function getTotalLeadListLeadsCount(LeadList $segment) + { + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); + + if (!count($segmentFilters)) { + $this->logger->debug('Segment QB: Segment has no filters', ['segmentId' => $segment->getId()]); + + return [$segment->getId() => [ + 'count' => '0', + 'maxId' => '0', + ], + ]; + } + + $qb = $this->getTotalSegmentContactsQuery($segment); + + $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); + + $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); + dump($qb->getDebugOutput()); + + $result = $this->timedFetch($qb, $segment->getId()); + + return [$segment->getId() => $result]; + } + /** * @param LeadList $segment * @param array $batchLimiters diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index d16633185e6..744f9aeddf9 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -211,7 +211,7 @@ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadList * * @return QueryBuilder */ - public function addManuallyUnsubsribedQuery(QueryBuilder $queryBuilder, $leadListId) + public function addManuallyUnsubscribedQuery(QueryBuilder $queryBuilder, $leadListId) { $tableAlias = $this->generateRandomParameterName(); $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index 2989c6903ed..aaa8b7b982e 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -98,7 +98,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil // If the segment contains no filters; it means its for manually subscribed only if (count($filters)) { - $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubsribedQuery($segmentQueryBuilder, $contactSegment->getId()); + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); } $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index c130836cd57..06a72e23da6 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1557,6 +1557,9 @@ public function guessPrimaryLeadContactIdColumn() { $parts = $this->getQueryParts(); $leadTable = $parts['from'][0]['alias']; + if (!isset($parts['join'][$leadTable])) { + return $leadTable.'.id'; + } $joins = $parts['join'][$leadTable]; foreach ($joins as $join) { diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php index c716c6ee8da..f0b04354c7d 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php @@ -2,7 +2,6 @@ namespace Mautic\LeadBundle\Tests\Segment; -use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Test\MauticWebTestCase; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListRepository; @@ -21,204 +20,177 @@ public function testSegmentCountIsCorrect() * @var ContactSegmentService */ $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); - - $dtHelper = new DateTimeHelper(); - $batchLimiters = ['dateTime' => $dtHelper->toUtcString()]; - - $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); - $segmentContacts = $contactSegmentService->getNewLeadListLeadsCount($segmentTest1Ref, $batchLimiters); - - $this->assertEquals( - 1, - $segmentContacts[$segmentTest1Ref->getId()]['count'], - 'There should be 1 contacts in the segment-test-1 segment.' - ); - - return; - - /** - * @var LeadListRepository - */ - $repo = $this->em->getRepository(LeadList::class); - - $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); - $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); - $segmentTest4Ref = $this->fixtures->getReference('segment-test-4'); - $segmentTest5Ref = $this->fixtures->getReference('segment-test-5'); - $likePercentEndRef = $this->fixtures->getReference('like-percent-end'); - $segmentTestWithoutFiltersRef = $this->fixtures->getReference('segment-test-without-filters'); - $segmentTestIncludeMembershipWithFiltersRef = $this->fixtures->getReference('segment-test-include-segment-with-filters'); - $segmentTestExcludeMembershipWithFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-with-filters'); - $segmentTestIncludeMembershipWithoutFiltersRef = $this->fixtures->getReference('segment-test-include-segment-without-filters'); - $segmentTestExcludeMembershipWithoutFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-without-filters'); - $segmentTestIncludeMembershipMixedFiltersRef = $this->fixtures->getReference('segment-test-include-segment-mixed-filters'); - $segmentTestExcludeMembershipMixedFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-mixed-filters'); - $segmentTestMixedIncludeExcludeRef = $this->fixtures->getReference('segment-test-mixed-include-exclude-filters'); - $segmentTestManualMembership = $this->fixtures->getReference('segment-test-manual-membership'); - $segmentTestIncludeMembershipManualMembersRef = $this->fixtures->getReference('segment-test-include-segment-manual-members'); - $segmentTestExcludeMembershipManualMembersRef = $this->fixtures->getReference('segment-test-exclude-segment-manual-members'); - $segmentTestExcludeMembershipWithoutOtherFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-without-other-filters'); - $segmentTestIncludeWithUnrelatedManualRemovalRef = $this->fixtures->getReference( - 'segment-test-include-segment-with-unrelated-segment-manual-removal' - ); - $segmentMembershipRegex = $this->fixtures->getReference('segment-membership-regexp'); - $segmentCompanyFields = $this->fixtures->getReference('segment-company-only-fields'); - $segmentMembershipCompanyOnlyFields = $this->fixtures->getReference('segment-including-segment-with-company-only-fields'); - - $logger = $this->getMockBuilder(Logger::class) - ->disableOriginalConstructor() - ->getMock(); - - // These expect filters to be part of the $lists passed to getLeadsByList so pass the entity - $segmentContacts = $repo->getLeadsByList( - [ - $segmentTest1Ref, - $segmentTest2Ref, - $segmentTest3Ref, - $segmentTest4Ref, - $segmentTest5Ref, - $likePercentEndRef, - $segmentTestWithoutFiltersRef, - $segmentTestIncludeMembershipWithFiltersRef, - $segmentTestExcludeMembershipWithFiltersRef, - $segmentTestIncludeMembershipWithoutFiltersRef, - $segmentTestExcludeMembershipWithoutFiltersRef, - $segmentTestIncludeMembershipMixedFiltersRef, - $segmentTestExcludeMembershipMixedFiltersRef, - $segmentTestMixedIncludeExcludeRef, - $segmentTestManualMembership, - $segmentTestIncludeMembershipManualMembersRef, - $segmentTestExcludeMembershipManualMembersRef, - $segmentTestExcludeMembershipWithoutOtherFiltersRef, - $segmentTestIncludeWithUnrelatedManualRemovalRef, - $segmentMembershipRegex, - $segmentCompanyFields, - $segmentMembershipCompanyOnlyFields, - ], - ['countOnly' => true], - $logger - ); - - $this->assertEquals( - 4, - $segmentContacts[$segmentTest2Ref->getId()]['count'], - 'There should be 4 contacts in the segment-test-2 segment.' - ); - - $this->assertEquals( - 24, - $segmentContacts[$segmentTest3Ref->getId()]['count'], - 'There should be 24 contacts in the segment-test-3 segment' - ); - - $this->assertEquals( - 1, - $segmentContacts[$segmentTest4Ref->getId()]['count'], - 'There should be 1 contacts in the segment-test-4 segment.' - ); - + /* + $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest1Ref); + $this->assertEquals( + 1, + $segmentContacts[$segmentTest1Ref->getId()]['count'], + 'There should be 1 contacts in the segment-test-1 segment.' + ); + + $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest2Ref); + $this->assertEquals( + 4, + $segmentContacts[$segmentTest2Ref->getId()]['count'], + 'There should be 4 contacts in the segment-test-2 segment.' + ); + + $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest3Ref); + $this->assertEquals( + 24, + $segmentContacts[$segmentTest3Ref->getId()]['count'], + 'There should be 24 contacts in the segment-test-3 segment' + ); + + $segmentTest4Ref = $this->fixtures->getReference('segment-test-4'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest4Ref); + $this->assertEquals( + 1, + $segmentContacts[$segmentTest4Ref->getId()]['count'], + 'There should be 1 contacts in the segment-test-4 segment.' + ); + */ + $segmentTest5Ref = $this->fixtures->getReference('segment-test-5'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest5Ref); $this->assertEquals( 53, $segmentContacts[$segmentTest5Ref->getId()]['count'], 'There should be 53 contacts in the segment-test-5 segment.' ); + $likePercentEndRef = $this->fixtures->getReference('like-percent-end'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($likePercentEndRef); $this->assertEquals( 32, $segmentContacts[$likePercentEndRef->getId()]['count'], 'There should be 32 contacts in the like-percent-end segment.' ); + $segmentTestWithoutFiltersRef = $this->fixtures->getReference('segment-test-without-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestWithoutFiltersRef); $this->assertEquals( 0, $segmentContacts[$segmentTestWithoutFiltersRef->getId()]['count'], 'There should be 0 contacts in the segment-test-without-filters segment.' ); + $segmentTestIncludeMembershipWithFiltersRef = $this->fixtures->getReference('segment-test-include-segment-with-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestIncludeMembershipWithFiltersRef); $this->assertEquals( 26, $segmentContacts[$segmentTestIncludeMembershipWithFiltersRef->getId()]['count'], 'There should be 26 contacts in the segment-test-include-segment-with-filters segment. 24 from segment-test-3 that was not added yet plus 4 from segment-test-2 minus 2 for being in both = 26.' ); + $segmentTestExcludeMembershipWithFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-with-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestExcludeMembershipWithFiltersRef); $this->assertEquals( 7, $segmentContacts[$segmentTestExcludeMembershipWithFiltersRef->getId()]['count'], 'There should be 7 contacts in the segment-test-exclude-segment-with-filters segment. 8 that are in the US minus 1 that is in segment-test-3.' ); + $segmentTestIncludeMembershipWithoutFiltersRef = $this->fixtures->getReference('segment-test-include-segment-without-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestIncludeMembershipWithoutFiltersRef); $this->assertEquals( 0, $segmentContacts[$segmentTestIncludeMembershipWithoutFiltersRef->getId()]['count'], 'There should be 0 contacts as there is no one in segment-test-without-filters' ); + $segmentTestExcludeMembershipWithoutFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-without-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestExcludeMembershipWithoutFiltersRef); $this->assertEquals( 11, $segmentContacts[$segmentTestExcludeMembershipWithoutFiltersRef->getId()]['count'], 'There should be 11 contacts in the United Kingdom and 0 from segment-test-without-filters.' ); + $segmentTestIncludeMembershipMixedFiltersRef = $this->fixtures->getReference('segment-test-include-segment-mixed-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestIncludeMembershipMixedFiltersRef); $this->assertEquals( 24, $segmentContacts[$segmentTestIncludeMembershipMixedFiltersRef->getId()]['count'], 'There should be 24 contacts. 0 from segment-test-without-filters and 24 from segment-test-3.' ); + $segmentTestExcludeMembershipMixedFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-mixed-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestExcludeMembershipMixedFiltersRef); $this->assertEquals( 30, $segmentContacts[$segmentTestExcludeMembershipMixedFiltersRef->getId()]['count'], 'There should be 30 contacts. 0 from segment-test-without-filters and 30 from segment-test-3.' ); + $segmentTestMixedIncludeExcludeRef = $this->fixtures->getReference('segment-test-mixed-include-exclude-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestMixedIncludeExcludeRef); $this->assertEquals( 8, $segmentContacts[$segmentTestMixedIncludeExcludeRef->getId()]['count'], 'There should be 8 contacts. 32 from like-percent-end minus 24 from segment-test-3.' ); + $segmentTestManualMembership = $this->fixtures->getReference('segment-test-manual-membership'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestManualMembership); $this->assertEquals( 12, $segmentContacts[$segmentTestManualMembership->getId()]['count'], 'There should be 12 contacts. 11 in the United Kingdom plus 3 manually added minus 2 manually removed.' ); + $segmentTestIncludeMembershipManualMembersRef = $this->fixtures->getReference('segment-test-include-segment-manual-members'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestIncludeMembershipManualMembersRef); $this->assertEquals( 12, $segmentContacts[$segmentTestIncludeMembershipManualMembersRef->getId()]['count'], 'There should be 12 contacts in the included segment-test-include-segment-manual-members segment' ); + $segmentTestExcludeMembershipManualMembersRef = $this->fixtures->getReference('segment-test-exclude-segment-manual-members'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestExcludeMembershipManualMembersRef); $this->assertEquals( 25, $segmentContacts[$segmentTestExcludeMembershipManualMembersRef->getId()]['count'], 'There should be 25 contacts in the segment-test-exclude-segment-manual-members segment' ); + $segmentTestExcludeMembershipWithoutOtherFiltersRef = $this->fixtures->getReference('segment-test-exclude-segment-without-other-filters'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestExcludeMembershipWithoutOtherFiltersRef); $this->assertEquals( 42, $segmentContacts[$segmentTestExcludeMembershipWithoutOtherFiltersRef->getId()]['count'], 'There should be 42 contacts in the included segment-test-exclude-segment-without-other-filters segment' ); + $segmentTestIncludeWithUnrelatedManualRemovalRef = $this->fixtures->getReference('segment-test-include-segment-with-unrelated-segment-manual-removal'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTestIncludeWithUnrelatedManualRemovalRef); $this->assertEquals( 11, $segmentContacts[$segmentTestIncludeWithUnrelatedManualRemovalRef->getId()]['count'], 'There should be 11 contacts in the segment-test-include-segment-with-unrelated-segment-manual-removal segment where a contact has been manually removed form another list' ); + $segmentMembershipRegex = $this->fixtures->getReference('segment-membership-regexp'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentMembershipRegex); $this->assertEquals( 11, $segmentContacts[$segmentMembershipRegex->getId()]['count'], 'There should be 11 contacts that match the regex with dayrep.com in it' ); + $segmentCompanyFields = $this->fixtures->getReference('segment-company-only-fields'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentCompanyFields); $this->assertEquals( 6, $segmentContacts[$segmentCompanyFields->getId()]['count'], 'There should only be 6 in this segment (6 contacts belong to HostGator based in Houston)' ); + $segmentMembershipCompanyOnlyFields = $this->fixtures->getReference('segment-including-segment-with-company-only-fields'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentMembershipCompanyOnlyFields); $this->assertEquals( 14, $segmentContacts[$segmentMembershipCompanyOnlyFields->getId()]['count'], From ee3fc5354aecba7cb9087f635746c7476e72acb0 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 23 Feb 2018 09:09:01 +0100 Subject: [PATCH 135/778] add command to generate output containing old and new segment query for further processing and benchmarks --- .../CheckQueryPerformanceSupplierCommand.php | 136 ++++++++++++++++++ .../LeadBundle/Entity/LeadListRepository.php | 13 +- .../Segment/ContactSegmentService.php | 10 +- parse-jmeter-output.php | 40 ++++++ 4 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php create mode 100644 parse-jmeter-output.php diff --git a/app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php b/app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php new file mode 100644 index 00000000000..4a71e35f7d3 --- /dev/null +++ b/app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php @@ -0,0 +1,136 @@ +setName('mautic:segments:check-performance') + ->setDescription('Blah') + ->addOption('--segment-id', '-i', InputOption::VALUE_OPTIONAL, 'Set the ID of segment to process') + ->addOption('--skip-old', null, InputOption::VALUE_NONE, 'Skip old query builder'); + + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + define('blah_2', true); + $container = $this->getContainer(); + $this->logger = $container->get('monolog.logger.mautic'); + + /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ + $listModel = $container->get('mautic.lead.model.list'); + + $id = $input->getOption('segment-id'); + $verbose = $input->getOption('verbose'); + $this->skipOld = $input->getOption('skip-old'); + + $failed = $ok = 0; + + if ($id && substr($id, strlen($id) - 1, 1) != '+') { + $list = $listModel->getEntity($id); + + if (!$list) { + $output->writeln('Segment with id "'.$id.'" not found'); + + return 1; + } + $response = $this->runSegment($output, $verbose, $list, $listModel); + if ($response) { + ++$ok; + } else { + ++$failed; + } + } else { + $lists = $listModel->getEntities( + [ + 'iterator_mode' => true, + 'orderBy' => 'l.id', + ] + ); + + while (($l = $lists->next()) !== false) { + // Get first item; using reset as the key will be the ID and not 0 + $l = reset($l); + + if (substr($id, strlen($id) - 1, 1) == '+' and $l->getId() < intval(trim($id, '+'))) { + continue; + } + $response = $this->runSegment($output, $verbose, $l, $listModel); + if (!$response) { + ++$failed; + } else { + ++$ok; + } + } + + unset($l); + + unset($lists); + } + + $total = $ok + $failed; + //$output->writeln(''); + //$output->writeln(sprintf('Total success rate: %d%%, %d succeeded: and %s%s failed... ', round(($ok / $total) * 100), $ok, ($failed ? $failed : ''), (!$failed ? $failed : ''))); + + return 0; + } + + private function format_period($inputSeconds) + { + $now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', '')); + + return $now->format('H:i:s.u'); + } + + private function runSegment($output, $verbose, $l, ListModel $listModel) + { + //$output->write('Running segment '.$l->getId().'...'); + + if (!$this->skipOld) { + $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); + + $timer1 = microtime(true); + $processed = $listModel->getVersionOld($l); + $timer1 = microtime(true) - $timer1; + } else { + $processed = ['count'=>-1, 'maxId'=>-1]; + $timer1 = 0; + } + + $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); + + $timer2 = microtime(true); + $processed2 = $listModel->getVersionNew($l); + $timer2 = microtime(true) - $timer2; + + $processed2 = array_shift($processed2); + + return 0; + } +} diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 3e016b798ad..ad6e57c0e87 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -529,10 +529,15 @@ public function getLeadsByList($lists, $args = [], Logger $logger) } $logger->debug(sprintf('Old version SQL: %s', $sqlT)); - $timer = microtime(true); - $results = $q->execute()->fetchAll(); - $timer = microtime(true) - $timer; - $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); + if (defined('blah_2')) { + echo $id.";1;\"{$sqlT}\"\n"; + $results = []; + } else { + $timer = microtime(true); + $results = $q->execute()->fetchAll(); + $timer = microtime(true) - $timer; + $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); + } foreach ($results as $r) { if ($countOnly) { diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 2955fe43fd9..f30648e3d65 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -98,11 +98,13 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); - dump($qb->getLogicStack()); - $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); - - $result = $this->timedFetch($qb, $segment->getId()); + if (defined('blah_2')) { + echo $segment->getId().';2;"'.$qb->getDebugOutput()."\"\n"; + $result = []; + } else { + $result = $this->timedFetch($qb, $segment->getId()); + } return [$segment->getId() => $result]; } diff --git a/parse-jmeter-output.php b/parse-jmeter-output.php new file mode 100644 index 00000000000..14af3676e92 --- /dev/null +++ b/parse-jmeter-output.php @@ -0,0 +1,40 @@ +children() as $node) { + $response = $node->responseData->__toString(); + + $matches = null; + + $check = preg_match("/\[Select Statement\] select ([0-9]+) as id, ([0-9]+) as version from \((.*)\)/", $node->samplerData->__toString(), $matches); + + if (!$check) { + throw new \Exception('Invalid data'); + } + $attributes = $node->attributes(); + + if (isset($rows[$matches[1]][$matches[2]])) { + $imSum = ($rows[$matches[1]][$matches[2]] + $attributes['t']->__toString()) / (count($attributes['t']->__toString()) + 1); + $rows[$matches[1]][$matches[2]] = $imSum; + } + $rows[$matches[1]][$matches[2]] = $attributes['t']->__toString(); +} + +foreach ($rows as $segmentId=>$times) { + if (isset($times[1]) && isset($times[2])) { + printf("%d;%s;%s\n", $segmentId, $times[1], $times[2]); + } +} From be0405c7e7e5f8e9d232ecd5411975b0fb932aae Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 23 Feb 2018 12:03:11 +0100 Subject: [PATCH 136/778] fix notLike behaviour for foreign tables, return __toString back to life for filter --- .../LeadBundle/Segment/ContactSegmentFilter.php | 17 +++++++++++++++++ .../Filter/ForeignValueFilterQueryBuilder.php | 12 +++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 3fb98040d9b..0137106be8d 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -198,4 +198,21 @@ public function getDoNotContactParts() { return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); } + + /** + * @return mixed + * + * @throws QueryException + */ + public function __toString() + { + $data = [ + 'column' => $this->getColumn()->getName(), + 'operator' => $this->getOperator(), + 'glue' => $this->getGlue(), + 'queryBuilder' => get_class($this->getFilterQueryBuilder()), + ]; + + return print_r($data, true); + } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index c6a902fbf21..137c285f308 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -70,6 +70,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil break; case 'neq': + case 'notLike': $tableAlias = $this->generateRandomParameterName(); $queryBuilder = $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), @@ -81,7 +82,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->addLogic($queryBuilder->expr()->isNull($tableAlias.'.lead_id'), 'and'); break; default: - $tableAlias = $queryBuilder->getTableAlias($filter->getTable(), 'inner'); + $tableAlias = $queryBuilder->getTableAlias($filter->getTable(), 'left'); if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); @@ -92,6 +93,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $tableAlias, $tableAlias.'.lead_id = l.id' ); + $queryBuilder->addLogic($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id'), 'and'); } @@ -123,6 +125,14 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->addJoinCondition($tableAlias, $expression); $queryBuilder->setParametersPairs($parameters, $filterParameters); break; + case 'notLike': + $expression = $queryBuilder->expr()->like( + $tableAlias.'.'.$filter->getField(), + $filterParametersHolder + ); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + $queryBuilder->setParametersPairs($parameters, $filterParameters); + break; default: $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), From d49c4524f4f4c7ed0d989b4834f3bafa47d955bb Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 23 Feb 2018 12:34:56 +0100 Subject: [PATCH 137/778] fix handling of foreign table logic performed via baseQueryBuilder, cleanup foreign qb, or logix for multiple inclusive segment references in SegQB, fix __toString not use database access --- .../Segment/ContactSegmentFilter.php | 2 +- .../Query/Filter/BaseFilterQueryBuilder.php | 19 ++++--------------- .../Filter/ForeignValueFilterQueryBuilder.php | 10 ---------- .../SegmentReferenceFilterQueryBuilder.php | 7 ++++++- 4 files changed, 11 insertions(+), 27 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 0137106be8d..07d5fe2a040 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -207,7 +207,7 @@ public function getDoNotContactParts() public function __toString() { $data = [ - 'column' => $this->getColumn()->getName(), + 'column' => $this->getTable().'.'.$this->getField(), 'operator' => $this->getOperator(), 'glue' => $this->getGlue(), 'queryBuilder' => get_class($this->getFilterQueryBuilder()), diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 9a1ebfb1f88..973b3c4c580 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\ContactSegmentFilter; -use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -160,32 +159,22 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'between': case 'notBetween': case 'notRegexp': - if ($filterAggr) { - $expression = $queryBuilder->expr()->$filterOperator( - sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), - $filterParametersHolder - ); - } else { $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), $filterParametersHolder ); - } + break; default: throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } if ($queryBuilder->isJoinTable($filter->getTable())) { - if ($filterAggr) { - throw new SegmentQueryException('aggregate functions should not be used in basic filters. use different query builder'); - } else { - $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); - } - } else { - $queryBuilder->addLogic($expression, $filterGlue); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } + $queryBuilder->addLogic($expression, $filterGlue); + $queryBuilder->setParametersPairs($parameters, $filterParameters); return $queryBuilder; diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 137c285f308..0db60037584 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -99,17 +99,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil switch ($filterOperator) { case 'empty': - $queryBuilder->addSelect($tableAlias.'.lead_id'); - $expression = $queryBuilder->expr()->isNull( - $tableAlias.'.lead_id'); - $queryBuilder->addLogic($expression, 'and'); - break; case 'notEmpty': - $queryBuilder->addSelect($tableAlias.'.lead_id'); - $expression = $queryBuilder->expr()->isNull( - $tableAlias.'.lead_id'); - $queryBuilder->addLogic($expression, 'and'); - break; case 'notIn': $queryBuilder->addSelect($tableAlias.'.lead_id'); $expression = $queryBuilder->expr()->isNull( diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index aaa8b7b982e..6701e82aae7 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -118,7 +118,12 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $expression = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); } $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); - $queryBuilder->addLogic($expression, $filter->getGlue()); + + if (!$exclusion && count($segmentIds) > 1) { + $queryBuilder->addLogic($expression, 'or'); + } else { + $queryBuilder->addLogic($expression, $filter->getGlue()); + } } } From 1de144f007cd79228649b06cb2ec111ae36c889c Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 23 Feb 2018 13:06:51 +0100 Subject: [PATCH 138/778] fix handling of foreign table logic performed via baseQueryBuilder, cleanup foreign qb, or logix for multiple inclusive segment references in SegQB, fix __toString not use database access --- .../Segment/ContactSegmentService.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 6abbb221734..25d272e2e41 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -72,6 +72,29 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) return $queryBuilder; } + /** + * @param LeadList $segment + * + * @return QueryBuilder + * + * @throws Exception\SegmentQueryException + * @throws \Exception + */ + private function getTotalSegmentContactsQuery(LeadList $segment) + { + if (!is_null($this->preparedQB)) { + return $this->preparedQB; + } + + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); + + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $segment->getId()); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId()); + + return $queryBuilder; + } + /** * @param LeadList $segment * @param array $batchLimiters From 756eb5b30f1d39b2dedf03abc0e40768a13b66d5 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 23 Feb 2018 16:12:27 +0100 Subject: [PATCH 139/778] Functional test for new code works now --- .../Segment/ContactSegmentService.php | 2 +- .../Tests/Model/ListModelFunctionalTest.php | 14 +- .../ContactSegmentServiceFunctionalTest.php | 176 +++++++----------- 3 files changed, 74 insertions(+), 118 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 25d272e2e41..f54b0b2dd63 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -158,7 +158,7 @@ public function getTotalLeadListLeadsCount(LeadList $segment) $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); - dump($qb->getDebugOutput()); + //dump($qb->getDebugOutput()); $result = $this->timedFetch($qb, $segment->getId()); diff --git a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php index 56d5425e8c0..86c6bc4e25e 100644 --- a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php @@ -11,9 +11,7 @@ class ListModelFunctionalTest extends MauticWebTestCase { public function testSegmentCountIsCorrect() { - /** - * @var LeadListRepository - */ + /** @var LeadListRepository $repo */ $repo = $this->em->getRepository(LeadList::class); $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); @@ -73,7 +71,7 @@ public function testSegmentCountIsCorrect() ['countOnly' => true], $logger ); - dump($segmentContacts); + $this->assertEquals( 1, $segmentContacts[$segmentTest1Ref->getId()]['count'], @@ -209,9 +207,7 @@ public function testSegmentCountIsCorrect() public function testPublicSegmentsInContactPreferences() { - /** - * @var LeadListRepository - */ + /** @var LeadListRepository $repo */ $repo = $this->em->getRepository(LeadList::class); $lists = $repo->getGlobalLists(); @@ -227,9 +223,7 @@ public function testPublicSegmentsInContactPreferences() public function testSegmentRebuildCommand() { - /** - * @var LeadListRepository - */ + /** @var LeadListRepository $repo */ $repo = $this->em->getRepository(LeadList::class); $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php index 12fc5922342..f8e89fb8bbf 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php @@ -3,10 +3,7 @@ namespace Mautic\LeadBundle\Tests\Segment; use Mautic\CoreBundle\Test\MauticWebTestCase; -use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Entity\LeadListRepository; use Mautic\LeadBundle\Segment\ContactSegmentService; -use Monolog\Logger; /** * Class ContactSegmentServiceFunctionalTest @@ -16,43 +13,41 @@ class ContactSegmentServiceFunctionalTest extends MauticWebTestCase { public function testSegmentCountIsCorrect() { - /** - * @var ContactSegmentService - */ + /** @var ContactSegmentService $contactSegmentService */ $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); - /* - $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest1Ref); - $this->assertEquals( - 1, - $segmentContacts[$segmentTest1Ref->getId()]['count'], - 'There should be 1 contacts in the segment-test-1 segment.' - ); - - $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest2Ref); - $this->assertEquals( - 4, - $segmentContacts[$segmentTest2Ref->getId()]['count'], - 'There should be 4 contacts in the segment-test-2 segment.' - ); - - $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest3Ref); - $this->assertEquals( - 24, - $segmentContacts[$segmentTest3Ref->getId()]['count'], - 'There should be 24 contacts in the segment-test-3 segment' - ); - - $segmentTest4Ref = $this->fixtures->getReference('segment-test-4'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest4Ref); - $this->assertEquals( - 1, - $segmentContacts[$segmentTest4Ref->getId()]['count'], - 'There should be 1 contacts in the segment-test-4 segment.' - ); - */ + + $segmentTest1Ref = $this->fixtures->getReference('segment-test-1'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest1Ref); + $this->assertEquals( + 1, + $segmentContacts[$segmentTest1Ref->getId()]['count'], + 'There should be 1 contacts in the segment-test-1 segment.' + ); + + $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest2Ref); + $this->assertEquals( + 4, + $segmentContacts[$segmentTest2Ref->getId()]['count'], + 'There should be 4 contacts in the segment-test-2 segment.' + ); + + $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest3Ref); + $this->assertEquals( + 24, + $segmentContacts[$segmentTest3Ref->getId()]['count'], + 'There should be 24 contacts in the segment-test-3 segment' + ); + + $segmentTest4Ref = $this->fixtures->getReference('segment-test-4'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest4Ref); + $this->assertEquals( + 1, + $segmentContacts[$segmentTest4Ref->getId()]['count'], + 'There should be 1 contacts in the segment-test-4 segment.' + ); + $segmentTest5Ref = $this->fixtures->getReference('segment-test-5'); $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest5Ref); $this->assertEquals( @@ -198,72 +193,39 @@ public function testSegmentCountIsCorrect() ); } -// -// public function testPublicSegmentsInContactPreferences() -// { -// /** -// * @var LeadListRepository $repo -// */ -// $repo = $this->em->getRepository(LeadList::class); -// -// $lists = $repo->getGlobalLists(); -// -// $segmentTest2Ref = $this->fixtures->getReference('segment-test-2'); -// -// $this->assertArrayNotHasKey( -// $segmentTest2Ref->getId(), -// $lists, -// 'Non-public lists should not be returned by the `getGlobalLists()` method.' -// ); -// } -// -// public function testSegmentRebuildCommand() -// { -// /** -// * @var LeadListRepository $repo -// */ -// $repo = $this->em->getRepository(LeadList::class); -// $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); -// -// $this->runCommand('mautic:segments:update', [ -// '-i' => $segmentTest3Ref->getId(), -// '--env' => 'test', -// ]); -// -// $logger = $this->getMockBuilder(Logger::class) -// ->disableOriginalConstructor() -// ->getMock(); -// -// $segmentContacts = $repo->getLeadsByList([ -// $segmentTest3Ref, -// ], ['countOnly' => true], $logger); -// -// $this->assertEquals( -// 24, -// $segmentContacts[$segmentTest3Ref->getId()]['count'], -// 'There should be 24 contacts in the segment-test-3 segment after rebuilding from the command line.' -// ); -// -// // Remove the title from all contacts, rebuild the list, and check that list is updated -// $this->em->getConnection()->query(sprintf('UPDATE %sleads SET title = NULL;', MAUTIC_TABLE_PREFIX)); -// -// $this->runCommand('mautic:segments:update', [ -// '-i' => $segmentTest3Ref->getId(), -// '--env' => 'test', -// ]); -// -// $logger = $this->getMockBuilder(Logger::class) -// ->disableOriginalConstructor() -// ->getMock(); -// -// $segmentContacts = $repo->getLeadsByList([ -// $segmentTest3Ref, -// ], ['countOnly' => true], $logger); -// -// $this->assertEquals( -// 0, -// $segmentContacts[$segmentTest3Ref->getId()]['count'], -// 'There should be no contacts in the segment-test-3 segment after removing contact titles and rebuilding from the command line.' -// ); -// } + public function testSegmentRebuildCommand() + { + /** @var ContactSegmentService $contactSegmentService */ + $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); + $segmentTest3Ref = $this->fixtures->getReference('segment-test-3'); + + $this->runCommand('mautic:segments:update', [ + '-i' => $segmentTest3Ref->getId(), + '--env' => 'test', + ]); + + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest3Ref); + + $this->assertEquals( + 24, + $segmentContacts[$segmentTest3Ref->getId()]['count'], + 'There should be 24 contacts in the segment-test-3 segment after rebuilding from the command line.' + ); + + // Remove the title from all contacts, rebuild the list, and check that list is updated + $this->em->getConnection()->query(sprintf('UPDATE %sleads SET title = NULL;', MAUTIC_TABLE_PREFIX)); + + $this->runCommand('mautic:segments:update', [ + '-i' => $segmentTest3Ref->getId(), + '--env' => 'test', + ]); + + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest3Ref); + + $this->assertEquals( + 0, + $segmentContacts[$segmentTest3Ref->getId()]['count'], + 'There should be no contacts in the segment-test-3 segment after removing contact titles and rebuilding from the command line.' + ); + } } From 9f029939d8c3c6b2dd8a0ba6351d0c13e7a8fca5 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 27 Feb 2018 16:52:19 +0100 Subject: [PATCH 140/778] Date refactoring - prevent wrong results when calling getParameterValue multiple times --- .../Decorator/Date/DateOptionAbstract.php | 27 +++++++++---------- .../Decorator/Date/DateOptionFactory.php | 27 +++++++++---------- .../Decorator/Date/Day/DateDayAbstract.php | 5 ++-- .../Decorator/Date/Day/DateDayToday.php | 5 ++-- .../Decorator/Date/Day/DateDayTomorrow.php | 6 +++-- .../Decorator/Date/Day/DateDayYesterday.php | 6 +++-- .../Date/Month/DateMonthAbstract.php | 5 ++-- .../Decorator/Date/Month/DateMonthLast.php | 6 +++-- .../Decorator/Date/Month/DateMonthNext.php | 6 +++-- .../Decorator/Date/Month/DateMonthThis.php | 6 +++-- .../Decorator/Date/Week/DateWeekAbstract.php | 9 ++++--- .../Decorator/Date/Week/DateWeekLast.php | 6 +++-- .../Decorator/Date/Week/DateWeekNext.php | 6 +++-- .../Decorator/Date/Week/DateWeekThis.php | 6 +++-- .../Decorator/Date/Year/DateYearAbstract.php | 5 ++-- .../Decorator/Date/Year/DateYearLast.php | 6 +++-- .../Decorator/Date/Year/DateYearNext.php | 6 +++-- .../Decorator/Date/Year/DateYearThis.php | 6 +++-- 18 files changed, 86 insertions(+), 63 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 00181db83d2..edab2628056 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -23,11 +23,6 @@ abstract class DateOptionAbstract implements FilterDecoratorInterface */ protected $dateDecorator; - /** - * @var DateTimeHelper - */ - protected $dateTimeHelper; - /** * @var DateOptionParameters */ @@ -35,21 +30,21 @@ abstract class DateOptionAbstract implements FilterDecoratorInterface /** * @param DateDecorator $dateDecorator - * @param DateTimeHelper $dateTimeHelper * @param DateOptionParameters $dateOptionParameters */ - public function __construct(DateDecorator $dateDecorator, DateTimeHelper $dateTimeHelper, DateOptionParameters $dateOptionParameters) + public function __construct(DateDecorator $dateDecorator, DateOptionParameters $dateOptionParameters) { $this->dateDecorator = $dateDecorator; - $this->dateTimeHelper = $dateTimeHelper; $this->dateOptionParameters = $dateOptionParameters; } /** * This function is responsible for setting date. $this->dateTimeHelper holds date with midnight today. * Eg. +1 day for "tomorrow", -1 for yesterday etc. + * + * @param DateTimeHelper $dateTimeHelper */ - abstract protected function modifyBaseDate(); + abstract protected function modifyBaseDate(DateTimeHelper $dateTimeHelper); /** * This function is responsible for date modification for between operator. @@ -63,9 +58,11 @@ abstract protected function getModifierForBetweenRange(); * This function returns a value if between range is needed. Could return string for like operator or array for between operator * Eg. //LIKE 2018-01-23% for today, //LIKE 2017-12-% for last month, //LIKE 2017-% for last year, array for this week. * + * @param DateTimeHelper $dateTimeHelper + * * @return string|array */ - abstract protected function getValueForBetweenRange(); + abstract protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper); /** * This function returns an operator if between range is needed. Could return like or between. @@ -102,21 +99,23 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $this->modifyBaseDate(); + $dateTimeHelper = new DateTimeHelper('midnight today', null, 'local'); + + $this->modifyBaseDate($dateTimeHelper); $modifier = $this->getModifierForBetweenRange(); $dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d'; if ($this->dateOptionParameters->isBetweenRequired()) { - return $this->getValueForBetweenRange(); + return $this->getValueForBetweenRange($dateTimeHelper); } if ($this->dateOptionParameters->shouldIncludeMidnigh()) { $modifier .= ' -1 second'; - $this->dateTimeHelper->modify($modifier); + $dateTimeHelper->modify($modifier); } - return $this->dateTimeHelper->toUtcString($dateFormat); + return $dateTimeHelper->toUtcString($dateFormat); } public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index 32da13c3ad7..f9bf0aa5c69 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date; -use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday; use Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow; @@ -63,37 +62,35 @@ public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) $relativeDateStrings = $this->relativeDate->getRelativeDateStrings(); $dateOptionParameters = new DateOptionParameters($leadSegmentFilterCrate, $relativeDateStrings); - $dtHelper = new DateTimeHelper('midnight today', null, 'local'); - $timeframe = $dateOptionParameters->getTimeframe(); switch ($timeframe) { case 'birthday': case 'anniversary': return new DateAnniversary($this->dateDecorator); case 'today': - return new DateDayToday($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateDayToday($this->dateDecorator, $dateOptionParameters); case 'tomorrow': - return new DateDayTomorrow($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateDayTomorrow($this->dateDecorator, $dateOptionParameters); case 'yesterday': - return new DateDayYesterday($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateDayYesterday($this->dateDecorator, $dateOptionParameters); case 'week_last': - return new DateWeekLast($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateWeekLast($this->dateDecorator, $dateOptionParameters); case 'week_next': - return new DateWeekNext($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateWeekNext($this->dateDecorator, $dateOptionParameters); case 'week_this': - return new DateWeekThis($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateWeekThis($this->dateDecorator, $dateOptionParameters); case 'month_last': - return new DateMonthLast($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateMonthLast($this->dateDecorator, $dateOptionParameters); case 'month_next': - return new DateMonthNext($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateMonthNext($this->dateDecorator, $dateOptionParameters); case 'month_this': - return new DateMonthThis($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateMonthThis($this->dateDecorator, $dateOptionParameters); case 'year_last': - return new DateYearLast($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateYearLast($this->dateDecorator, $dateOptionParameters); case 'year_next': - return new DateYearNext($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateYearNext($this->dateDecorator, $dateOptionParameters); case 'year_this': - return new DateYearThis($this->dateDecorator, $dtHelper, $dateOptionParameters); + return new DateYearThis($this->dateDecorator, $dateOptionParameters); case $timeframe && (false !== strpos($timeframe[0], '-') || false !== strpos($timeframe[0], '+')): return new DateRelativeInterval($this->dateDecorator, $originalValue); default: diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php index ac596545f2a..ac03f116dcc 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayAbstract.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; @@ -27,9 +28,9 @@ protected function getModifierForBetweenRange() /** * {@inheritdoc} */ - protected function getValueForBetweenRange() + protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { - return $this->dateTimeHelper->toUtcString('Y-m-d%'); + return $dateTimeHelper->toUtcString('Y-m-d%'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php index 3c3ef194f36..3f4392efc05 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayToday.php @@ -11,13 +11,14 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateDayToday extends DateDayAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php index fd45b1b3fa8..bd91e32adc7 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayTomorrow.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateDayTomorrow extends DateDayAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->modify('+1 day'); + $dateTimeHelper->modify('+1 day'); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php index 222c35aa2e7..2a416fb41c2 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Day/DateDayYesterday.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Day; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateDayYesterday extends DateDayAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->modify('-1 day'); + $dateTimeHelper->modify('-1 day'); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php index 57f2be42cc4..db3bcb502d4 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthAbstract.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; @@ -27,9 +28,9 @@ protected function getModifierForBetweenRange() /** * {@inheritdoc} */ - protected function getValueForBetweenRange() + protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { - return $this->dateTimeHelper->toUtcString('Y-m-%'); + return $dateTimeHelper->toUtcString('Y-m-%'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php index 40e468e5dbc..2833bb0a59d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthLast.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateMonthLast extends DateMonthAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight first day of last month', null); + $dateTimeHelper->setDateTime('midnight first day of last month', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php index 81eb3797d80..e7acd31cf65 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthNext.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateMonthNext extends DateMonthAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight first day of next month', null); + $dateTimeHelper->setDateTime('midnight first day of next month', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php index 52d2e60bf9d..c9fc0030f49 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Month/DateMonthThis.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Month; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateMonthThis extends DateMonthAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight first day of this month', null); + $dateTimeHelper->setDateTime('midnight first day of this month', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php index 3d11e72b699..ea3b853dc3c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekAbstract.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; @@ -27,14 +28,14 @@ protected function getModifierForBetweenRange() /** * {@inheritdoc} */ - protected function getValueForBetweenRange() + protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { $dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d'; - $startWith = $this->dateTimeHelper->toUtcString($dateFormat); + $startWith = $dateTimeHelper->toUtcString($dateFormat); $modifier = $this->getModifierForBetweenRange().' -1 second'; - $this->dateTimeHelper->modify($modifier); - $endWith = $this->dateTimeHelper->toUtcString($dateFormat); + $dateTimeHelper->modify($modifier); + $endWith = $dateTimeHelper->toUtcString($dateFormat); return [$startWith, $endWith]; } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php index 382fe955e31..cb6ce3b9d0d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekLast.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateWeekLast extends DateWeekAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight monday last week', null); + $dateTimeHelper->setDateTime('midnight monday last week', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php index c24be66925f..8949a5f0240 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekNext.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateWeekNext extends DateWeekAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight monday next week', null); + $dateTimeHelper->setDateTime('midnight monday next week', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php index 1c74cc70f79..1bc030a772a 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Week/DateWeekThis.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Week; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateWeekThis extends DateWeekAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight monday this week', null); + $dateTimeHelper->setDateTime('midnight monday this week', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php index 89737319319..e2e95f283cd 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearAbstract.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionAbstract; @@ -27,9 +28,9 @@ protected function getModifierForBetweenRange() /** * {@inheritdoc} */ - protected function getValueForBetweenRange() + protected function getValueForBetweenRange(DateTimeHelper $dateTimeHelper) { - return $this->dateTimeHelper->toUtcString('Y-%'); + return $dateTimeHelper->toUtcString('Y-%'); } /** diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php index 7f332b897de..f3ad6b3c227 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateYearLast extends DateYearAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight first day of last year', null); + $dateTimeHelper->setDateTime('midnight first day of last year', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php index 134c51b8d5a..5db52997233 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateYearNext extends DateYearAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight first day of next year', null); + $dateTimeHelper->setDateTime('midnight first day of next year', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php index 11ccffa4bd5..a10c22b6a33 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php @@ -11,13 +11,15 @@ namespace Mautic\LeadBundle\Segment\Decorator\Date\Year; +use Mautic\CoreBundle\Helper\DateTimeHelper; + class DateYearThis extends DateYearAbstract { /** * {@inheritdoc} */ - protected function modifyBaseDate() + protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $this->dateTimeHelper->setDateTime('midnight first day of this year', null); + $dateTimeHelper->setDateTime('midnight first day of this year', null); } } From 6bb16eb9b383402e07e75eabd411943e69f17c1c Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 27 Feb 2018 16:53:07 +0100 Subject: [PATCH 141/778] Functional test for dates - last week + tomorrow --- .../DataFixtures/ORM/LoadLeadDateData.php | 97 +++++++++++++++++++ .../DataFixtures/ORM/LoadSegmentsData.php | 34 +++++++ .../Tests/Model/ListModelFunctionalTest.php | 4 +- .../ContactSegmentServiceFunctionalTest.php | 4 +- .../Date/RelativeDateFunctionalTest.php | 56 +++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php new file mode 100644 index 00000000000..b15ae964732 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php @@ -0,0 +1,97 @@ +container = $container; + } + + /** + * @param ObjectManager $manager + */ + public function load(ObjectManager $manager) + { + /** @var LeadRepository $leadRepository */ + $leadRepository = $this->container->get('doctrine.orm.default_entity_manager')->getRepository(Lead::class); + + $data = [ + [ + 'name' => 'Tomorrow', + 'initialTime' => 'midnight tomorrow', + 'dateModifier' => '+10 seconds', + ], + [ + 'name' => 'Last week', + 'initialTime' => 'midnight monday last week', + 'dateModifier' => '+2 days', + ], + ]; + + foreach ($data as $lead) { + $this->createLead($leadRepository, $lead['name'], $lead['initialTime'], $lead['dateModifier']); + } + } + + /** + * @return int + */ + public function getOrder() + { + return 6; + } + + /** + * @param LeadRepository $leadRepository + * @param string $name + * @param string $initialTime + * @param string $dateModifier + */ + private function createLead(LeadRepository $leadRepository, $name, $initialTime, $dateModifier) + { + $date = new \DateTime($initialTime); + $date->modify($dateModifier); + + $lead = new Lead(); + $lead->setLastname('Date'); + $lead->setFirstname($name); + $lead->setDateIdentified($date); + + $leadRepository->saveEntity($lead); + + $alias = strtolower(InputHelper::alphanum($name, false, '-')); + + $this->setReference('lead-date-'.$alias, $lead); + } +} diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index f529c650206..0f39da27d62 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -486,6 +486,40 @@ public function load(ObjectManager $manager) ], 'populate' => true, ], + [ // ID 26 + 'name' => 'Segment with relative date - last week', + 'alias' => 'segment-with-relative-date-last-week', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'last week', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 27 + 'name' => 'Segment with relative date - tomorrow', + 'alias' => 'segment-with-relative-date-tomorrow', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'tomorrow', + 'display' => null, + ], + ], + 'populate' => false, + ], ]; foreach ($segments as $segmentConfig) { diff --git a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php index 86c6bc4e25e..e74bc85832e 100644 --- a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php @@ -97,9 +97,9 @@ public function testSegmentCountIsCorrect() ); $this->assertEquals( - 53, + 54, $segmentContacts[$segmentTest5Ref->getId()]['count'], - 'There should be 53 contacts in the segment-test-5 segment.' + 'There should be 54 contacts in the segment-test-5 segment.' ); $this->assertEquals( diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php index f8e89fb8bbf..56fa764d27a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php @@ -51,9 +51,9 @@ public function testSegmentCountIsCorrect() $segmentTest5Ref = $this->fixtures->getReference('segment-test-5'); $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest5Ref); $this->assertEquals( - 53, + 54, $segmentContacts[$segmentTest5Ref->getId()]['count'], - 'There should be 53 contacts in the segment-test-5 segment.' + 'There should be 54 contacts in the segment-test-5 segment.' ); $likePercentEndRef = $this->fixtures->getReference('like-percent-end'); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php new file mode 100644 index 00000000000..b261c226b95 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php @@ -0,0 +1,56 @@ +container->get('mautic.lead.model.lead_segment_service'); + + $segmentLastWeekRef = $this->fixtures->getReference('segment-with-relative-date-last-week'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentLastWeekRef); + + $leadLastWeekRef = $this->fixtures->getReference('lead-date-last-week'); + + $this->assertEquals( + 1, + $segmentContacts[$segmentLastWeekRef->getId()]['count'], + 'There should be 1 contacts in the segment-with-relative-date-last-week segment.' + ); + $this->assertEquals( + $leadLastWeekRef->getId(), + $segmentContacts[$segmentLastWeekRef->getId()]['maxId'], + 'MaxId in the segment-with-relative-date-last-week segment should be ID of Lead.' + ); + } + + public function testSegmentCountIsCorrectForTomorrow() + { + /** @var ContactSegmentService $contactSegmentService */ + $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); + + $segmentTomorrowRef = $this->fixtures->getReference('segment-with-relative-date-tomorrow'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTomorrowRef); + + $leadTomorrowRef = $this->fixtures->getReference('lead-date-tomorrow'); + + $this->assertEquals( + 1, + $segmentContacts[$segmentTomorrowRef->getId()]['count'], + 'There should be 1 contacts in the segment-with-relative-date-tomorrow segment.' + ); + $this->assertEquals( + $leadTomorrowRef->getId(), + $segmentContacts[$segmentTomorrowRef->getId()]['maxId'], + 'MaxId in the segment-with-relative-date-tomorrow segment should be ID of Lead.' + ); + } +} From ab687918d42e8384c4a575cc1901b544f00acd94 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 11:17:13 +0100 Subject: [PATCH 142/778] Delete LeadListSegmentRepository - replaced by service --- .../Entity/LeadListSegmentRepository.php | 1162 ----------------- 1 file changed, 1162 deletions(-) delete mode 100644 app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php diff --git a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php b/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php deleted file mode 100644 index a690a4fad03..00000000000 --- a/app/bundles/LeadBundle/Entity/LeadListSegmentRepository.php +++ /dev/null @@ -1,1162 +0,0 @@ -entityManager = $entityManager; - $this->dispatcher = $dispatcher; - $this->randomParameterName = $randomParameterName; - } - - public function getNewLeadsByListCount($id, ContactSegmentFilters $leadSegmentFilters, array $batchLimiters) - { - //TODO - $withMinId = false; - $limit = null; - $start = null; - - if (!count($leadSegmentFilters)) { - return 0; - } - - $q = $this->entityManager->getConnection()->createQueryBuilder(); - $select = 'count(l.id) as lead_count, max(l.id) as max_id'; - if ($withMinId) { - $select .= ', min(l.id) as min_id'; - } - - $q->select($select) - ->from(MAUTIC_TABLE_PREFIX.'leads', 'l'); - - $batchExpr = $q->expr()->andX(); - // Only leads that existed at the time of count - if ($batchLimiters) { - if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { - $batchExpr->add( - $q->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") - ); - } elseif (!empty($batchLimiters['maxId'])) { - $batchExpr->add( - $q->expr()->lte('l.id', $batchLimiters['maxId']) - ); - } - } - - $expr = $this->generateSegmentExpression($leadSegmentFilters, $q, $id); - - // Leads that do not have any record in the lead_lists_leads table for this lead list - // For non null fields - it's apparently better to use left join over not exists due to not using nullable - // fields - https://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/ - $listOnExpr = $q->expr()->andX( - $q->expr()->eq('ll.leadlist_id', $id), - $q->expr()->eq('ll.lead_id', 'l.id') - ); - - if (!empty($batchLimiters['dateTime'])) { - // Only leads in the list at the time of count - $listOnExpr->add( - $q->expr()->lte('ll.date_added', $q->expr()->literal($batchLimiters['dateTime'])) - ); - } - - $q->leftJoin( - 'l', - MAUTIC_TABLE_PREFIX.'lead_lists_leads', - 'll', - $listOnExpr - ); - - $expr->add($q->expr()->isNull('ll.lead_id')); - - if ($batchExpr->count()) { - $expr->add($batchExpr); - } - - if ($expr->count()) { - $q->andWhere($expr); - } - - if (!empty($limit)) { - $q->setFirstResult($start) - ->setMaxResults($limit); - } - - // remove any possible group by - $q->resetQueryPart('groupBy'); - - dump($q->getSQL()); - - $start = microtime(true); - $results = $q->execute()->fetchAll(); - $end = microtime(true) - $start; - dump('Query took '.(1000 * $end).'ms'); - - $leads = []; - foreach ($results as $r) { - $leads = [ - 'count' => $r['lead_count'], - 'maxId' => $r['max_id'], - ]; - if ($withMinId) { - $leads['minId'] = $r['min_id']; - } - } - - return $leads; - } - - private function generateSegmentExpression(ContactSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId = null) - { - $expr = $this->getListFilterExpr($leadSegmentFilters, $q, $listId); - - if ($leadSegmentFilters->isHasCompanyFilter()) { - $this->applyCompanyFieldFilters($q, $leadSegmentFilters); - } - - return $expr; - } - - /** - * @param ContactSegmentFilters $leadSegmentFilters - * @param QueryBuilder $q - * @param int $listId - * - * @return \Doctrine\DBAL\Query\Expression\CompositeExpression|mixed - */ - private function getListFilterExpr(ContactSegmentFilters $leadSegmentFilters, QueryBuilder $q, $listId) - { - $parameters = []; - - $schema = $this->entityManager->getConnection()->getSchemaManager(); - // Get table columns - $leadTableSchema = $schema->listTableColumns(MAUTIC_TABLE_PREFIX.'leads'); - $companyTableSchema = $schema->listTableColumns(MAUTIC_TABLE_PREFIX.'companies'); - - $groups = []; - $groupExpr = $q->expr()->andX(); - - foreach ($leadSegmentFilters as $k => $leadSegmentFilter) { - $leadSegmentFilter = new ContactSegmentFilterOld((array) $leadSegmentFilter->contactSegmentFilterCrate); - //$object = $leadSegmentFilter->getObject(); - - $column = false; - $field = false; - - $filterField = $leadSegmentFilter->getField(); - - if ($filterField == 'lead_email_read_date') { - } - - if ($leadSegmentFilter->isLeadType()) { - $column = isset($leadTableSchema[$filterField]) ? $leadTableSchema[$filterField] : false; - $field = "l.{$filterField}"; - } elseif ($leadSegmentFilter->isCompanyType()) { - $column = isset($companyTableSchema[$filterField]) ? $companyTableSchema[$filterField] : false; - $field = "comp.{$filterField}"; - } - - //the next one will determine the group - if ($leadSegmentFilter->getGlue() === 'or') { - // Create a new group of andX expressions - if ($groupExpr->count()) { - $groups[] = $groupExpr; - $groupExpr = $q->expr()->andX(); - } - } - - $parameter = $this->generateRandomParameterName(); - $exprParameter = ":$parameter"; - $ignoreAutoFilter = false; - - $func = $leadSegmentFilter->getFunc(); - // Generate a unique alias - $alias = $this->generateRandomParameterName(); - -// var_dump($func.":".$leadSegmentFilter->getField()); -// var_dump($exprParameter); - - switch ($leadSegmentFilter->getField()) { - case 'hit_url': - case 'referer': - case 'source': - case 'source_id': - case 'url_title': - $operand = in_array( - $func, - [ - 'eq', - 'like', - 'regexp', - 'notRegexp', - 'startsWith', - 'endsWith', - 'contains', - ] - ) ? 'EXISTS' : 'NOT EXISTS'; - - $ignoreAutoFilter = true; - $column = $leadSegmentFilter->getField(); - - if ($column === 'hit_url') { - $column = 'url'; - } - - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX.'page_hits', $alias); - - switch ($func) { - case 'eq': - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.'.$column, $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) - ); - break; - case 'regexp': - case 'notRegexp': - $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); - $not = ($func === 'notRegexp') ? ' NOT' : ''; - $subqb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.lead_id', 'l.id'), - $alias.'.'.$column.$not.' REGEXP '.$exprParameter - ) - ); - break; - case 'like': - case 'notLike': - case 'startsWith': - case 'endsWith': - case 'contains': - switch ($func) { - case 'like': - case 'notLike': - case 'contains': - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; - break; - case 'startsWith': - $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; - break; - case 'endsWith': - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); - break; - } - - $subqb->where( - $q->expr()->andX( - $q->expr()->like($alias.'.'.$column, $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) - ); - break; - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'device_model': - $ignoreAutoFilter = true; - $operand = in_array($func, ['eq', 'like', 'regexp', 'notRegexp']) ? 'EXISTS' : 'NOT EXISTS'; - - $column = $leadSegmentFilter->getField(); - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX.'lead_devices', $alias); - switch ($func) { - case 'eq': - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.'.$column, $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) - ); - break; - case 'like': - case '!like': - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; - $subqb->where( - $q->expr()->andX( - $q->expr()->like($alias.'.'.$column, $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) - ); - break; - case 'regexp': - case 'notRegexp': - $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); - $not = ($func === 'notRegexp') ? ' NOT' : ''; - $subqb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.lead_id', 'l.id'), - $alias.'.'.$column.$not.' REGEXP '.$exprParameter - ) - ); - break; - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - - break; - case 'hit_url_date': - case 'lead_email_read_date': - $operand = (in_array($func, ['eq', 'gt', 'lt', 'gte', 'lte', 'between'])) ? 'EXISTS' : 'NOT EXISTS'; - $table = 'page_hits'; - $column = 'date_hit'; - - if ($leadSegmentFilter->getField() === 'lead_email_read_date') { - $column = 'date_read'; - $table = 'email_stats'; - } - - if ($filterField == 'lead_email_read_date') { - var_dump($func); - } - - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('id') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - switch ($func) { - case 'eq': - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias.'.'.$column, $exprParameter), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) - ); - break; - case 'between': - case 'notBetween': - // Filter should be saved with double || to separate options - $parameter2 = $this->generateRandomParameterName(); - $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; - $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; - $exprParameter2 = ":$parameter2"; - $ignoreAutoFilter = true; - $field = $column; - - if ($func === 'between') { - $subqb->where( - $q->expr() - ->andX( - $q->expr()->gte($alias.'.'.$field, $exprParameter), - $q->expr()->lt($alias.'.'.$field, $exprParameter2), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) - ); - } else { - $subqb->where( - $q->expr() - ->andX( - $q->expr()->lt($alias.'.'.$field, $exprParameter), - $q->expr()->gte($alias.'.'.$field, $exprParameter2), - $q->expr()->eq($alias.'.lead_id', 'l.id') - ) - ); - } - break; - default: - $parameter2 = $this->generateRandomParameterName(); - - if ($filterField == 'lead_email_read_date') { - var_dump($exprParameter); - } - $parameters[$parameter2] = $leadSegmentFilter->getFilter(); - - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->$func( - $alias.'.'.$column, - $parameter2 - ), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) - ); - break; - } - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'page_id': - case 'email_id': - case 'redirect_id': - case 'notification': - $operand = ($func === 'eq') ? 'EXISTS' : 'NOT EXISTS'; - $column = $leadSegmentFilter->getField(); - $table = 'page_hits'; - $select = 'id'; - - if ($leadSegmentFilter->getField() === 'notification') { - $table = 'push_ids'; - $column = 'id'; - } - - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - if ($leadSegmentFilter->getFilter() == 1) { - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->isNotNull($alias.'.'.$column), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) - ); - } else { - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->isNull($alias.'.'.$column), - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) - ); - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'sessions': - $operand = 'EXISTS'; - $table = 'page_hits'; - $select = 'COUNT(id)'; - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - $alias2 = $this->generateRandomParameterName(); - $subqb2 = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($alias2.'.id') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias2); - - $subqb2->where( - $q->expr() - ->andX( - $q->expr()->eq($alias2.'.lead_id', 'l.id'), - $q->expr()->gt($alias2.'.date_hit', '('.$alias.'.date_hit - INTERVAL 30 MINUTE)'), - $q->expr()->lt($alias2.'.date_hit', $alias.'.date_hit') - ) - ); - - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias.'.lead_id', 'l.id'), - $q->expr() - ->isNull($alias.'.email_id'), - $q->expr() - ->isNull($alias.'.redirect_id'), - sprintf('%s (%s)', 'NOT EXISTS', $subqb2->getSQL()) - ) - ); - - $opr = ''; - switch ($func) { - case 'eq': - $opr = '='; - break; - case 'gt': - $opr = '>'; - break; - case 'gte': - $opr = '>='; - break; - case 'lt': - $opr = '<'; - break; - case 'lte': - $opr = '<='; - break; - } - if ($opr) { - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); - } - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - case 'hit_url_count': - case 'lead_email_read_count': - $operand = 'EXISTS'; - $table = 'page_hits'; - $select = 'COUNT(id)'; - if ($leadSegmentFilter->getField() === 'lead_email_read_count') { - $table = 'email_stats'; - $select = 'COALESCE(SUM(open_count),0)'; - } - $subqb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select($select) - ->from(MAUTIC_TABLE_PREFIX.$table, $alias); - - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->where( - $q->expr() - ->andX( - $q->expr() - ->eq($alias.'.lead_id', 'l.id') - ) - ); - - $opr = ''; - switch ($func) { - case 'eq': - $opr = '='; - break; - case 'gt': - $opr = '>'; - break; - case 'gte': - $opr = '>='; - break; - case 'lt': - $opr = '<'; - break; - case 'lte': - $opr = '<='; - break; - } - - if ($opr) { - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - $subqb->having($select.$opr.$leadSegmentFilter->getFilter()); - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subqb->getSQL())); - break; - - case 'dnc_bounced': - case 'dnc_unsubscribed': - case 'dnc_bounced_sms': - case 'dnc_unsubscribed_sms': - // Special handling of do not contact - $func = (($func === 'eq' && $leadSegmentFilter->getFilter()) || ($func === 'neq' && !$leadSegmentFilter->getFilter())) ? 'EXISTS' : 'NOT EXISTS'; - - $parts = explode('_', $leadSegmentFilter->getField()); - $channel = 'email'; - - if (count($parts) === 3) { - $channel = $parts[2]; - } - - $channelParameter = $this->generateRandomParameterName(); - $subqb = $this->entityManager->getConnection()->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', $alias) - ->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.reason', $exprParameter), - $q->expr()->eq($alias.'.lead_id', 'l.id'), - $q->expr()->eq($alias.'.channel', ":$channelParameter") - ) - ); - - $groupExpr->add( - sprintf('%s (%s)', $func, $subqb->getSQL()) - ); - - // Filter will always be true and differentiated via EXISTS/NOT EXISTS - $leadSegmentFilter->setFilter(true); - - $ignoreAutoFilter = true; - - $parameters[$parameter] = ($parts[1] === 'bounced') ? DoNotContact::BOUNCED : DoNotContact::UNSUBSCRIBED; - $parameters[$channelParameter] = $channel; - - break; - - case 'leadlist': - $table = 'lead_lists_leads'; - $column = 'leadlist_id'; - $falseParameter = $this->generateRandomParameterName(); - $parameters[$falseParameter] = false; - $trueParameter = $this->generateRandomParameterName(); - $parameters[$trueParameter] = true; - $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; - $ignoreAutoFilter = true; - - if ($filterListIds = (array) $leadSegmentFilter->getFilter()) { - $listQb = $this->entityManager->getConnection()->createQueryBuilder() - ->select('l.id, l.filters') - ->from(MAUTIC_TABLE_PREFIX.'lead_lists', 'l'); - $listQb->where( - $listQb->expr()->in('l.id', $filterListIds) - ); - $filterLists = $listQb->execute()->fetchAll(); - $not = 'NOT EXISTS' === $func; - - // Each segment's filters must be appended as ORs so that each list is evaluated individually - $existsExpr = $not ? $listQb->expr()->andX() : $listQb->expr()->orX(); - - foreach ($filterLists as $list) { - $alias = $this->generateRandomParameterName(); - $id = (int) $list['id']; - if ($id === (int) $listId) { - // Ignore as somehow self is included in the list - continue; - } - - $listFilters = unserialize($list['filters']); - if (empty($listFilters)) { - // Use an EXISTS/NOT EXISTS on contact membership as this is a manual list - $subQb = $this->createFilterExpressionSubQuery( - $table, - $alias, - $column, - $id, - $parameters, - [ - $alias.'.manually_removed' => $falseParameter, - ] - ); - } else { - // Build a EXISTS/NOT EXISTS using the filters for this list to include/exclude those not processed yet - // but also leverage the current membership to take into account those manually added or removed from the segment - - // Build a "live" query based on current filters to catch those that have not been processed yet - $subQb = $this->createFilterExpressionSubQuery('leads', $alias, null, null, $parameters); - $filterExpr = $this->generateSegmentExpression($leadSegmentFilters, $subQb, $id); - - // Left join membership to account for manually added and removed - $membershipAlias = $this->generateRandomParameterName(); - $subQb->leftJoin( - $alias, - MAUTIC_TABLE_PREFIX.$table, - $membershipAlias, - "$membershipAlias.lead_id = $alias.id AND $membershipAlias.leadlist_id = $id" - ) - ->where( - $subQb->expr()->orX( - $filterExpr, - $subQb->expr()->eq("$membershipAlias.manually_added", ":$trueParameter") //include manually added - ) - ) - ->andWhere( - $subQb->expr()->eq("$alias.id", 'l.id'), - $subQb->expr()->orX( - $subQb->expr()->isNull("$membershipAlias.manually_removed"), // account for those not in a list yet - $subQb->expr()->eq("$membershipAlias.manually_removed", ":$falseParameter") //exclude manually removed - ) - ); - } - - $existsExpr->add( - sprintf('%s (%s)', $func, $subQb->getSQL()) - ); - } - - if ($existsExpr->count()) { - $groupExpr->add($existsExpr); - } - } - - break; - case 'tags': - case 'globalcategory': - case 'lead_email_received': - case 'lead_email_sent': - case 'device_type': - case 'device_brand': - case 'device_os': - // Special handling of lead lists and tags - $func = in_array($func, ['eq', 'in'], true) ? 'EXISTS' : 'NOT EXISTS'; - - $ignoreAutoFilter = true; - - // Collect these and apply after building the query because we'll want to apply the lead first for each of the subqueries - $subQueryFilters = []; - switch ($leadSegmentFilter->getField()) { - case 'tags': - $table = 'lead_tags_xref'; - $column = 'tag_id'; - break; - case 'globalcategory': - $table = 'lead_categories'; - $column = 'category_id'; - break; - case 'lead_email_received': - $table = 'email_stats'; - $column = 'email_id'; - - $trueParameter = $this->generateRandomParameterName(); - $subQueryFilters[$alias.'.is_read'] = $trueParameter; - $parameters[$trueParameter] = true; - break; - case 'lead_email_sent': - $table = 'email_stats'; - $column = 'email_id'; - break; - case 'device_type': - $table = 'lead_devices'; - $column = 'device'; - break; - case 'device_brand': - $table = 'lead_devices'; - $column = 'device_brand'; - break; - case 'device_os': - $table = 'lead_devices'; - $column = 'device_os_name'; - break; - } - - $subQb = $this->createFilterExpressionSubQuery( - $table, - $alias, - $column, - $leadSegmentFilter->getFilter(), - $parameters, - $subQueryFilters - ); - - $groupExpr->add( - sprintf('%s (%s)', $func, $subQb->getSQL()) - ); - break; - case 'stage': - // A note here that SQL EXISTS is being used for the eq and neq cases. - // I think this code might be inefficient since the sub-query is rerun - // for every row in the outer query's table. This might have to be refactored later on - // if performance is desired. - - $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'stages', $alias); - - switch ($func) { - case 'empty': - $groupExpr->add( - $q->expr()->isNull('l.stage_id') - ); - break; - case 'notEmpty': - $groupExpr->add( - $q->expr()->isNotNull('l.stage_id') - ); - break; - case 'eq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subQb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.id', 'l.stage_id'), - $q->expr()->eq($alias.'.id', ":$parameter") - ) - ); - $groupExpr->add(sprintf('EXISTS (%s)', $subQb->getSQL())); - break; - case 'neq': - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - - $subQb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.id', 'l.stage_id'), - $q->expr()->eq($alias.'.id', ":$parameter") - ) - ); - $groupExpr->add(sprintf('NOT EXISTS (%s)', $subQb->getSQL())); - break; - } - - break; - case 'integration_campaigns': - $parameter2 = $this->generateRandomParameterName(); - $operand = in_array($func, ['eq', 'neq']) ? 'EXISTS' : 'NOT EXISTS'; - $ignoreAutoFilter = true; - - $subQb = $this->entityManager->getConnection() - ->createQueryBuilder() - ->select('null') - ->from(MAUTIC_TABLE_PREFIX.'integration_entity', $alias); - switch ($func) { - case 'eq': - case 'neq': - if (strpos($leadSegmentFilter->getFilter(), '::') !== false) { - list($integrationName, $campaignId) = explode('::', $leadSegmentFilter->getFilter()); - } else { - // Assuming this is a Salesforce integration for BC with pre 2.11.0 - $integrationName = 'Salesforce'; - $campaignId = $leadSegmentFilter->getFilter(); - } - - $parameters[$parameter] = $campaignId; - $parameters[$parameter2] = $integrationName; - $subQb->where( - $q->expr()->andX( - $q->expr()->eq($alias.'.integration', ":$parameter2"), - $q->expr()->eq($alias.'.integration_entity', "'CampaignMember'"), - $q->expr()->eq($alias.'.integration_entity_id', ":$parameter"), - $q->expr()->eq($alias.'.internal_entity', "'lead'"), - $q->expr()->eq($alias.'.internal_entity_id', 'l.id') - ) - ); - break; - } - - $groupExpr->add(sprintf('%s (%s)', $operand, $subQb->getSQL())); - - break; - default: - if (!$column) { - // Column no longer exists so continue - continue; - } - - switch ($func) { - case 'between': - case 'notBetween': - // Filter should be saved with double || to separate options - $parameter2 = $this->generateRandomParameterName(); - $parameters[$parameter] = $leadSegmentFilter->getFilter()[0]; - $parameters[$parameter2] = $leadSegmentFilter->getFilter()[1]; - $exprParameter2 = ":$parameter2"; - $ignoreAutoFilter = true; - - if ($func === 'between') { - $groupExpr->add( - $q->expr()->andX( - $q->expr()->gte($field, $exprParameter), - $q->expr()->lt($field, $exprParameter2) - ) - ); - } else { - $groupExpr->add( - $q->expr()->andX( - $q->expr()->lt($field, $exprParameter), - $q->expr()->gte($field, $exprParameter2) - ) - ); - } - break; - - case 'notEmpty': - $groupExpr->add( - $q->expr()->andX( - $q->expr()->isNotNull($field), - $q->expr()->neq($field, $q->expr()->literal('')) - ) - ); - $ignoreAutoFilter = true; - break; - - case 'empty': - $leadSegmentFilter->setFilter(''); - $groupExpr->add( - $this->generateFilterExpression($q, $field, 'eq', $exprParameter, true) - ); - break; - - case 'in': - case 'notIn': - $cleanFilter = []; - foreach ($leadSegmentFilter->getFilter() as $key => $value) { - $cleanFilter[] = $q->expr()->literal( - InputHelper::clean($value) - ); - } - $leadSegmentFilter->setFilter($cleanFilter); - - if ($leadSegmentFilter->getType() === 'multiselect') { - foreach ($leadSegmentFilter->getFilter() as $filter) { - $filter = trim($filter, "'"); - - if (substr($func, 0, 3) === 'not') { - $operator = 'NOT REGEXP'; - } else { - $operator = 'REGEXP'; - } - - $groupExpr->add( - $field." $operator '\\\\|?$filter\\\\|?'" - ); - } - } else { - $groupExpr->add( - $this->generateFilterExpression($q, $field, $func, $leadSegmentFilter->getFilter(), null) - ); - } - $ignoreAutoFilter = true; - break; - - case 'neq': - $groupExpr->add( - $this->generateFilterExpression($q, $field, $func, $exprParameter, null) - ); - break; - - case 'like': - case 'notLike': - case 'startsWith': - case 'endsWith': - case 'contains': - $ignoreAutoFilter = true; - - switch ($func) { - case 'like': - case 'notLike': - $parameters[$parameter] = (strpos($leadSegmentFilter->getFilter(), '%') === false) ? '%'.$leadSegmentFilter->getFilter().'%' - : $leadSegmentFilter->getFilter(); - break; - case 'startsWith': - $func = 'like'; - $parameters[$parameter] = $leadSegmentFilter->getFilter().'%'; - break; - case 'endsWith': - $func = 'like'; - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter(); - break; - case 'contains': - $func = 'like'; - $parameters[$parameter] = '%'.$leadSegmentFilter->getFilter().'%'; - break; - } - - $groupExpr->add( - $this->generateFilterExpression($q, $field, $func, $exprParameter, null) - ); - break; - case 'regexp': - case 'notRegexp': - $ignoreAutoFilter = true; - $parameters[$parameter] = $this->prepareRegex($leadSegmentFilter->getFilter()); - $not = ($func === 'notRegexp') ? ' NOT' : ''; - $groupExpr->add( - // Escape single quotes while accounting for those that may already be escaped - $field.$not.' REGEXP '.$exprParameter - ); - break; - default: - $ignoreAutoFilter = true; - $groupExpr->add($q->expr()->$func($field, $exprParameter)); - $parameters[$exprParameter] = $leadSegmentFilter->getFilter(); - } - } - - if (!$ignoreAutoFilter) { - $parameters[$parameter] = $leadSegmentFilter->getFilter(); - } - - if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { - $event = new LeadListFilteringEvent($leadSegmentFilter->toArray(), null, $alias, $func, $q, $this->entityManager); - $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); - if ($event->isFilteringDone()) { - $groupExpr->add($event->getSubQuery()); - } - } - } - - // Get the last of the filters - if ($groupExpr->count()) { - $groups[] = $groupExpr; - } - if (count($groups) === 1) { - // Only one andX expression - $expr = $groups[0]; - } elseif (count($groups) > 1) { - // Sets of expressions grouped by OR - $orX = $q->expr()->orX(); - $orX->addMultiple($groups); - - // Wrap in a andX for other functions to append - $expr = $q->expr()->andX($orX); - } else { - $expr = $groupExpr; - } - - foreach ($parameters as $k => $v) { - $paramType = null; - - if (is_array($v) && isset($v['type'], $v['value'])) { - $paramType = $v['type']; - $v = $v['value']; - } - $q->setParameter($k, $v, $paramType); - } - - return $expr; - } - - /** - * Generate a unique parameter name. - * - * @return string - */ - private function generateRandomParameterName() - { - return $this->randomParameterName->generateRandomParameterName(); - } - - /** - * @param QueryBuilder|\Doctrine\ORM\QueryBuilder $q - * @param $column - * @param $operator - * @param $parameter - * @param $includeIsNull true/false or null to auto determine based on operator - * - * @return mixed - */ - public function generateFilterExpression($q, $column, $operator, $parameter, $includeIsNull) - { - // in/notIn for dbal will use a raw array - if (!is_array($parameter) && strpos($parameter, ':') !== 0) { - $parameter = ":$parameter"; - } - - if (null === $includeIsNull) { - // Auto determine based on negate operators - $includeIsNull = (in_array($operator, ['neq', 'notLike', 'notIn'])); - } - - if ($includeIsNull) { - $expr = $q->expr()->orX( - $q->expr()->$operator($column, $parameter), - $q->expr()->isNull($column) - ); - } else { - $expr = $q->expr()->$operator($column, $parameter); - } - - return $expr; - } - - /** - * @param $table - * @param $alias - * @param $column - * @param $value - * @param array $parameters - * @param null $leadId - * @param array $subQueryFilters - * - * @return QueryBuilder - */ - protected function createFilterExpressionSubQuery($table, $alias, $column, $value, array &$parameters, array $subQueryFilters = []) - { - $subQb = $this->entityManager->getConnection()->createQueryBuilder(); - $subExpr = $subQb->expr()->andX(); - - if ('leads' !== $table) { - $subExpr->add( - $subQb->expr()->eq($alias.'.lead_id', 'l.id') - ); - } - - foreach ($subQueryFilters as $subColumn => $subParameter) { - $subExpr->add( - $subQb->expr()->eq($subColumn, ":$subParameter") - ); - } - - if (null !== $value && !empty($column)) { - $subFilterParamter = $this->generateRandomParameterName(); - $subFunc = 'eq'; - if (is_array($value)) { - $subFunc = 'in'; - $subExpr->add( - $subQb->expr()->in(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") - ); - $parameters[$subFilterParamter] = ['value' => $value, 'type' => \Doctrine\DBAL\Connection::PARAM_STR_ARRAY]; - } else { - $parameters[$subFilterParamter] = $value; - } - - $subExpr->add( - $subQb->expr()->$subFunc(sprintf('%s.%s', $alias, $column), ":$subFilterParamter") - ); - } - - $subQb->select('null') - ->from(MAUTIC_TABLE_PREFIX.$table, $alias) - ->where($subExpr); - - return $subQb; - } - - /** - * If there is a negate comparison such as not equal, empty, isNotLike or isNotIn then contacts without companies should - * be included but the way the relationship is handled needs to be different to optimize best for a posit vs negate. - * - * @param QueryBuilder $q - * @param ContactSegmentFilters $leadSegmentFilters - */ - private function applyCompanyFieldFilters(QueryBuilder $q, ContactSegmentFilters $leadSegmentFilters) - { - $joinType = $leadSegmentFilters->isListFiltersInnerJoinCompany() ? 'join' : 'leftJoin'; - // Join company tables for query optimization - $q->$joinType('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'l.id = cl.lead_id') - ->$joinType( - 'cl', - MAUTIC_TABLE_PREFIX.'companies', - 'comp', - 'cl.company_id = comp.id' - ); - - // Return only unique contacts - $q->groupBy('l.id'); - } -} From ecdde9f5258191ace659cfb53c091ad5d73d6107 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 11:29:04 +0100 Subject: [PATCH 143/778] Remove LeadListSegmentRepository from config too --- app/bundles/LeadBundle/Config/config.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index e9b4ac7b043..ba079eb5695 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -874,14 +874,6 @@ 'mautic.lead.model.random_parameter_name' => [ 'class' => \Mautic\LeadBundle\Segment\RandomParameterName::class, ], - 'mautic.lead.repository.lead_list_segment_repository' => [ - 'class' => \Mautic\LeadBundle\Entity\LeadListSegmentRepository::class, - 'arguments' => [ - 'doctrine.orm.entity_manager', - 'event_dispatcher', - 'mautic.lead.model.random_parameter_name', - ], - ], 'mautic.lead.segment.operator_options' => [ 'class' => \Mautic\LeadBundle\Segment\OperatorOptions::class, ], From 87c9d9de8f2184b321b4ea92227c50e0e70ac777 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 15:01:38 +0100 Subject: [PATCH 144/778] Functional tests for segments - do not use fixtures, each test fills its own data --- .../DataFixtures/ORM/LoadLeadDateData.php | 36 ++++++++++ .../Tests/Model/ListModelFunctionalTest.php | 4 +- .../Date/RelativeDateFunctionalTest.php | 68 +++++++++++++------ 3 files changed, 87 insertions(+), 21 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php index b15ae964732..d5b447a7ca2 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php @@ -47,17 +47,53 @@ public function load(ObjectManager $manager) $leadRepository = $this->container->get('doctrine.orm.default_entity_manager')->getRepository(Lead::class); $data = [ + [ + 'name' => 'Today', + 'initialTime' => 'midnight today', + 'dateModifier' => '+10 seconds', + ], [ 'name' => 'Tomorrow', 'initialTime' => 'midnight tomorrow', 'dateModifier' => '+10 seconds', ], + [ + 'name' => 'Yesterday', + 'initialTime' => 'midnight today', + 'dateModifier' => '-10 seconds', + ], [ 'name' => 'Last week', 'initialTime' => 'midnight monday last week', 'dateModifier' => '+2 days', ], + [ + 'name' => 'Next week', + 'initialTime' => 'midnight monday next week', + 'dateModifier' => '+2 days', + ], + [ + 'name' => 'This week', + 'initialTime' => 'midnight monday this week', + 'dateModifier' => '+2 days', + ], + [ + 'name' => 'Last month', + 'initialTime' => 'midnight first day of last month', + 'dateModifier' => '+2 days', + ], + [ + 'name' => 'Next month', + 'initialTime' => 'midnight first day of next month', + 'dateModifier' => '+2 days', + ], + [ + 'name' => 'This month', + 'initialTime' => 'midnight first day of this month', + 'dateModifier' => '+2 days', + ], ]; + $data = []; foreach ($data as $lead) { $this->createLead($leadRepository, $lead['name'], $lead['initialTime'], $lead['dateModifier']); diff --git a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php index e74bc85832e..86c6bc4e25e 100644 --- a/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/ListModelFunctionalTest.php @@ -97,9 +97,9 @@ public function testSegmentCountIsCorrect() ); $this->assertEquals( - 54, + 53, $segmentContacts[$segmentTest5Ref->getId()]['count'], - 'There should be 54 contacts in the segment-test-5 segment.' + 'There should be 53 contacts in the segment-test-5 segment.' ); $this->assertEquals( diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php index b261c226b95..53ff06bc088 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php @@ -3,6 +3,8 @@ namespace Mautic\LeadBundle\Tests\Segment\Decorator\Date; use Mautic\CoreBundle\Test\MauticWebTestCase; +use Mautic\LeadBundle\Entity\Lead; +use Mautic\LeadBundle\Entity\LeadRepository; use Mautic\LeadBundle\Segment\ContactSegmentService; /** @@ -10,47 +12,75 @@ */ class RelativeDateFunctionalTest extends MauticWebTestCase { + public function testSegmentCountIsCorrectForTomorrow() + { + $lead = $this->createLead('Tomorrow', 'midnight tomorrow', '+10 seconds'); + + /** @var ContactSegmentService $contactSegmentService */ + $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); + + $segmentTomorrowRef = $this->fixtures->getReference('segment-with-relative-date-tomorrow'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTomorrowRef); + + $this->assertEquals( + 1, + $segmentContacts[$segmentTomorrowRef->getId()]['count'], + 'There should be 1 contacts in the segment-with-relative-date-tomorrow segment.' + ); + $this->assertEquals( + $lead->getId(), + $segmentContacts[$segmentTomorrowRef->getId()]['maxId'], + 'MaxId in the segment-with-relative-date-tomorrow segment should be ID of Lead.' + ); + } + public function testSegmentCountIsCorrectForLastWeek() { + $lead = $this->createLead('Last week', 'midnight monday last week', '+2 days'); + /** @var ContactSegmentService $contactSegmentService */ $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); $segmentLastWeekRef = $this->fixtures->getReference('segment-with-relative-date-last-week'); $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentLastWeekRef); - $leadLastWeekRef = $this->fixtures->getReference('lead-date-last-week'); - $this->assertEquals( 1, $segmentContacts[$segmentLastWeekRef->getId()]['count'], 'There should be 1 contacts in the segment-with-relative-date-last-week segment.' ); $this->assertEquals( - $leadLastWeekRef->getId(), + $lead->getId(), $segmentContacts[$segmentLastWeekRef->getId()]['maxId'], 'MaxId in the segment-with-relative-date-last-week segment should be ID of Lead.' ); } - public function testSegmentCountIsCorrectForTomorrow() + /** + * @param string $name + * @param string $initialTime + * @param string $dateModifier + * + * @return Lead + */ + private function createLead($name, $initialTime, $dateModifier) { - /** @var ContactSegmentService $contactSegmentService */ - $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); + // Remove the title from all contacts, rebuild the list, and check that list is updated + $this->em->getConnection()->query(sprintf("DELETE FROM %sleads WHERE lastname = 'Date';", MAUTIC_TABLE_PREFIX)); - $segmentTomorrowRef = $this->fixtures->getReference('segment-with-relative-date-tomorrow'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTomorrowRef); + /** @var LeadRepository $leadRepository */ + $leadRepository = $this->container->get('doctrine.orm.default_entity_manager')->getRepository(Lead::class); - $leadTomorrowRef = $this->fixtures->getReference('lead-date-tomorrow'); + $date = new \DateTime($initialTime); + $date->modify($dateModifier); - $this->assertEquals( - 1, - $segmentContacts[$segmentTomorrowRef->getId()]['count'], - 'There should be 1 contacts in the segment-with-relative-date-tomorrow segment.' - ); - $this->assertEquals( - $leadTomorrowRef->getId(), - $segmentContacts[$segmentTomorrowRef->getId()]['maxId'], - 'MaxId in the segment-with-relative-date-tomorrow segment should be ID of Lead.' - ); + $lead = new Lead(); + $lead->setLastname('Date'); + $lead->setFirstname($name); + $lead->setDateIdentified($date); + + $leadRepository->saveEntity($lead); + + return $lead; } } From d03061a4e19459b1bd66ead4475ecb1362a37b46 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 15:36:26 +0100 Subject: [PATCH 145/778] Simplify calling date tests, add filter to segment to be sure only date segments will apply, add test for today --- .../DataFixtures/ORM/LoadSegmentsData.php | 50 ++++++++++++++- .../Date/RelativeDateFunctionalTest.php | 63 ++++++++++++------- 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index 0f39da27d62..38660be1457 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -487,8 +487,8 @@ public function load(ObjectManager $manager) 'populate' => true, ], [ // ID 26 - 'name' => 'Segment with relative date - last week', - 'alias' => 'segment-with-relative-date-last-week', + 'name' => 'Segment with relative date - today', + 'alias' => 'segment-with-relative-date-today', 'public' => true, 'filters' => [ [ @@ -497,7 +497,16 @@ public function load(ObjectManager $manager) 'object' => 'lead', 'field' => 'date_identified', 'operator' => '=', - 'filter' => 'last week', + 'filter' => 'today', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', 'display' => null, ], ], @@ -517,6 +526,41 @@ public function load(ObjectManager $manager) 'filter' => 'tomorrow', 'display' => null, ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID xx + 'name' => 'Segment with relative date - last week', + 'alias' => 'segment-with-relative-date-last-week', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'last week', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], ], 'populate' => false, ], diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php index 53ff06bc088..cca15167c2a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php @@ -2,6 +2,7 @@ namespace Mautic\LeadBundle\Tests\Segment\Decorator\Date; +use Mautic\CoreBundle\Helper\InputHelper; use Mautic\CoreBundle\Test\MauticWebTestCase; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; @@ -12,47 +13,56 @@ */ class RelativeDateFunctionalTest extends MauticWebTestCase { - public function testSegmentCountIsCorrectForTomorrow() + public function testSegmentCountIsCorrectForToday() { - $lead = $this->createLead('Tomorrow', 'midnight tomorrow', '+10 seconds'); + $name = 'Today'; + $lead = $this->createLead($name, 'midnight today', '+10 seconds'); - /** @var ContactSegmentService $contactSegmentService */ - $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); + $this->checkSegmentResult($name, $lead); + } - $segmentTomorrowRef = $this->fixtures->getReference('segment-with-relative-date-tomorrow'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTomorrowRef); + public function testSegmentCountIsCorrectForTomorrow() + { + $name = 'Tomorrow'; + $lead = $this->createLead('Tomorrow', 'midnight tomorrow', '+10 seconds'); - $this->assertEquals( - 1, - $segmentContacts[$segmentTomorrowRef->getId()]['count'], - 'There should be 1 contacts in the segment-with-relative-date-tomorrow segment.' - ); - $this->assertEquals( - $lead->getId(), - $segmentContacts[$segmentTomorrowRef->getId()]['maxId'], - 'MaxId in the segment-with-relative-date-tomorrow segment should be ID of Lead.' - ); + $this->checkSegmentResult($name, $lead); } public function testSegmentCountIsCorrectForLastWeek() { + $name = 'Last week'; $lead = $this->createLead('Last week', 'midnight monday last week', '+2 days'); + $this->checkSegmentResult($name, $lead); + } + + /** + * @param string $name + * @param Lead $lead + */ + private function checkSegmentResult($name, Lead $lead) + { /** @var ContactSegmentService $contactSegmentService */ $contactSegmentService = $this->container->get('mautic.lead.model.lead_segment_service'); - $segmentLastWeekRef = $this->fixtures->getReference('segment-with-relative-date-last-week'); - $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentLastWeekRef); + $alias = strtolower(InputHelper::alphanum($name, false, '-')); + + $segmentName = 'segment-with-relative-date-'.$alias; + $segmentRef = $this->fixtures->getReference($segmentName); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentRef); + + $this->removeAllDateRelatedLeads(); //call before assert to be sure cleaning will process $this->assertEquals( 1, - $segmentContacts[$segmentLastWeekRef->getId()]['count'], - 'There should be 1 contacts in the segment-with-relative-date-last-week segment.' + $segmentContacts[$segmentRef->getId()]['count'], + 'There should be 1 contacts in the '.$segmentName.' segment.' ); $this->assertEquals( $lead->getId(), - $segmentContacts[$segmentLastWeekRef->getId()]['maxId'], - 'MaxId in the segment-with-relative-date-last-week segment should be ID of Lead.' + $segmentContacts[$segmentRef->getId()]['maxId'], + 'MaxId in the '.$segmentName.' segment should be ID of Lead.' ); } @@ -65,8 +75,7 @@ public function testSegmentCountIsCorrectForLastWeek() */ private function createLead($name, $initialTime, $dateModifier) { - // Remove the title from all contacts, rebuild the list, and check that list is updated - $this->em->getConnection()->query(sprintf("DELETE FROM %sleads WHERE lastname = 'Date';", MAUTIC_TABLE_PREFIX)); + $this->removeAllDateRelatedLeads(); /** @var LeadRepository $leadRepository */ $leadRepository = $this->container->get('doctrine.orm.default_entity_manager')->getRepository(Lead::class); @@ -83,4 +92,10 @@ private function createLead($name, $initialTime, $dateModifier) return $lead; } + + private function removeAllDateRelatedLeads() + { + // Remove all date related leads to not affect other test + $this->em->getConnection()->query(sprintf("DELETE FROM %sleads WHERE lastname = 'Date';", MAUTIC_TABLE_PREFIX)); + } } From bdefef5ae1715aaa8a4f3507ebaa99540348f2a1 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 16:23:24 +0100 Subject: [PATCH 146/778] Tests for date based on day / week / month / year --- .../DataFixtures/ORM/LoadLeadDateData.php | 133 ----------- .../DataFixtures/ORM/LoadSegmentsData.php | 210 +++++++++++++++++- .../Date/RelativeDateFunctionalTest.php | 70 +++++- 3 files changed, 276 insertions(+), 137 deletions(-) delete mode 100644 app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php deleted file mode 100644 index d5b447a7ca2..00000000000 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadLeadDateData.php +++ /dev/null @@ -1,133 +0,0 @@ -container = $container; - } - - /** - * @param ObjectManager $manager - */ - public function load(ObjectManager $manager) - { - /** @var LeadRepository $leadRepository */ - $leadRepository = $this->container->get('doctrine.orm.default_entity_manager')->getRepository(Lead::class); - - $data = [ - [ - 'name' => 'Today', - 'initialTime' => 'midnight today', - 'dateModifier' => '+10 seconds', - ], - [ - 'name' => 'Tomorrow', - 'initialTime' => 'midnight tomorrow', - 'dateModifier' => '+10 seconds', - ], - [ - 'name' => 'Yesterday', - 'initialTime' => 'midnight today', - 'dateModifier' => '-10 seconds', - ], - [ - 'name' => 'Last week', - 'initialTime' => 'midnight monday last week', - 'dateModifier' => '+2 days', - ], - [ - 'name' => 'Next week', - 'initialTime' => 'midnight monday next week', - 'dateModifier' => '+2 days', - ], - [ - 'name' => 'This week', - 'initialTime' => 'midnight monday this week', - 'dateModifier' => '+2 days', - ], - [ - 'name' => 'Last month', - 'initialTime' => 'midnight first day of last month', - 'dateModifier' => '+2 days', - ], - [ - 'name' => 'Next month', - 'initialTime' => 'midnight first day of next month', - 'dateModifier' => '+2 days', - ], - [ - 'name' => 'This month', - 'initialTime' => 'midnight first day of this month', - 'dateModifier' => '+2 days', - ], - ]; - $data = []; - - foreach ($data as $lead) { - $this->createLead($leadRepository, $lead['name'], $lead['initialTime'], $lead['dateModifier']); - } - } - - /** - * @return int - */ - public function getOrder() - { - return 6; - } - - /** - * @param LeadRepository $leadRepository - * @param string $name - * @param string $initialTime - * @param string $dateModifier - */ - private function createLead(LeadRepository $leadRepository, $name, $initialTime, $dateModifier) - { - $date = new \DateTime($initialTime); - $date->modify($dateModifier); - - $lead = new Lead(); - $lead->setLastname('Date'); - $lead->setFirstname($name); - $lead->setDateIdentified($date); - - $leadRepository->saveEntity($lead); - - $alias = strtolower(InputHelper::alphanum($name, false, '-')); - - $this->setReference('lead-date-'.$alias, $lead); - } -} diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index 38660be1457..f1227fa4242 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -538,7 +538,33 @@ public function load(ObjectManager $manager) ], 'populate' => false, ], - [ // ID xx + [ // ID 28 + 'name' => 'Segment with relative date - yesterday', + 'alias' => 'segment-with-relative-date-yesterday', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'yesterday', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 29 'name' => 'Segment with relative date - last week', 'alias' => 'segment-with-relative-date-last-week', 'public' => true, @@ -564,6 +590,188 @@ public function load(ObjectManager $manager) ], 'populate' => false, ], + [ // ID 30 + 'name' => 'Segment with relative date - next week', + 'alias' => 'segment-with-relative-date-next-week', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'next week', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 31 + 'name' => 'Segment with relative date - this week', + 'alias' => 'segment-with-relative-date-this-week', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'this week', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 32 + 'name' => 'Segment with relative date - last month', + 'alias' => 'segment-with-relative-date-last-month', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'last month', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 33 + 'name' => 'Segment with relative date - next month', + 'alias' => 'segment-with-relative-date-next-month', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'next month', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 34 + 'name' => 'Segment with relative date - this month', + 'alias' => 'segment-with-relative-date-this-month', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'this month', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 35 + 'name' => 'Segment with relative date - last year', + 'alias' => 'segment-with-relative-date-last-year', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'last year', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 36 + 'name' => 'Segment with relative date - next year', + 'alias' => 'segment-with-relative-date-next-year', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => 'next year', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], ]; foreach ($segments as $segmentConfig) { diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php index cca15167c2a..d6f445ce81a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php @@ -24,15 +24,79 @@ public function testSegmentCountIsCorrectForToday() public function testSegmentCountIsCorrectForTomorrow() { $name = 'Tomorrow'; - $lead = $this->createLead('Tomorrow', 'midnight tomorrow', '+10 seconds'); + $lead = $this->createLead($name, 'midnight tomorrow', '+10 seconds'); $this->checkSegmentResult($name, $lead); } - public function testSegmentCountIsCorrectForLastWeek() + public function testSegmentCountIsCorrectForYesterday() + { + $name = 'Yesterday'; + $lead = $this->createLead($name, 'midnight today', '-10 seconds'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForWeekLast() { $name = 'Last week'; - $lead = $this->createLead('Last week', 'midnight monday last week', '+2 days'); + $lead = $this->createLead($name, 'midnight monday last week', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForWeekNext() + { + $name = 'Next week'; + $lead = $this->createLead($name, 'midnight monday next week', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForWeekThis() + { + $name = 'This week'; + $lead = $this->createLead($name, 'midnight monday this week', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForMonthLast() + { + $name = 'Last month'; + $lead = $this->createLead($name, 'midnight first day of last month', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForMonthNext() + { + $name = 'Next month'; + $lead = $this->createLead($name, 'midnight first day of next month', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForMonthThis() + { + $name = 'This month'; + $lead = $this->createLead($name, 'midnight first day of this month', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForYearLast() + { + $name = 'Last year'; + $lead = $this->createLead($name, 'midnight first day of last year', '+2 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForYearNext() + { + $name = 'Next year'; + $lead = $this->createLead($name, 'midnight first day of next year', '+2 days'); $this->checkSegmentResult($name, $lead); } From 90259b1bb9b8c2c7e899c9f01a10567478d3905d Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 16:54:13 +0100 Subject: [PATCH 147/778] Tests fix - Count for ContactSegmentService --- .../Tests/Segment/ContactSegmentServiceFunctionalTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php index 56fa764d27a..f8e89fb8bbf 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php @@ -51,9 +51,9 @@ public function testSegmentCountIsCorrect() $segmentTest5Ref = $this->fixtures->getReference('segment-test-5'); $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentTest5Ref); $this->assertEquals( - 54, + 53, $segmentContacts[$segmentTest5Ref->getId()]['count'], - 'There should be 54 contacts in the segment-test-5 segment.' + 'There should be 53 contacts in the segment-test-5 segment.' ); $likePercentEndRef = $this->fixtures->getReference('like-percent-end'); From 2f4352fd9664d15c424cc253933a63c9acd0c88a Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 28 Feb 2018 16:54:38 +0100 Subject: [PATCH 148/778] Tests for relative dates - plus / minus exact days --- .../DataFixtures/ORM/LoadSegmentsData.php | 52 +++++++++++++++++++ .../Date/RelativeDateFunctionalTest.php | 16 ++++++ 2 files changed, 68 insertions(+) diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index f1227fa4242..04d727c8fd1 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -772,6 +772,58 @@ public function load(ObjectManager $manager) ], 'populate' => false, ], + [ // ID 37 + 'name' => 'Segment with relative date - relative plus', + 'alias' => 'segment-with-relative-date-relative-plus', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => '+5 days', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], + [ // ID 38 + 'name' => 'Segment with relative date - relative minus', + 'alias' => 'segment-with-relative-date-relative-minus', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => '-4 days', + 'display' => null, + ], + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'lastname', + 'operator' => '=', + 'filter' => 'Date', + 'display' => null, + ], + ], + 'populate' => false, + ], ]; foreach ($segments as $segmentConfig) { diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php index d6f445ce81a..50df38a9512 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/RelativeDateFunctionalTest.php @@ -101,6 +101,22 @@ public function testSegmentCountIsCorrectForYearNext() $this->checkSegmentResult($name, $lead); } + public function testSegmentCountIsCorrectForRelativePlus() + { + $name = 'Relative plus'; + $lead = $this->createLead($name, 'now', '+5 days'); + + $this->checkSegmentResult($name, $lead); + } + + public function testSegmentCountIsCorrectForRelativeMinus() + { + $name = 'Relative minus'; + $lead = $this->createLead($name, 'now', '-4 days'); + + $this->checkSegmentResult($name, $lead); + } + /** * @param string $name * @param Lead $lead From 3338313f202a0c890cc5259be96e4e5a5dda0647 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 1 Mar 2018 14:49:38 +0100 Subject: [PATCH 149/778] Fix phpStan error --- app/bundles/LeadBundle/Model/ListModel.php | 249 ------------------ .../Query/ContactSegmentQueryBuilder.php | 24 -- .../LeadBundle/Segment/Query/QueryBuilder.php | 5 +- 3 files changed, 4 insertions(+), 274 deletions(-) diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index d4df4bf3ff9..5cf4f094bda 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -1015,255 +1015,6 @@ public function rebuildListLeads() throw new \Exception('Deprecated function, use updateLeadList instead'); } - /** - * Rebuild lead lists. - * - * @param LeadList $leadList - * @param int $limit - * @param bool $maxLeads - * @param OutputInterface $output - * - * @throws \Exception - * - * @return int - */ - public function rebuildListLeadsOld(LeadList $leadList, $limit = 1000, $maxLeads = false, OutputInterface $output = null) - { - defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1); - - $id = $leadList->getId(); - $list = ['id' => $id, 'filters' => $leadList->getFilters()]; - $dtHelper = new DateTimeHelper(); - $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); - - $batchLimiters = [ - 'dateTime' => $dtHelper->toUtcString(), - ]; - - $localDateTime = $dtHelper->getLocalDateTime(); - - $this->dispatcher->dispatch( - LeadEvents::LIST_PRE_PROCESS_LIST, - $list, false - ); - - // Get a count of leads to add - $newLeadsCount = $this->leadSegmentService->getNewLeadsByListCount($leadList, $batchLimiters); - - dump($newLeadsCount); - echo '
Original result:'; - - $versionStart = microtime(true); - - // Get a count of leads to add - $newLeadsCount = $this->getLeadsByList( - $list, - true, - [ - 'countOnly' => true, - 'newOnly' => true, - 'batchLimiters' => $batchLimiters, - ] - ); - $versionEnd = microtime(true) - $versionStart; - dump('Total query assembly took:'.(1000 * $versionEnd).'ms'); - - dump(array_shift($newLeadsCount)); - exit; - // Ensure the same list is used each batch - $batchLimiters['maxId'] = (int) $newLeadsCount[$id]['maxId']; - - // Number of total leads to process - $leadCount = (int) $newLeadsCount[$id]['count']; - - if ($output) { - $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_added', ['%leads%' => $leadCount, '%batch%' => $limit])); - } - - // Handle by batches - $start = $lastRoundPercentage = $leadsProcessed = 0; - - // Try to save some memory - gc_enable(); - - if ($leadCount) { - $maxCount = ($maxLeads) ? $maxLeads : $leadCount; - - if ($output) { - $progress = ProgressBarHelper::init($output, $maxCount); - $progress->start(); - } - - // Add leads - while ($start < $leadCount) { - // Keep CPU down for large lists; sleep per $limit batch - $this->batchSleep(); - - $newLeadList = $this->getLeadsByList( - $list, - true, - [ - 'newOnly' => true, - // No start set because of newOnly thus always at 0 - 'limit' => $limit, - 'batchLimiters' => $batchLimiters, - ] - ); - - if (empty($newLeadList[$id])) { - // Somehow ran out of leads so break out - break; - } - - $processedLeads = []; - foreach ($newLeadList[$id] as $l) { - $this->addLead($l, $leadList, false, true, -1, $localDateTime); - $processedLeads[] = $l; - unset($l); - - ++$leadsProcessed; - if ($output && $leadsProcessed < $maxCount) { - $progress->setProgress($leadsProcessed); - } - - if ($maxLeads && $leadsProcessed >= $maxLeads) { - break; - } - } - - $start += $limit; - - // Dispatch batch event - if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { - $this->dispatcher->dispatch( - LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $leadList, true) - ); - } - - unset($newLeadList); - - // Free some memory - gc_collect_cycles(); - - if ($maxLeads && $leadsProcessed >= $maxLeads) { - if ($output) { - $progress->finish(); - $output->writeln(''); - } - - return $leadsProcessed; - } - } - - if ($output) { - $progress->finish(); - $output->writeln(''); - } - } - - // Unset max ID to prevent capping at newly added max ID - unset($batchLimiters['maxId']); - - // Get a count of leads to be removed - $removeLeadCount = $this->getLeadsByList( - $list, - true, - [ - 'countOnly' => true, - 'nonMembersOnly' => true, - 'batchLimiters' => $batchLimiters, - ] - ); - - // Ensure the same list is used each batch - $batchLimiters['maxId'] = (int) $removeLeadCount[$id]['maxId']; - - // Restart batching - $start = $lastRoundPercentage = 0; - $leadCount = $removeLeadCount[$id]['count']; - - if ($output) { - $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_removed', ['%leads%' => $leadCount, '%batch%' => $limit])); - } - - if ($leadCount) { - $maxCount = ($maxLeads) ? $maxLeads : $leadCount; - - if ($output) { - $progress = ProgressBarHelper::init($output, $maxCount); - $progress->start(); - } - - // Remove leads - while ($start < $leadCount) { - // Keep CPU down for large lists; sleep per $limit batch - $this->batchSleep(); - - $removeLeadList = $this->getLeadsByList( - $list, - true, - [ - // No start because the items are deleted so always 0 - 'limit' => $limit, - 'nonMembersOnly' => true, - 'batchLimiters' => $batchLimiters, - ] - ); - - if (empty($removeLeadList[$id])) { - // Somehow ran out of leads so break out - break; - } - - $processedLeads = []; - foreach ($removeLeadList[$id] as $l) { - $this->removeLead($l, $leadList, false, true, true); - $processedLeads[] = $l; - ++$leadsProcessed; - if ($output && $leadsProcessed < $maxCount) { - $progress->setProgress($leadsProcessed); - } - - if ($maxLeads && $leadsProcessed >= $maxLeads) { - break; - } - } - - // Dispatch batch event - if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) { - $this->dispatcher->dispatch( - LeadEvents::LEAD_LIST_BATCH_CHANGE, - new ListChangeEvent($processedLeads, $leadList, false) - ); - } - - $start += $limit; - - unset($removeLeadList); - - // Free some memory - gc_collect_cycles(); - - if ($maxLeads && $leadsProcessed >= $maxLeads) { - if ($output) { - $progress->finish(); - $output->writeln(''); - } - - return $leadsProcessed; - } - } - - if ($output) { - $progress->finish(); - $output->writeln(''); - } - } - - return $leadsProcessed; - } - /** * Add lead to lists. * diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 744f9aeddf9..882b75c8da0 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -232,30 +232,6 @@ private function generateRandomParameterName() return $this->randomParameterName->generateRandomParameterName(); } - /** - * @return LeadSegmentFilterDescriptor - * - * @TODO Remove this function - */ - public function getTranslator() - { - return $this->translator; - } - - /** - * @param LeadSegmentFilterDescriptor $translator - * - * @return ContactSegmentQueryBuilder - * - * @TODO Remove this function - */ - public function setTranslator($translator) - { - $this->translator = $translator; - - return $this; - } - /** * @return \Doctrine\DBAL\Schema\AbstractSchemaManager * diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 06a72e23da6..32fbb53a75e 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1411,11 +1411,14 @@ public function getJoinCondition($alias) /** * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts + * @TODO Throwing \Exception - replace it with any specific one? Functions calling this method does not handle exception, is it neccessary? * * @param $alias * @param $expr * * @return $this + * + * @throws \Exception */ public function addJoinCondition($alias, $expr) { @@ -1431,7 +1434,7 @@ public function addJoinCondition($alias, $expr) } if (!isset($inserted)) { - throw new QueryBuilderException('Inserting condition to nonexistent join '.$alias); + throw new \Exception('Inserting condition to nonexistent join '.$alias); } $this->setQueryPart('join', $result); From 8b3bca0ba3310c3e465569523424ec939e9c9b88 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 1 Mar 2018 15:52:01 +0100 Subject: [PATCH 150/778] Fix PhpStan error - correct Exception --- .../LeadBundle/Segment/Query/QueryBuilder.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 32fbb53a75e..a50ecfd9726 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1411,14 +1411,14 @@ public function getJoinCondition($alias) /** * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts - * @TODO Throwing \Exception - replace it with any specific one? Functions calling this method does not handle exception, is it neccessary? + * @TODO Throwing QueryException - Functions calling this method do not handle exception, is it neccessary? * * @param $alias * @param $expr * * @return $this * - * @throws \Exception + * @throws QueryException */ public function addJoinCondition($alias, $expr) { @@ -1434,7 +1434,7 @@ public function addJoinCondition($alias, $expr) } if (!isset($inserted)) { - throw new \Exception('Inserting condition to nonexistent join '.$alias); + throw new QueryException('Inserting condition to nonexistent join '.$alias); } $this->setQueryPart('join', $result); @@ -1444,11 +1444,15 @@ public function addJoinCondition($alias, $expr) /** * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts + * @TODO This function seems not used at all + * @TODO Throwing QueryException - Functions calling this method do not handle exception, is it neccessary? * * @param $alias * @param $expr * * @return $this + * + * @throws QueryException */ public function addOrJoinCondition($alias, $expr) { @@ -1464,7 +1468,7 @@ public function addOrJoinCondition($alias, $expr) } if (!isset($inserted)) { - throw new QueryBuilderException('Inserting condition to nonexistent join '.$alias); + throw new QueryException('Inserting condition to nonexistent join '.$alias); } $this->setQueryPart('join', $result); From 4e2a5897afc91f69651afad7950426db9f8e96b1 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 1 Mar 2018 16:37:21 +0100 Subject: [PATCH 151/778] CS fixer errors fix --- app/bundles/CoreBundle/Config/config.php | 4 ++-- app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php | 2 +- .../LeadBundle/Segment/ContactSegmentFilterOperator.php | 4 +--- app/bundles/LeadBundle/Segment/ContactSegmentFilters.php | 5 ++--- .../LeadBundle/Services/ContactSegmentFilterDictionary.php | 3 +-- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/bundles/CoreBundle/Config/config.php b/app/bundles/CoreBundle/Config/config.php index 61841c9a95f..38713be7edd 100644 --- a/app/bundles/CoreBundle/Config/config.php +++ b/app/bundles/CoreBundle/Config/config.php @@ -606,8 +606,8 @@ 'class' => \Mautic\CoreBundle\Security\Cryptography\Cipher\Symmetric\McryptCipher::class, ], 'mautic.cipher.openssl' => [ - 'class' => \Mautic\CoreBundle\Security\Cryptography\Cipher\Symmetric\OpenSSLCipher::class, - 'arguments' => ["%kernel.environment%"] + 'class' => \Mautic\CoreBundle\Security\Cryptography\Cipher\Symmetric\OpenSSLCipher::class, + 'arguments' => ['%kernel.environment%'], ], 'mautic.factory' => [ 'class' => 'Mautic\CoreBundle\Factory\MauticFactory', diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 11cd5350f0b..6e2e6459e41 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -14,7 +14,7 @@ class ContactSegmentFilterCrate { const CONTACT_OBJECT = 'lead'; - const COMPANY_OBJECT = 'company'; + const COMPANY_OBJECT = 'company'; /** * @var string|null diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php index 1c60bcdcc03..3115e30c297 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php @@ -17,9 +17,7 @@ use Symfony\Component\Translation\TranslatorInterface; /** - * Class ContactSegmentFilterOperator - * - * @package Mautic\LeadBundle\Segment + * Class ContactSegmentFilterOperator. */ class ContactSegmentFilterOperator { diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php index 0c43dced6dd..4aafea6ab4a 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php @@ -12,9 +12,7 @@ namespace Mautic\LeadBundle\Segment; /** - * Class ContactSegmentFilters is array object containing filters - * - * @package Mautic\LeadBundle\Segment + * Class ContactSegmentFilters is array object containing filters. */ class ContactSegmentFilters implements \Iterator, \Countable { @@ -46,6 +44,7 @@ class ContactSegmentFilters implements \Iterator, \Countable public function addContactSegmentFilter(ContactSegmentFilter $contactSegmentFilter) { $this->contactSegmentFilters[] = $contactSegmentFilter; + return $this; } diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index 7cadabfc79b..9f0c08d38ab 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -19,9 +19,8 @@ use Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder; /** - * Class ContactSegmentFilterDictionary + * Class ContactSegmentFilterDictionary. * - * @package Mautic\LeadBundle\Services * @todo @petr Já jsem to myslím předělával už. Chtěl jsem z toho pak udělat i objekt, aby se člověk nemusel ptát na klíče v poli, ale pak jsme na to nesahali, protože to nebylo komplet */ class ContactSegmentFilterDictionary extends \ArrayIterator From 06f6d092ab8dd3890c154376ba69e9af9e0fc98e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 2 Mar 2018 09:25:32 +0100 Subject: [PATCH 152/778] Unit test for DateOptionFactory --- .../Decorator/Date/DateOptionFactoryTest.php | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php new file mode 100644 index 00000000000..d5507db5d53 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php @@ -0,0 +1,279 @@ +getFilterDecorator($filterName); + + $this->assertInstanceOf(DateAnniversary::class, $filterDecorator); + + $filterName = 'birthday'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateAnniversary::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testDayToday() + { + $filterName = 'today'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateDayToday::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testDayTomorrow() + { + $filterName = 'tomorrow'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateDayTomorrow::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testDayYesterday() + { + $filterName = 'yesterday'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateDayYesterday::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testWeekLast() + { + $filterName = 'last week'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateWeekLast::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testWeekNext() + { + $filterName = 'next week'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateWeekNext::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testWeekThis() + { + $filterName = 'this week'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateWeekThis::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testMonthLast() + { + $filterName = 'last month'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateMonthLast::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testMonthNext() + { + $filterName = 'next month'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateMonthNext::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testMonthThis() + { + $filterName = 'this month'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateMonthThis::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testYearLast() + { + $filterName = 'last year'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateYearLast::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testYearNext() + { + $filterName = 'next year'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateYearNext::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testYearThis() + { + $filterName = 'this year'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateYearThis::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testRelativePlus() + { + $filterName = '+20 days'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateRelativeInterval::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testRelativeMinus() + { + $filterName = '+20 days'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateRelativeInterval::class, $filterDecorator); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testDateDefault() + { + $filterName = '2018-01-01'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateDefault::class, $filterDecorator); + } + + /** + * @param string $filterName + * + * @return \Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface + */ + private function getFilterDecorator($filterName) + { + $dateDecorator = $this->createMock(DateDecorator::class); + $relativeDate = $this->createMock(RelativeDate::class); + + $relativeDate->method('getRelativeDateStrings') + ->willReturn( + [ + 'mautic.lead.list.month_last' => 'last month', + 'mautic.lead.list.month_next' => 'next month', + 'mautic.lead.list.month_this' => 'this month', + 'mautic.lead.list.today' => 'today', + 'mautic.lead.list.tomorrow' => 'tomorrow', + 'mautic.lead.list.yesterday' => 'yesterday', + 'mautic.lead.list.week_last' => 'last week', + 'mautic.lead.list.week_next' => 'next week', + 'mautic.lead.list.week_this' => 'this week', + 'mautic.lead.list.year_last' => 'last year', + 'mautic.lead.list.year_next' => 'next year', + 'mautic.lead.list.year_this' => 'this year', + 'mautic.lead.list.birthday' => 'birthday', + 'mautic.lead.list.anniversary' => 'anniversary', + ] + ); + + $dateOptionFactory = new DateOptionFactory($dateDecorator, $relativeDate); + + $filter = [ + 'glue' => 'and', + 'type' => 'datetime', + 'object' => 'lead', + 'field' => 'date_identified', + 'operator' => '=', + 'filter' => $filterName, + 'display' => null, + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + return $dateOptionFactory->getDateOption($contactSegmentFilterCrate); + } +} From 0656d4f687781e3116bdd4810a0dfaabf35d1c09 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 2 Mar 2018 15:59:24 +0100 Subject: [PATCH 153/778] Date refactoring - get rid of dependance on system Date - replace by function in DateDecorator --- .../Segment/Decorator/Date/DateOptionAbstract.php | 2 +- .../Segment/Decorator/Date/Other/DateAnniversary.php | 4 ++++ .../LeadBundle/Segment/Decorator/Date/Other/DateDefault.php | 4 ++-- .../Segment/Decorator/Date/Other/DateRelativeInterval.php | 4 ++-- app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php | 6 ++++++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index edab2628056..3d53dbe0920 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -99,7 +99,7 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $dateTimeHelper = new DateTimeHelper('midnight today', null, 'local'); + $dateTimeHelper = $this->dateDecorator->getDefaultDate(); $this->modifyBaseDate($dateTimeHelper); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index f79341786d1..5b88cafde07 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -49,6 +49,10 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { + $dateTimeHelper = $this->dateDecorator->getDefaultDate(); + + return $dateTimeHelper->toUtcString('%-m-d'); + return '%'.date('-m-d'); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php index 5fb866c71a8..12044e70d72 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php @@ -33,8 +33,8 @@ class DateDefault implements FilterDecoratorInterface */ public function __construct(DateDecorator $dateDecorator, $originalValue) { - $this->dateDecorator = $dateDecorator; - $this->originalValue = $originalValue; + $this->dateDecorator = $dateDecorator; + $this->originalValue = $originalValue; } public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php index f4264979a19..9a8a69d9f29 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php @@ -66,7 +66,7 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - $date = new \DateTime('now'); + $date = $this->dateDecorator->getDefaultDate(); $date->modify($this->originalValue); $operator = $this->getOperator($contactSegmentFilterCrate); @@ -75,7 +75,7 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte $format .= '%'; } - return $date->format($format); + return $date->toUtcString($format); } public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index 28fe533ebb2..93e7e31be8c 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Decorator; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; /** @@ -29,4 +30,9 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte { throw new \Exception('Instance of Date option need to implement this function'); } + + public function getDefaultDate() + { + return new DateTimeHelper('midnight today', null, 'local'); + } } From 2167e925c3e1ba5545b3ea72b1b277061469fe95 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 2 Mar 2018 16:01:01 +0100 Subject: [PATCH 154/778] Unit tests for date filters - Day + other --- .../Decorator/Date/DateOptionFactoryTest.php | 2 +- .../Decorator/Date/Day/DateDayTodayTest.php | 112 +++++++++++++++++ .../Date/Day/DateDayTomorrowTest.php | 112 +++++++++++++++++ .../Date/Day/DateDayYesterdayTest.php | 112 +++++++++++++++++ .../Date/Other/DateAnniversaryTest.php | 53 ++++++++ .../Decorator/Date/Other/DateDefaultTest.php | 32 +++++ .../Date/Other/DateRelativeIntervalTest.php | 119 ++++++++++++++++++ 7 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateDefaultTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php index d5507db5d53..fbdbfcecb07 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Tests\Model; +namespace Mautic\LeadBundle\Tests\Segment\Decorator\Date; use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; use Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory; diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php new file mode 100644 index 00000000000..4bd59f697b5 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-02%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayToday::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-02', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php new file mode 100644 index 00000000000..d28db3ae0aa --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-03%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayTomorrow::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-03', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php new file mode 100644 index 00000000000..84ba00957b2 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-01%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Day\DateDayYesterday::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php new file mode 100644 index 00000000000..00df452c1c1 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateAnniversaryTest.php @@ -0,0 +1,53 @@ +createMock(DateDecorator::class); + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateAnniversary($dateDecorator); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateAnniversary::getParameterValue + */ + public function testGetParameterValue() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateAnniversary($dateDecorator); + + $this->assertEquals('%-03-02', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateDefaultTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateDefaultTest.php new file mode 100644 index 00000000000..78fc900762d --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateDefaultTest.php @@ -0,0 +1,32 @@ +createMock(DateDecorator::class); + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateDefault($dateDecorator, '2018-03-02 01:02:03'); + + $this->assertEquals('2018-03-02 01:02:03', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php new file mode 100644 index 00000000000..971f7d604f4 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php @@ -0,0 +1,119 @@ +createMock(DateDecorator::class); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getOperator + */ + public function testGetOperatorNotEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + + $this->assertEquals('notLike', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getParameterValue + */ + public function testGetParameterValuePlusDaysWithGreaterOperator() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '>', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '+5 days'); + + $this->assertEquals('2018-03-07', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getParameterValue + */ + public function testGetParameterValueMinusMonthDaysWithNotEqualOperator() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '-3 months'); + + $this->assertEquals('2017-12-02%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} From 6a6ec34a55feafc4a274e14b639a3154eeb1e4e4 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 2 Mar 2018 16:46:23 +0100 Subject: [PATCH 155/778] Unit tests for date filters - week + month --- .../Date/Month/DateMonthLastTest.php | 112 +++++++++++++++++ .../Date/Month/DateMonthNextTest.php | 112 +++++++++++++++++ .../Date/Month/DateMonthThisTest.php | 112 +++++++++++++++++ .../Decorator/Date/Week/DateWeekLastTest.php | 115 ++++++++++++++++++ .../Decorator/Date/Week/DateWeekNextTest.php | 115 ++++++++++++++++++ .../Decorator/Date/Week/DateWeekThisTest.php | 115 ++++++++++++++++++ 6 files changed, 681 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php new file mode 100644 index 00000000000..21ed08015a2 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthLast::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthLast::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-02-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthLast::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-02-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php new file mode 100644 index 00000000000..a309db5cbcc --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthNext::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthNext::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-04-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthNext::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-04-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php new file mode 100644 index 00000000000..0943776c91c --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Month\DateMonthThis::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php new file mode 100644 index 00000000000..b62183b4431 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php @@ -0,0 +1,115 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals(['2018-02-19', '2018-02-25'], $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-02-19', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php new file mode 100644 index 00000000000..57e24f6eea6 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php @@ -0,0 +1,115 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals(['2018-03-05', '2018-03-11'], $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-03-05', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php new file mode 100644 index 00000000000..e1d7b85f219 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php @@ -0,0 +1,115 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals(['2018-02-26', '2018-03-04'], $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-02-26', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} From abe533cef9604b3a6b8a84b28c16bbe05379fca4 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 5 Mar 2018 11:26:47 +0100 Subject: [PATCH 156/778] Unit tests for date filters - year --- .../Decorator/Date/Year/DateYearLast.php | 2 +- .../Decorator/Date/Year/DateYearNext.php | 2 +- .../Decorator/Date/Year/DateYearThis.php | 2 +- .../Decorator/Date/Year/DateYearLastTest.php | 112 ++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php index f3ad6b3c227..ed9351cb593 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearLast.php @@ -20,6 +20,6 @@ class DateYearLast extends DateYearAbstract */ protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $dateTimeHelper->setDateTime('midnight first day of last year', null); + $dateTimeHelper->setDateTime('midnight first day of January last year', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php index 5db52997233..4bbe87ae230 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearNext.php @@ -20,6 +20,6 @@ class DateYearNext extends DateYearAbstract */ protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $dateTimeHelper->setDateTime('midnight first day of next year', null); + $dateTimeHelper->setDateTime('midnight first day of January next year', null); } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php index a10c22b6a33..8b8be389a94 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Year/DateYearThis.php @@ -20,6 +20,6 @@ class DateYearThis extends DateYearAbstract */ protected function modifyBaseDate(DateTimeHelper $dateTimeHelper) { - $dateTimeHelper->setDateTime('midnight first day of this year', null); + $dateTimeHelper->setDateTime('midnight first day of January this year', null); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php new file mode 100644 index 00000000000..ceb1d251d1b --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearLast::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearLast::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2017-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearLast::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2017-01-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} From d72de9c43017bc1ce8bbf9c81b0c5dc044732a83 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 5 Mar 2018 11:30:39 +0100 Subject: [PATCH 157/778] Unit tests for date filters - year --- .../Decorator/Date/Year/DateYearNextTest.php | 112 ++++++++++++++++++ .../Decorator/Date/Year/DateYearThisTest.php | 112 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php new file mode 100644 index 00000000000..76627a19863 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearNext::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearNext::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2019-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearNext::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2019-01-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php new file mode 100644 index 00000000000..70d9054c9b1 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php @@ -0,0 +1,112 @@ +createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('like', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis::getParameterValue + */ + public function testGetParameterValueBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Year\DateYearThis::getParameterValue + */ + public function testGetParameterValueSingle() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); + + $this->assertEquals('2018-01-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } +} From f4eab020446177e12df1949057afda97a3532f38 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 5 Mar 2018 12:34:44 +0100 Subject: [PATCH 158/778] Reset composer - composer changes are in different PR --- composer.json | 2 +- composer.lock | 156 +++++++++++++++----------------------------------- 2 files changed, 47 insertions(+), 111 deletions(-) diff --git a/composer.json b/composer.json index 163578fde36..c4ac6e25b26 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "doctrine/cache": "~1.5.4", "doctrine/migrations": "~1.2.2", "doctrine/orm": "~2.5.4", - "doctrine/doctrine-bundle": "1.6.*", + "doctrine/doctrine-bundle": "~1.6", "doctrine/doctrine-cache-bundle": "~1.3.0", "doctrine/doctrine-fixtures-bundle": "~2.3.0", "doctrine/doctrine-migrations-bundle": "~1.1.1", diff --git a/composer.lock b/composer.lock index 9fea11b0e6c..70a8d45c876 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "a47e0fb480e38aaf6c896172ccd1b236", + "content-hash": "81292be6ce780f1e60caa2b49b6dd5eb", "packages": [ { "name": "aws/aws-sdk-php", @@ -722,7 +722,7 @@ "orm", "persistence" ], - "time": "2017-10-11T19:48:03+00:00" + "time": "2017-10-04T22:58:25+00:00" }, { "name": "doctrine/doctrine-cache-bundle", @@ -810,7 +810,7 @@ "cache", "caching" ], - "time": "2017-10-12T17:23:29+00:00" + "time": "2017-09-29T14:39:10+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", @@ -1244,7 +1244,7 @@ "database", "orm" ], - "time": "2017-10-23T18:21:04+00:00" + "time": "2017-09-18T06:50:20+00:00" }, { "name": "egeloen/ordered-form", @@ -1615,7 +1615,7 @@ "geolocation", "maxmind" ], - "time": "2017-10-27T19:20:22+00:00" + "time": "2017-07-10T17:59:43+00:00" }, { "name": "giggsey/libphonenumber-for-php", @@ -1683,7 +1683,7 @@ "phonenumber", "validation" ], - "time": "2017-10-16T14:06:44+00:00" + "time": "2017-10-04T10:08:33+00:00" }, { "name": "giggsey/locale", @@ -2364,7 +2364,7 @@ "serialization", "xml" ], - "time": "2017-10-27T07:15:54+00:00" + "time": "2017-09-28T15:17:28+00:00" }, { "name": "jms/serializer-bundle", @@ -3127,7 +3127,7 @@ "geolocation", "maxmind" ], - "time": "2017-10-27T19:15:33+00:00" + "time": "2017-01-19T23:49:38+00:00" }, { "name": "maxmind/web-service-common", @@ -3483,7 +3483,7 @@ "plupload", "upload" ], - "time": "2017-10-10T08:09:38+00:00" + "time": "2017-09-19T09:38:39+00:00" }, { "name": "paragonie/random_compat", @@ -4848,7 +4848,7 @@ } ], "description": "A security checker for your composer.lock", - "time": "2017-10-29T18:48:08+00:00" + "time": "2017-08-22T22:18:16+00:00" }, { "name": "simshaun/recurr", @@ -5281,7 +5281,7 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-12T12:59:33+00:00" }, { "name": "symfony/cache", @@ -5351,7 +5351,7 @@ "caching", "psr6" ], - "time": "2017-10-04T07:58:49+00:00" + "time": "2017-09-03T14:06:51+00:00" }, { "name": "symfony/class-loader", @@ -5404,7 +5404,7 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-05T06:50:35+00:00" }, { "name": "symfony/config", @@ -5460,7 +5460,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-10-04T18:56:36+00:00" + "time": "2017-04-12T14:07:15+00:00" }, { "name": "symfony/console", @@ -5521,7 +5521,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-08-27T14:29:03+00:00" }, { "name": "symfony/debug", @@ -5578,7 +5578,7 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-08-27T14:29:03+00:00" }, { "name": "symfony/dependency-injection", @@ -5641,7 +5641,7 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2017-10-02T07:17:52+00:00" + "time": "2017-08-10T14:42:21+00:00" }, { "name": "symfony/doctrine-bridge", @@ -5718,7 +5718,7 @@ ], "description": "Symfony Doctrine Bridge", "homepage": "https://symfony.com", - "time": "2017-10-02T08:46:46+00:00" + "time": "2017-07-22T16:46:29+00:00" }, { "name": "symfony/dom-crawler", @@ -5774,7 +5774,7 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-08-15T13:30:53+00:00" }, { "name": "symfony/dotenv", @@ -5891,7 +5891,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-06-02T07:47:27+00:00" }, { "name": "symfony/expression-language", @@ -5940,7 +5940,7 @@ ], "description": "Symfony ExpressionLanguage Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-06-01T20:52:29+00:00" }, { "name": "symfony/filesystem", @@ -5989,7 +5989,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-10-02T08:46:46+00:00" + "time": "2017-07-11T07:12:11+00:00" }, { "name": "symfony/finder", @@ -6038,7 +6038,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-06-01T20:52:29+00:00" }, { "name": "symfony/form", @@ -6113,7 +6113,7 @@ ], "description": "Symfony Form Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-28T15:21:22+00:00" }, { "name": "symfony/framework-bundle", @@ -6210,7 +6210,7 @@ ], "description": "Symfony FrameworkBundle", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-05T06:32:23+00:00" }, { "name": "symfony/http-foundation", @@ -6265,7 +6265,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-10-05T23:06:47+00:00" + "time": "2017-08-10T07:04:10+00:00" }, { "name": "symfony/http-kernel", @@ -6348,7 +6348,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-10-05T23:24:02+00:00" + "time": "2017-08-28T19:21:40+00:00" }, { "name": "symfony/intl", @@ -6424,7 +6424,7 @@ "l10n", "localization" ], - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-08-27T14:29:03+00:00" }, { "name": "symfony/monolog-bridge", @@ -6487,7 +6487,7 @@ ], "description": "Symfony Monolog Bridge", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-04-12T14:07:15+00:00" }, { "name": "symfony/monolog-bundle", @@ -6601,7 +6601,7 @@ "configuration", "options" ], - "time": "2017-09-11T20:39:16+00:00" + "time": "2017-04-12T14:07:15+00:00" }, { "name": "symfony/polyfill-apcu", @@ -6657,7 +6657,7 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "time": "2017-07-05T15:09:33+00:00" }, { "name": "symfony/polyfill-intl-icu", @@ -6715,7 +6715,7 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "time": "2017-06-14T15:44:48+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -6832,7 +6832,7 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "time": "2017-06-14T15:44:48+00:00" }, { "name": "symfony/polyfill-php55", @@ -6888,7 +6888,7 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "time": "2017-06-14T15:44:48+00:00" }, { "name": "symfony/polyfill-php56", @@ -7104,7 +7104,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-03T08:04:30+00:00" }, { "name": "symfony/property-access", @@ -7239,7 +7239,7 @@ "uri", "url" ], - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-06-20T23:27:56+00:00" }, { "name": "symfony/security", @@ -7451,71 +7451,7 @@ ], "description": "Symfony SecurityBundle", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" - }, - { - "name": "symfony/serializer", - "version": "v2.8.33", - "source": { - "type": "git", - "url": "https://github.com/symfony/serializer.git", - "reference": "989ed5bce3cc508b19d3dea6143ea61117957c8f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/989ed5bce3cc508b19d3dea6143ea61117957c8f", - "reference": "989ed5bce3cc508b19d3dea6143ea61117957c8f", - "shasum": "" - }, - "require": { - "php": ">=5.3.9", - "symfony/polyfill-php55": "~1.0" - }, - "require-dev": { - "doctrine/annotations": "~1.0", - "doctrine/cache": "~1.0", - "symfony/config": "~2.2|~3.0.0", - "symfony/property-access": "~2.3|~3.0.0", - "symfony/yaml": "^2.0.5|~3.0.0" - }, - "suggest": { - "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", - "doctrine/cache": "For using the default cached annotation reader and metadata cache.", - "symfony/config": "For using the XML mapping loader.", - "symfony/property-access": "For using the ObjectNormalizer.", - "symfony/yaml": "For using the default YAML mapping loader." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Serializer\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Serializer Component", - "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" + "time": "2017-07-11T07:12:11+00:00" }, { "name": "symfony/stopwatch", @@ -7623,7 +7559,7 @@ ], "description": "Symfony SwiftmailerBundle", "homepage": "http://symfony.com", - "time": "2017-10-19T01:06:41+00:00" + "time": "2017-07-22T07:18:13+00:00" }, { "name": "symfony/templating", @@ -7889,7 +7825,7 @@ ], "description": "Symfony TwigBundle", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-11T07:12:11+00:00" }, { "name": "symfony/validator", @@ -7962,7 +7898,7 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-08-27T14:29:03+00:00" }, { "name": "symfony/yaml", @@ -8011,7 +7947,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-10-05T14:38:30+00:00" + "time": "2017-06-01T20:52:29+00:00" }, { "name": "twig/twig", @@ -8259,7 +8195,7 @@ "oauth", "server" ], - "time": "2017-05-10T00:39:21+00:00" + "time": "2017-05-10 00:39:21" } ], "packages-dev": [ @@ -8832,7 +8768,7 @@ "object", "object graph" ], - "time": "2017-10-19T19:58:43+00:00" + "time": "2017-04-12T18:52:22+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -9427,7 +9363,7 @@ "testing", "xunit" ], - "time": "2017-10-15T06:13:55+00:00" + "time": "2017-09-24T07:23:38+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -10342,7 +10278,7 @@ "debug", "dump" ], - "time": "2017-10-02T06:42:24+00:00" + "time": "2017-08-27T14:52:21+00:00" }, { "name": "symfony/web-profiler-bundle", @@ -10401,7 +10337,7 @@ ], "description": "Symfony WebProfilerBundle", "homepage": "https://symfony.com", - "time": "2017-10-01T21:00:16+00:00" + "time": "2017-07-19T17:48:51+00:00" }, { "name": "webfactory/exceptions-bundle", From 0d30cbadf57f71bd440341a81d18c3e0c3fb4ef3 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 6 Mar 2018 12:00:35 +0100 Subject: [PATCH 159/778] Replace strings with constants in the Email entity --- app/bundles/EmailBundle/Entity/Email.php | 43 ++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index d97ae13b763..b1f45393f26 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -12,6 +12,7 @@ namespace Mautic\EmailBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Events; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; @@ -229,78 +230,78 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) $builder = new ClassMetadataBuilder($metadata); $builder->setTable('emails') - ->setCustomRepositoryClass('Mautic\EmailBundle\Entity\EmailRepository') + ->setCustomRepositoryClass(EmailRepository::class) ->addLifecycleEvent('cleanUrlsInContent', Events::preUpdate) ->addLifecycleEvent('cleanUrlsInContent', Events::prePersist); $builder->addIdColumns(); - $builder->createField('subject', 'text') + $builder->createField('subject', Type::TEXT) ->nullable() ->build(); - $builder->createField('fromAddress', 'string') + $builder->createField('fromAddress', Type::STRING) ->columnName('from_address') ->nullable() ->build(); - $builder->createField('fromName', 'string') + $builder->createField('fromName', Type::STRING) ->columnName('from_name') ->nullable() ->build(); - $builder->createField('replyToAddress', 'string') + $builder->createField('replyToAddress', Type::STRING) ->columnName('reply_to_address') ->nullable() ->build(); - $builder->createField('bccAddress', 'string') + $builder->createField('bccAddress', Type::STRING) ->columnName('bcc_address') ->nullable() ->build(); - $builder->createField('template', 'string') + $builder->createField('template', Type::STRING) ->nullable() ->build(); - $builder->createField('content', 'array') + $builder->createField('content', Type::ARRAY) ->nullable() ->build(); - $builder->createField('utmTags', 'array') + $builder->createField('utmTags', Type::ARRAY) ->columnName('utm_tags') ->nullable() ->build(); - $builder->createField('plainText', 'text') + $builder->createField('plainText', Type::TEXT) ->columnName('plain_text') ->nullable() ->build(); - $builder->createField('customHtml', 'text') + $builder->createField('customHtml', Type::TEXT) ->columnName('custom_html') ->nullable() ->build(); - $builder->createField('emailType', 'text') + $builder->createField('emailType', Type::TEXT) ->columnName('email_type') ->nullable() ->build(); $builder->addPublishDates(); - $builder->createField('readCount', 'integer') + $builder->createField('readCount', Type::INTEGER) ->columnName('read_count') ->build(); - $builder->createField('sentCount', 'integer') + $builder->createField('sentCount', Type::INTEGER) ->columnName('sent_count') ->build(); - $builder->addField('revision', 'integer'); + $builder->addField('revision', Type::INTEGER); $builder->addCategory(); - $builder->createManyToMany('lists', 'Mautic\LeadBundle\Entity\LeadList') + $builder->createManyToMany('lists', LeadList::class) ->setJoinTable('email_list_xref') ->setIndexBy('id') ->addInverseJoinColumn('leadlist_id', 'id', false, false, 'CASCADE') @@ -319,23 +320,23 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) self::addVariantMetadata($builder, self::class); self::addDynamicContentMetadata($builder); - $builder->createField('variantSentCount', 'integer') + $builder->createField('variantSentCount', Type::INTEGER) ->columnName('variant_sent_count') ->build(); - $builder->createField('variantReadCount', 'integer') + $builder->createField('variantReadCount', Type::INTEGER) ->columnName('variant_read_count') ->build(); - $builder->createManyToOne('unsubscribeForm', 'Mautic\FormBundle\Entity\Form') + $builder->createManyToOne('unsubscribeForm', Form::class) ->addJoinColumn('unsubscribeform_id', 'id', true, false, 'SET NULL') ->build(); - $builder->createManyToOne('preferenceCenter', 'Mautic\PageBundle\Entity\Page') + $builder->createManyToOne('preferenceCenter', Page::class) ->addJoinColumn('preference_center_id', 'id', true, false, 'SET NULL') ->build(); - $builder->createManyToMany('assetAttachments', 'Mautic\AssetBundle\Entity\Asset') + $builder->createManyToMany('assetAttachments', Asset::class) ->setJoinTable('email_assets_xref') ->addInverseJoinColumn('asset_id', 'id', false, false, 'CASCADE') ->addJoinColumn('email_id', 'id', false, false, 'CASCADE') From ba87a73b6f81ac300fbb3e4d024a4d19da5dbc47 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 6 Mar 2018 12:10:48 +0100 Subject: [PATCH 160/778] Use shorter syntax to define Email's SQL columns --- app/bundles/EmailBundle/Entity/Email.php | 85 +++++------------------- 1 file changed, 15 insertions(+), 70 deletions(-) diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index b1f45393f26..98cea017c32 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -235,70 +235,23 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) ->addLifecycleEvent('cleanUrlsInContent', Events::prePersist); $builder->addIdColumns(); - $builder->createField('subject', Type::TEXT) - ->nullable() - ->build(); - - $builder->createField('fromAddress', Type::STRING) - ->columnName('from_address') - ->nullable() - ->build(); - - $builder->createField('fromName', Type::STRING) - ->columnName('from_name') - ->nullable() - ->build(); - - $builder->createField('replyToAddress', Type::STRING) - ->columnName('reply_to_address') - ->nullable() - ->build(); - - $builder->createField('bccAddress', Type::STRING) - ->columnName('bcc_address') - ->nullable() - ->build(); - - $builder->createField('template', Type::STRING) - ->nullable() - ->build(); - - $builder->createField('content', Type::ARRAY) - ->nullable() - ->build(); - - $builder->createField('utmTags', Type::ARRAY) - ->columnName('utm_tags') - ->nullable() - ->build(); - - $builder->createField('plainText', Type::TEXT) - ->columnName('plain_text') - ->nullable() - ->build(); - - $builder->createField('customHtml', Type::TEXT) - ->columnName('custom_html') - ->nullable() - ->build(); - - $builder->createField('emailType', Type::TEXT) - ->columnName('email_type') - ->nullable() - ->build(); - + $builder->addNullableField('subject', Type::TEXT); + $builder->addNullableField('fromAddress', Type::STRING, 'from_address'); + $builder->addNullableField('fromName', Type::STRING, 'from_name'); + $builder->addNullableField('replyToAddress', Type::STRING, 'reply_to_address'); + $builder->addNullableField('bccAddress', Type::STRING, 'bcc_address'); + $builder->addNullableField('template', Type::STRING); + $builder->addNullableField('content', Type::ARRAY); + $builder->addNullableField('utmTags', Type::ARRAY, 'utm_tags'); + $builder->addNullableField('plainText', Type::TEXT, 'plain_text'); + $builder->addNullableField('customHtml', Type::TEXT, 'custom_html'); + $builder->addNullableField('emailType', Type::TEXT, 'email_type'); $builder->addPublishDates(); - - $builder->createField('readCount', Type::INTEGER) - ->columnName('read_count') - ->build(); - - $builder->createField('sentCount', Type::INTEGER) - ->columnName('sent_count') - ->build(); - + $builder->addNamedField('readCount', Type::INTEGER, 'read_count'); + $builder->addNamedField('sentCount', Type::INTEGER, 'sent_count'); + $builder->addNamedField('variantSentCount', Type::INTEGER, 'variant_sent_count'); + $builder->addNamedField('variantReadCount', Type::INTEGER, 'variant_read_count'); $builder->addField('revision', Type::INTEGER); - $builder->addCategory(); $builder->createManyToMany('lists', LeadList::class) @@ -320,14 +273,6 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) self::addVariantMetadata($builder, self::class); self::addDynamicContentMetadata($builder); - $builder->createField('variantSentCount', Type::INTEGER) - ->columnName('variant_sent_count') - ->build(); - - $builder->createField('variantReadCount', Type::INTEGER) - ->columnName('variant_read_count') - ->build(); - $builder->createManyToOne('unsubscribeForm', Form::class) ->addJoinColumn('unsubscribeform_id', 'id', true, false, 'SET NULL') ->build(); From fcfb5c849ef810092f0eceb4153d9fa1b45a0df0 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 6 Mar 2018 13:20:18 +0100 Subject: [PATCH 161/778] Reset read counts on clone --- app/bundles/EmailBundle/Controller/EmailController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/EmailBundle/Controller/EmailController.php b/app/bundles/EmailBundle/Controller/EmailController.php index 99778e2e148..07ba6cb6ca9 100644 --- a/app/bundles/EmailBundle/Controller/EmailController.php +++ b/app/bundles/EmailBundle/Controller/EmailController.php @@ -1065,8 +1065,10 @@ public function abtestAction($objectId) //reset $clone->clearStats(); $clone->setSentCount(0); + $clone->setReadCount(0); $clone->setRevision(0); $clone->setVariantSentCount(0); + $clone->setVariantReadCount(0); $clone->setVariantStartDate(null); $clone->setIsPublished(false); $clone->setEmailType($emailType); From e59af72f4640f7f9fc975a5b18d88ff136f5616f Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 6 Mar 2018 13:20:42 +0100 Subject: [PATCH 162/778] Constant name fix --- app/bundles/EmailBundle/Entity/Email.php | 4 ++-- app/bundles/EmailBundle/Entity/EmailRepository.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index 98cea017c32..aff040a935c 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -241,8 +241,8 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) $builder->addNullableField('replyToAddress', Type::STRING, 'reply_to_address'); $builder->addNullableField('bccAddress', Type::STRING, 'bcc_address'); $builder->addNullableField('template', Type::STRING); - $builder->addNullableField('content', Type::ARRAY); - $builder->addNullableField('utmTags', Type::ARRAY, 'utm_tags'); + $builder->addNullableField('content', Type::TARRAY); + $builder->addNullableField('utmTags', Type::TARRAY, 'utm_tags'); $builder->addNullableField('plainText', Type::TEXT, 'plain_text'); $builder->addNullableField('customHtml', Type::TEXT, 'custom_html'); $builder->addNullableField('emailType', Type::TEXT, 'email_type'); diff --git a/app/bundles/EmailBundle/Entity/EmailRepository.php b/app/bundles/EmailBundle/Entity/EmailRepository.php index 8e50b31a7ac..4d56bc8c3ea 100644 --- a/app/bundles/EmailBundle/Entity/EmailRepository.php +++ b/app/bundles/EmailBundle/Entity/EmailRepository.php @@ -523,7 +523,7 @@ public function resetVariants($relatedIds, $date) /** * Up the read/sent counts. * - * @param $id + * @param int $id * @param string $type * @param int $increaseBy * @param bool|false $variant From 8db043ef9ffbb756b502ff85c17a15d76f1e2305 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 6 Mar 2018 13:27:33 +0100 Subject: [PATCH 163/778] Actually, the clone can be much simpler since it's in the __clone method already --- app/bundles/EmailBundle/Controller/EmailController.php | 8 -------- app/bundles/EmailBundle/Entity/Email.php | 3 +++ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/Controller/EmailController.php b/app/bundles/EmailBundle/Controller/EmailController.php index 07ba6cb6ca9..5de741f1c2d 100644 --- a/app/bundles/EmailBundle/Controller/EmailController.php +++ b/app/bundles/EmailBundle/Controller/EmailController.php @@ -1062,14 +1062,6 @@ public function abtestAction($objectId) $clone = clone $entity; - //reset - $clone->clearStats(); - $clone->setSentCount(0); - $clone->setReadCount(0); - $clone->setRevision(0); - $clone->setVariantSentCount(0); - $clone->setVariantReadCount(0); - $clone->setVariantStartDate(null); $clone->setIsPublished(false); $clone->setEmailType($emailType); $clone->setVariantParent($entity); diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index aff040a935c..07217841df0 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -194,6 +194,7 @@ public function __clone() $this->readCount = 0; $this->revision = 0; $this->variantSentCount = 0; + $this->variantReadCount = 0; $this->variantStartDate = null; $this->emailType = null; $this->sessionId = 'new_'.hash('sha1', uniqid(mt_rand())); @@ -216,6 +217,8 @@ public function __construct() /** * Clear stats. + * + * @deprecated since 2.13.0, to be removed in 3.0.0 - not used anywhere */ public function clearStats() { From 19625bb22fa3d2e23a58a74d23b184d1a4845da8 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 6 Mar 2018 13:43:39 +0100 Subject: [PATCH 164/778] Actually, lets keep the clearStats method instead of reseting the property --- app/bundles/EmailBundle/Entity/Email.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index 07217841df0..db2d6dc1d04 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -189,7 +189,6 @@ class Email extends FormEntity implements VariantEntityInterface, TranslationEnt public function __clone() { $this->id = null; - $this->stats = new ArrayCollection(); $this->sentCount = 0; $this->readCount = 0; $this->revision = 0; @@ -200,6 +199,7 @@ public function __clone() $this->sessionId = 'new_'.hash('sha1', uniqid(mt_rand())); $this->clearTranslations(); $this->clearVariants(); + $this->clearStats(); parent::__clone(); } @@ -217,8 +217,6 @@ public function __construct() /** * Clear stats. - * - * @deprecated since 2.13.0, to be removed in 3.0.0 - not used anywhere */ public function clearStats() { From 3d5a60b7e5d43492bcd130c16b5eaff6d25a6911 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 8 Mar 2018 15:51:17 +0100 Subject: [PATCH 165/778] Contact segment filter tweak --- .../Segment/ContactSegmentFilter.php | 2 +- .../Segment/ContactSegmentFilterCrate.php | 38 +++---------------- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 07d5fe2a040..f61a061d39d 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -188,7 +188,7 @@ public function isContactSegmentReference() */ public function isColumnTypeBoolean() { - return $this->contactSegmentFilterCrate->getType() === 'boolean'; + return $this->contactSegmentFilterCrate->isBooleanType(); } /** diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 6e2e6459e41..0b715a49d08 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -13,8 +13,8 @@ class ContactSegmentFilterCrate { - const CONTACT_OBJECT = 'lead'; - const COMPANY_OBJECT = 'company'; + const CONTACT_OBJECT = 'lead'; + const COMPANY_OBJECT = 'company'; /** * @var string|null @@ -41,21 +41,11 @@ class ContactSegmentFilterCrate */ private $filter; - /** - * @var string|null - */ - private $display; - /** * @var string|null */ private $operator; - /** - * @var string - */ - private $func; - /** * ContactSegmentFilterCrate constructor. * @@ -67,8 +57,6 @@ public function __construct(array $filter) $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->display = isset($filter['display']) ? $filter['display'] : null; - $this->func = isset($filter['func']) ? $filter['func'] : null; $this->operator = isset($filter['operator']) ? $filter['operator'] : null; $this->filter = isset($filter['filter']) ? $filter['filter'] : null; } @@ -89,14 +77,6 @@ public function getField() return $this->field; } - /** - * @return string|null - */ - public function getObject() - { - return $this->object; - } - /** * @return bool */ @@ -129,14 +109,6 @@ public function getFilter() return $this->filter; } - /** - * @return string|null - */ - public function getDisplay() - { - return $this->display; - } - /** * @return string|null */ @@ -146,11 +118,11 @@ public function getOperator() } /** - * @return string + * @return bool */ - public function getFunc() + public function isBooleanType() { - return $this->func; + return $this->getType() === 'boolean'; } /** From b2fa07163f9b544915b8222e00aad04370a3f141 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 8 Mar 2018 15:52:05 +0100 Subject: [PATCH 166/778] Contact segment filter crate - test --- .../Segment/ContactSegmentFilterCrateTest.php | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php new file mode 100644 index 00000000000..ae2ef5ee469 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php @@ -0,0 +1,128 @@ +assertNull($contactSegmentFilterCrate->getGlue()); + $this->assertNull($contactSegmentFilterCrate->getField()); + $this->assertTrue($contactSegmentFilterCrate->isContactType()); + $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); + $this->assertNull($contactSegmentFilterCrate->getType()); + $this->assertNull($contactSegmentFilterCrate->getFilter()); + $this->assertNull($contactSegmentFilterCrate->getOperator()); + $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); + $this->assertFalse($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testDateIdentifiedFilter() + { + $filter = [ + 'glue' => 'and', + 'field' => 'date_identified', + 'object' => 'lead', + 'type' => 'datetime', + 'filter' => null, + 'display' => null, + 'operator' => '!empty', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertEquals('and', $contactSegmentFilterCrate->getGlue()); + $this->assertEquals('date_identified', $contactSegmentFilterCrate->getField()); + $this->assertTrue($contactSegmentFilterCrate->isContactType()); + $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); + $this->assertEquals('datetime', $contactSegmentFilterCrate->getType()); + $this->assertNull($contactSegmentFilterCrate->getFilter()); + $this->assertEquals('!empty', $contactSegmentFilterCrate->getOperator()); + $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); + $this->assertTrue($contactSegmentFilterCrate->isDateType()); + $this->assertTrue($contactSegmentFilterCrate->hasTimeParts()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testDateFilter() + { + $filter = [ + 'glue' => 'and', + 'field' => 'date_identified', + 'object' => 'lead', + 'type' => 'date', + 'filter' => null, + 'display' => null, + 'operator' => '!empty', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertEquals('and', $contactSegmentFilterCrate->getGlue()); + $this->assertEquals('date_identified', $contactSegmentFilterCrate->getField()); + $this->assertTrue($contactSegmentFilterCrate->isContactType()); + $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); + $this->assertEquals('date', $contactSegmentFilterCrate->getType()); + $this->assertNull($contactSegmentFilterCrate->getFilter()); + $this->assertEquals('!empty', $contactSegmentFilterCrate->getOperator()); + $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); + $this->assertTrue($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testBooleanFilter() + { + $filter = [ + 'type' => 'boolean', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertEquals('boolean', $contactSegmentFilterCrate->getType()); + $this->assertTrue($contactSegmentFilterCrate->isBooleanType()); + $this->assertFalse($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testCompanyTypeFilter() + { + $filter = [ + 'object' => 'company', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertFalse($contactSegmentFilterCrate->isContactType()); + $this->assertTrue($contactSegmentFilterCrate->isCompanyType()); + } +} From 7467ecaf6fbe059cd9136072f57dc0be591a3e98 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 8 Mar 2018 15:54:36 +0100 Subject: [PATCH 167/778] Basic test for contact segment filter factory --- .../ContactSegmentFilterFactoryTest.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php new file mode 100644 index 00000000000..5e479856177 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php @@ -0,0 +1,69 @@ +createMock(TableSchemaColumnsCache::class); + $container = $this->createMock(Container::class); + $decoratorFactory = $this->createMock(DecoratorFactory::class); + + $filterDecorator = $this->createMock(FilterDecoratorInterface::class); + $decoratorFactory->expects($this->once()) + ->method('getDecoratorForFilter') + ->willReturn($filterDecorator); + + $filterDecorator->expects($this->once()) + ->method('getQueryType') + ->willReturn('MyQueryTypeId'); + + $filterQueryBuilder = $this->createMock(FilterQueryBuilderInterface::class); + $container->expects($this->once()) + ->method('get') + ->with('MyQueryTypeId') + ->willReturn($filterQueryBuilder); + + $contactSegmentFilterFactory = new ContactSegmentFilterFactory($tableSchemaColumnsCache, $container, $decoratorFactory); + + $leadList = new LeadList(); + $leadList->setFilters([ + [ + 'glue' => 'and', + 'field' => 'date_identified', + 'object' => 'lead', + 'type' => 'datetime', + 'filter' => null, + 'display' => null, + 'operator' => '!empty', + ], + ]); + + $contactSegmentFilters = $contactSegmentFilterFactory->getSegmentFilters($leadList); + + $this->assertInstanceOf(ContactSegmentFilters::class, $contactSegmentFilters); + $this->assertCount(1, $contactSegmentFilters); + } +} From 7924edfad49ec30b7396680d8589ec50feb9bee2 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 8 Mar 2018 17:07:34 +0100 Subject: [PATCH 168/778] Contact segment filter crate refactoring - hide type as implementation detail --- .../Segment/ContactSegmentFilterCrate.php | 43 +++++++++++++++---- .../Segment/Decorator/BaseDecorator.php | 7 +-- .../Segment/ContactSegmentFilterCrateTest.php | 40 ++++++++++++----- 3 files changed, 65 insertions(+), 25 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 0b715a49d08..1efa1efb97a 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -94,18 +94,17 @@ public function isCompanyType() } /** - * @return string|null - */ - public function getType() - { - return $this->type; - } - - /** - * @return string|array|null + * @return string|array|bool|float|null */ public function getFilter() { + switch ($this->getType()) { + case 'number': + return (float) $this->filter; + case 'boolean': + return (bool) $this->filter; + } + return $this->filter; } @@ -125,6 +124,14 @@ public function isBooleanType() return $this->getType() === 'boolean'; } + /** + * @return bool + */ + public function isNumberType() + { + return $this->getType() === 'number'; + } + /** * @return bool */ @@ -140,4 +147,22 @@ public function hasTimeParts() { return $this->getType() === 'datetime'; } + + /** + * Filter value could be used directly - no modification (like regex etc.) needed. + * + * @return bool + */ + public function filterValueDoNotNeedAdjustment() + { + return $this->isNumberType() || $this->isBooleanType(); + } + + /** + * @return string|null + */ + private function getType() + { + return $this->type; + } } diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 159ed9828d4..84db4cddc82 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -123,11 +123,8 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte { $filter = $contactSegmentFilterCrate->getFilter(); - switch ($contactSegmentFilterCrate->getType()) { - case 'number': - return (float) $filter; - case 'boolean': - return (bool) $filter; + if ($contactSegmentFilterCrate->filterValueDoNotNeedAdjustment()) { + return $filter; } switch ($this->getOperator($contactSegmentFilterCrate)) { diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php index ae2ef5ee469..ff6fcf8cf54 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php @@ -28,7 +28,6 @@ public function testEmptyFilter() $this->assertNull($contactSegmentFilterCrate->getField()); $this->assertTrue($contactSegmentFilterCrate->isContactType()); $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); - $this->assertNull($contactSegmentFilterCrate->getType()); $this->assertNull($contactSegmentFilterCrate->getFilter()); $this->assertNull($contactSegmentFilterCrate->getOperator()); $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); @@ -53,13 +52,12 @@ public function testDateIdentifiedFilter() $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $this->assertEquals('and', $contactSegmentFilterCrate->getGlue()); - $this->assertEquals('date_identified', $contactSegmentFilterCrate->getField()); + $this->assertSame('and', $contactSegmentFilterCrate->getGlue()); + $this->assertSame('date_identified', $contactSegmentFilterCrate->getField()); $this->assertTrue($contactSegmentFilterCrate->isContactType()); $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); - $this->assertEquals('datetime', $contactSegmentFilterCrate->getType()); $this->assertNull($contactSegmentFilterCrate->getFilter()); - $this->assertEquals('!empty', $contactSegmentFilterCrate->getOperator()); + $this->assertSame('!empty', $contactSegmentFilterCrate->getOperator()); $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); $this->assertTrue($contactSegmentFilterCrate->isDateType()); $this->assertTrue($contactSegmentFilterCrate->hasTimeParts()); @@ -82,13 +80,12 @@ public function testDateFilter() $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $this->assertEquals('and', $contactSegmentFilterCrate->getGlue()); - $this->assertEquals('date_identified', $contactSegmentFilterCrate->getField()); + $this->assertSame('and', $contactSegmentFilterCrate->getGlue()); + $this->assertSame('date_identified', $contactSegmentFilterCrate->getField()); $this->assertTrue($contactSegmentFilterCrate->isContactType()); $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); - $this->assertEquals('date', $contactSegmentFilterCrate->getType()); $this->assertNull($contactSegmentFilterCrate->getFilter()); - $this->assertEquals('!empty', $contactSegmentFilterCrate->getOperator()); + $this->assertSame('!empty', $contactSegmentFilterCrate->getOperator()); $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); $this->assertTrue($contactSegmentFilterCrate->isDateType()); $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); @@ -100,15 +97,36 @@ public function testDateFilter() public function testBooleanFilter() { $filter = [ - 'type' => 'boolean', + 'type' => 'boolean', + 'filter' => '1', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - $this->assertEquals('boolean', $contactSegmentFilterCrate->getType()); + $this->assertTrue($contactSegmentFilterCrate->getFilter()); $this->assertTrue($contactSegmentFilterCrate->isBooleanType()); $this->assertFalse($contactSegmentFilterCrate->isDateType()); $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + $this->assertTrue($contactSegmentFilterCrate->filterValueDoNotNeedAdjustment()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testNumericFilter() + { + $filter = [ + 'type' => 'number', + 'filter' => '2', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertSame(2.0, $contactSegmentFilterCrate->getFilter()); + $this->assertTrue($contactSegmentFilterCrate->isNumberType()); + $this->assertFalse($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + $this->assertTrue($contactSegmentFilterCrate->filterValueDoNotNeedAdjustment()); } /** From e5c4d5fb569a4755bb6f67c55a23a8086b88ebe3 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 11:55:22 +0100 Subject: [PATCH 169/778] Fix DateWeek tests - get add relative date to be sure test does not depends on current date --- .../Decorator/Date/Week/DateWeekLastTest.php | 101 ++++++++++-------- .../Decorator/Date/Week/DateWeekNextTest.php | 15 ++- .../Decorator/Date/Week/DateWeekThisTest.php | 15 ++- 3 files changed, 82 insertions(+), 49 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php index b62183b4431..fcee37f405a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php @@ -19,48 +19,48 @@ class DateWeekLastTest extends \PHPUnit_Framework_TestCase { - /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator - */ - public function testGetOperatorBetween() - { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); - - $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); - - $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); - } - - /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator - */ - public function testGetOperatorLessOrEqual() - { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateDecorator->method('getOperator') - ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); - - $filter = [ - 'operator' => '=<', - ]; - $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); - - $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); - - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); - } +// /** +// * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator +// */ +// public function testGetOperatorBetween() +// { +// $dateDecorator = $this->createMock(DateDecorator::class); +// $dateOptionParameters = $this->createMock(DateOptionParameters::class); +// +// $dateOptionParameters->method('isBetweenRequired') +// ->willReturn(true); +// +// $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); +// +// $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); +// +// $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); +// } +// +// /** +// * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator +// */ +// public function testGetOperatorLessOrEqual() +// { +// $dateDecorator = $this->createMock(DateDecorator::class); +// $dateOptionParameters = $this->createMock(DateOptionParameters::class); +// +// $dateDecorator->method('getOperator') +// ->with() +// ->willReturn('==<<'); //Test that value is really returned from Decorator +// +// $dateOptionParameters->method('isBetweenRequired') +// ->willReturn(false); +// +// $filter = [ +// 'operator' => '=<', +// ]; +// $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); +// +// $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); +// +// $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); +// } /** * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,16 @@ public function testGetParameterValueBetween() $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); - $this->assertEquals(['2018-02-19', '2018-02-25'], $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDateStart = new \DateTime('monday last week'); + $expectedDateEnd = new \DateTime('sunday last week'); + + $this->assertEquals( + [ + $expectedDateStart->format('Y-m-d'), + $expectedDateEnd->format('Y-m-d'), + ], + $filterDecorator->getParameterValue($contactSegmentFilterCrate) + ); } /** @@ -110,6 +119,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-02-19', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('monday last week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php index 57e24f6eea6..b283da1c893 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php @@ -83,7 +83,16 @@ public function testGetParameterValueBetween() $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); - $this->assertEquals(['2018-03-05', '2018-03-11'], $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDateStart = new \DateTime('monday next week'); + $expectedDateEnd = new \DateTime('sunday next week'); + + $this->assertEquals( + [ + $expectedDateStart->format('Y-m-d'), + $expectedDateEnd->format('Y-m-d'), + ], + $filterDecorator->getParameterValue($contactSegmentFilterCrate) + ); } /** @@ -110,6 +119,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-03-05', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('monday next week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php index e1d7b85f219..519c78fb3d8 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php @@ -83,7 +83,16 @@ public function testGetParameterValueBetween() $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); - $this->assertEquals(['2018-02-26', '2018-03-04'], $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDateStart = new \DateTime('monday this week'); + $expectedDateEnd = new \DateTime('sunday this week'); + + $this->assertEquals( + [ + $expectedDateStart->format('Y-m-d'), + $expectedDateEnd->format('Y-m-d'), + ], + $filterDecorator->getParameterValue($contactSegmentFilterCrate) + ); } /** @@ -110,6 +119,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-02-26', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('monday this week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } From 890841e101c2214cafd4ab235baba2c3fe00f958 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 11:56:11 +0100 Subject: [PATCH 170/778] Date last week test - uncomment tests --- .../Decorator/Date/Week/DateWeekLastTest.php | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php index fcee37f405a..43b907c1488 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php @@ -19,48 +19,48 @@ class DateWeekLastTest extends \PHPUnit_Framework_TestCase { -// /** -// * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator -// */ -// public function testGetOperatorBetween() -// { -// $dateDecorator = $this->createMock(DateDecorator::class); -// $dateOptionParameters = $this->createMock(DateOptionParameters::class); -// -// $dateOptionParameters->method('isBetweenRequired') -// ->willReturn(true); -// -// $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); -// -// $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); -// -// $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); -// } -// -// /** -// * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator -// */ -// public function testGetOperatorLessOrEqual() -// { -// $dateDecorator = $this->createMock(DateDecorator::class); -// $dateOptionParameters = $this->createMock(DateOptionParameters::class); -// -// $dateDecorator->method('getOperator') -// ->with() -// ->willReturn('==<<'); //Test that value is really returned from Decorator -// -// $dateOptionParameters->method('isBetweenRequired') -// ->willReturn(false); -// -// $filter = [ -// 'operator' => '=<', -// ]; -// $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); -// -// $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); -// -// $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); -// } + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator + */ + public function testGetOperatorBetween() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(true); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('between', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator + */ + public function testGetOperatorLessOrEqual() + { + $dateDecorator = $this->createMock(DateDecorator::class); + $dateOptionParameters = $this->createMock(DateOptionParameters::class); + + $dateDecorator->method('getOperator') + ->with() + ->willReturn('==<<'); //Test that value is really returned from Decorator + + $dateOptionParameters->method('isBetweenRequired') + ->willReturn(false); + + $filter = [ + 'operator' => '=<', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + } /** * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue From 1077a558c26d01814e47549526b9b379506d2828 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 12:07:01 +0100 Subject: [PATCH 171/778] Test for week - get rid of fixed date time which does not make any sense now --- .../Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php | 2 +- .../Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php | 4 ++-- .../Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php index 43b907c1488..0199725969f 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php @@ -106,7 +106,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php index b283da1c893..69cde785ca0 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -106,7 +106,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php index 519c78fb3d8..54fafaf21d7 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -106,7 +106,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() From 792f2532df55c2733c247cc31d3c841717eeec82 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 12:11:47 +0100 Subject: [PATCH 172/778] Fix DateMonth tests - use relative date to be sure test does not depend on current date --- .../Decorator/Date/Month/DateMonthLastTest.php | 12 ++++++++---- .../Decorator/Date/Month/DateMonthNextTest.php | 12 ++++++++---- .../Decorator/Date/Month/DateMonthThisTest.php | 12 ++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php index 21ed08015a2..cf664656b3e 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,9 @@ public function testGetParameterValueBetween() $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-02-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of last month'); + + $this->assertEquals($expectedDate->format('Y-m-%'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } /** @@ -97,7 +99,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -107,6 +109,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-02-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of last month'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php index a309db5cbcc..2d609b2a522 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,9 @@ public function testGetParameterValueBetween() $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-04-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of next month'); + + $this->assertEquals($expectedDate->format('Y-m-%'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } /** @@ -97,7 +99,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -107,6 +109,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-04-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of next month'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php index 0943776c91c..a00080e1ded 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,9 @@ public function testGetParameterValueBetween() $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-03-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of this month'); + + $this->assertEquals($expectedDate->format('Y-m-%'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } /** @@ -97,7 +99,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -107,6 +109,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-03-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of this month'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } From 1a0a34b84904592f3b6f0ba03c90a1e4cda43faa Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 12:18:32 +0100 Subject: [PATCH 173/778] Fix DateYear tests - use relative date to be sure test does not depend on current date --- .../Segment/Decorator/Date/Year/DateYearLastTest.php | 12 ++++++++---- .../Segment/Decorator/Date/Year/DateYearNextTest.php | 12 ++++++++---- .../Segment/Decorator/Date/Year/DateYearThisTest.php | 12 ++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php index ceb1d251d1b..ee156d725fa 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,9 @@ public function testGetParameterValueBetween() $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('2017-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of january last year'); + + $this->assertEquals($expectedDate->format('Y-%'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } /** @@ -97,7 +99,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -107,6 +109,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('2017-01-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of january last year'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php index 76627a19863..a0f8e786b7b 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,9 @@ public function testGetParameterValueBetween() $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('2019-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of january next year'); + + $this->assertEquals($expectedDate->format('Y-%'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } /** @@ -97,7 +99,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -107,6 +109,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('2019-01-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of january next year'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php index 70d9054c9b1..e891c9fb3d7 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php @@ -73,7 +73,7 @@ public function testGetParameterValueBetween() $dateOptionParameters->method('isBetweenRequired') ->willReturn(true); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -83,7 +83,9 @@ public function testGetParameterValueBetween() $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of january this year'); + + $this->assertEquals($expectedDate->format('Y-%'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } /** @@ -97,7 +99,7 @@ public function testGetParameterValueSingle() $dateOptionParameters->method('isBetweenRequired') ->willReturn(false); - $date = new DateTimeHelper('2018-03-02', null, 'local'); + $date = new DateTimeHelper('', null, 'local'); $dateDecorator->method('getDefaultDate') ->with() @@ -107,6 +109,8 @@ public function testGetParameterValueSingle() $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('2018-01-01', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + $expectedDate = new \DateTime('first day of january this year'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } } From a4e6316095d4699e1934d9c6644b263a4685ade5 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 13:28:29 +0100 Subject: [PATCH 174/778] Add test for date relative format (x days ago) --- .../Date/Other/DateRelativeIntervalTest.php | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php index 971f7d604f4..9a21f7adbde 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php @@ -97,7 +97,7 @@ public function testGetParameterValuePlusDaysWithGreaterOperator() /** * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getParameterValue */ - public function testGetParameterValueMinusMonthDaysWithNotEqualOperator() + public function testGetParameterValueMinusMonthWithNotEqualOperator() { $dateDecorator = $this->createMock(DateDecorator::class); @@ -116,4 +116,50 @@ public function testGetParameterValueMinusMonthDaysWithNotEqualOperator() $this->assertEquals('2017-12-02%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getParameterValue + */ + public function testGetParameterValueDaysAgoWithNotEqualOperator() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '5 days ago'); + + $this->assertEquals('2018-02-25%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getParameterValue + */ + public function testGetParameterValueYearsAgoWithGreaterOperator() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '>', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '2 years ago'); + + $this->assertEquals('2016-03-02', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } } From b58d16f50f3364ae188b17c6d91eebd567c3dbff Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 9 Mar 2018 14:41:35 +0100 Subject: [PATCH 175/778] Add test for date relative format (x days) --- .../Date/Other/DateRelativeIntervalTest.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php index 9a21f7adbde..9da19592f94 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Other/DateRelativeIntervalTest.php @@ -162,4 +162,27 @@ public function testGetParameterValueYearsAgoWithGreaterOperator() $this->assertEquals('2016-03-02', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Other\DateRelativeInterval::getParameterValue + */ + public function testGetParameterValueDaysWithEqualOperator() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('2018-03-02', null, 'local'); + + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $filterDecorator = new DateRelativeInterval($dateDecorator, '5 days'); + + $this->assertEquals('2018-03-07%', $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } } From 65c366092c0688473c948e1e8226eb7da392d3a6 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 9 Mar 2018 15:25:28 +0100 Subject: [PATCH 176/778] add limits for beanstalk command to segment service --- .../Segment/ContactSegmentService.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index f54b0b2dd63..76ec58bcf06 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment; +use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; @@ -38,14 +39,21 @@ class ContactSegmentService */ private $preparedQB; + /** + * @var EntityManager + */ + private $entityManager; + public function __construct( ContactSegmentFilterFactory $contactSegmentFilterFactory, ContactSegmentQueryBuilder $queryBuilder, - Logger $logger + Logger $logger, + EntityManager $entityManager ) { $this->contactSegmentFilterFactory = $contactSegmentFilterFactory; $this->contactSegmentQueryBuilder = $queryBuilder; $this->logger = $logger; + $this->entityManager = $entityManager; } /** @@ -119,9 +127,15 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters $qb = $this->getNewSegmentContactsQuery($segment, $batchLimiters); - $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); + if (isset($batchLimiters['minId'])) { + $qb->andWhere($qb->expr()->gte('l.id', $qb->expr()->literal(intval($batchLimiters['minId'])))); + } + + if (isset($batchLimiters['maxId'])) { + $qb->andWhere($qb->expr()->lte('l.id', $qb->expr()->literal(intval($batchLimiters['maxId'])))); + } - //dump($qb->getLogicStack()); + $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); @@ -177,7 +191,7 @@ public function getTotalLeadListLeadsCount(LeadList $segment) public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $limit = 1000) { $queryBuilder = $this->getNewSegmentContactsQuery($segment, $batchLimiters); - $queryBuilder->select('DISTINCT l.id'); + $queryBuilder->select('DISTINCT l.*'); $this->logger->debug('Segment QB: Create Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); From a1d172f946a7cf111d9b56309da88e7ac10ee0ea Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 12 Mar 2018 17:36:07 +0100 Subject: [PATCH 177/778] ContactSegmentService fix - remove unused EM from constructor --- .../LeadBundle/Segment/ContactSegmentService.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 76ec58bcf06..32c67360653 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Segment; -use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryBuilder; @@ -39,21 +38,14 @@ class ContactSegmentService */ private $preparedQB; - /** - * @var EntityManager - */ - private $entityManager; - public function __construct( ContactSegmentFilterFactory $contactSegmentFilterFactory, ContactSegmentQueryBuilder $queryBuilder, - Logger $logger, - EntityManager $entityManager + Logger $logger ) { $this->contactSegmentFilterFactory = $contactSegmentFilterFactory; $this->contactSegmentQueryBuilder = $queryBuilder; $this->logger = $logger; - $this->entityManager = $entityManager; } /** From dedda1b2bf32b184a40eaa71bb5fd8e421626a46 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 12 Mar 2018 17:37:29 +0100 Subject: [PATCH 178/778] Relative format ago (5 days ago) works like Relative interval --- .../Segment/Decorator/Date/DateOptionFactory.php | 6 +++++- .../Segment/Decorator/Date/DateOptionFactoryTest.php | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index f9bf0aa5c69..b7b63abfe2d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -91,7 +91,11 @@ public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) return new DateYearNext($this->dateDecorator, $dateOptionParameters); case 'year_this': return new DateYearThis($this->dateDecorator, $dateOptionParameters); - case $timeframe && (false !== strpos($timeframe[0], '-') || false !== strpos($timeframe[0], '+')): + case $timeframe && ( + false !== strpos($timeframe[0], '-') || // -5 days + false !== strpos($timeframe[0], '+') || // +5 days + false !== strpos($timeframe, ' ago') // 5 days ago + ): return new DateRelativeInterval($this->dateDecorator, $originalValue); default: return new DateDefault($this->dateDecorator, $originalValue); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php index fbdbfcecb07..105c04bd04c 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php @@ -219,6 +219,18 @@ public function testRelativeMinus() $this->assertInstanceOf(DateRelativeInterval::class, $filterDecorator); } + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testRelativeAgo() + { + $filterName = '20 days ago'; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateRelativeInterval::class, $filterDecorator); + } + /** * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption */ From cf0e17694eea7aefc82f4a7c600fe3bbeec7c288 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 12 Mar 2018 17:59:32 +0100 Subject: [PATCH 179/778] Date filters - Fix for cases when we need to include whole date range (less than or equal operator) or start after date range (greater than operator) + tests --- .../Decorator/Date/DateOptionAbstract.php | 4 +- .../Decorator/Date/DateOptionParameters.php | 18 +-- .../Decorator/Date/Day/DateDayTodayTest.php | 49 ++++---- .../Date/Day/DateDayTomorrowTest.php | 49 ++++---- .../Date/Day/DateDayYesterdayTest.php | 49 ++++---- .../Date/Month/DateMonthLastTest.php | 49 ++++---- .../Date/Month/DateMonthNextTest.php | 49 ++++---- .../Date/Month/DateMonthThisTest.php | 49 ++++---- .../Decorator/Date/Week/DateWeekLastTest.php | 97 +++++++++++----- .../Decorator/Date/Week/DateWeekNextTest.php | 105 ++++++++++++------ .../Decorator/Date/Week/DateWeekThisTest.php | 105 ++++++++++++------ .../Decorator/Date/Year/DateYearLastTest.php | 47 ++++---- .../Decorator/Date/Year/DateYearNextTest.php | 47 ++++---- .../Decorator/Date/Year/DateYearThisTest.php | 47 ++++---- 14 files changed, 435 insertions(+), 329 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php index 3d53dbe0920..cb5a4736c56 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionAbstract.php @@ -103,14 +103,14 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte $this->modifyBaseDate($dateTimeHelper); - $modifier = $this->getModifierForBetweenRange(); $dateFormat = $this->dateOptionParameters->hasTimePart() ? 'Y-m-d H:i:s' : 'Y-m-d'; if ($this->dateOptionParameters->isBetweenRequired()) { return $this->getValueForBetweenRange($dateTimeHelper); } - if ($this->dateOptionParameters->shouldIncludeMidnigh()) { + if ($this->dateOptionParameters->shouldUseLastDayOfRange()) { + $modifier = $this->getModifierForBetweenRange(); $modifier .= ' -1 second'; $dateTimeHelper->modify($modifier); } diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php index ee2331778a3..438dc39c8f1 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionParameters.php @@ -33,7 +33,7 @@ class DateOptionParameters /** * @var bool */ - private $includeMidnigh; + private $shouldUseLastDayOfRange; /** * @param ContactSegmentFilterCrate $leadSegmentFilterCrate @@ -41,10 +41,10 @@ class DateOptionParameters */ public function __construct(ContactSegmentFilterCrate $leadSegmentFilterCrate, array $relativeDateStrings) { - $this->hasTimePart = $leadSegmentFilterCrate->hasTimeParts(); - $this->timeframe = $this->parseTimeFrame($leadSegmentFilterCrate, $relativeDateStrings); - $this->requiresBetween = in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); - $this->includeMidnigh = in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); + $this->hasTimePart = $leadSegmentFilterCrate->hasTimeParts(); + $this->timeframe = $this->parseTimeFrame($leadSegmentFilterCrate, $relativeDateStrings); + $this->requiresBetween = in_array($leadSegmentFilterCrate->getOperator(), ['=', '!='], true); + $this->shouldUseLastDayOfRange = in_array($leadSegmentFilterCrate->getOperator(), ['gt', 'lte'], true); } /** @@ -72,11 +72,15 @@ public function isBetweenRequired() } /** + * This function indicates that we need to modify date to the last date of range. + * "Less than or equal" operator means that we need to include whole week / month / year > last day from range + * "Grater than" needs same logic. + * * @return bool */ - public function shouldIncludeMidnigh() + public function shouldUseLastDayOfRange() { - return $this->includeMidnigh; + return $this->shouldUseLastDayOfRange; } /** diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php index 4bd59f697b5..23de4fbae02 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTodayTest.php @@ -24,13 +24,13 @@ class DateDayTodayTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); @@ -42,24 +42,21 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); @@ -91,11 +88,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); @@ -103,7 +96,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayToday($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php index d28db3ae0aa..2f1e3e41c6a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayTomorrowTest.php @@ -24,13 +24,13 @@ class DateDayTomorrowTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); @@ -42,24 +42,21 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); @@ -91,11 +88,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); @@ -103,7 +96,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayTomorrow($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php index 84ba00957b2..d3da77c713a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Day/DateDayYesterdayTest.php @@ -24,13 +24,13 @@ class DateDayYesterdayTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); @@ -42,24 +42,21 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); @@ -91,11 +88,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('2018-03-02', null, 'local'); @@ -103,7 +96,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateDayYesterday($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php index cf664656b3e..aa6b7904ad3 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthLastTest.php @@ -24,13 +24,13 @@ class DateMonthLastTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); @@ -42,24 +42,21 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); @@ -93,11 +90,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -105,7 +98,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthLast($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php index 2d609b2a522..a26cd0c915b 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthNextTest.php @@ -24,13 +24,13 @@ class DateMonthNextTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); @@ -42,24 +42,21 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); @@ -93,11 +90,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -105,7 +98,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthNext($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php index a00080e1ded..132bf7a2024 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Month/DateMonthThisTest.php @@ -24,13 +24,13 @@ class DateMonthThisTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); @@ -42,24 +42,21 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); @@ -93,11 +90,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -105,7 +98,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateMonthThis($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php index 0199725969f..4573ac40346 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekLastTest.php @@ -24,13 +24,13 @@ class DateWeekLastTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -42,24 +42,20 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** @@ -67,11 +63,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +71,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -100,11 +96,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -113,9 +105,10 @@ public function testGetParameterValueSingle() ->willReturn($date); $filter = [ - 'operator' => '<', + 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); @@ -123,4 +116,54 @@ public function testGetParameterValueSingle() $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueforGreaterOperatorIncludesSunday() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('', null, 'local'); + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => 'gt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $expectedDate = new \DateTime('sunday last week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueForLessThanOperatorIncludesSunday() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('', null, 'local'); + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => 'lte', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + + $filterDecorator = new DateWeekLast($dateDecorator, $dateOptionParameters); + + $expectedDate = new \DateTime('sunday last week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php index 69cde785ca0..e294ca41576 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekNextTest.php @@ -20,17 +20,17 @@ class DateWeekNextTest extends \PHPUnit_Framework_TestCase { /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getOperator + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -38,40 +38,32 @@ public function testGetOperatorBetween() } /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getOperator + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getParameterValue + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +71,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -96,15 +92,11 @@ public function testGetParameterValueBetween() } /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekNext::getParameterValue + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -113,9 +105,10 @@ public function testGetParameterValueSingle() ->willReturn($date); $filter = [ - 'operator' => '<', + 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); @@ -123,4 +116,54 @@ public function testGetParameterValueSingle() $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueforGreaterOperatorIncludesSunday() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('', null, 'local'); + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => 'gt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + + $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); + + $expectedDate = new \DateTime('sunday next week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueForLessThanOperatorIncludesSunday() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('', null, 'local'); + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => 'lte', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + + $filterDecorator = new DateWeekNext($dateDecorator, $dateOptionParameters); + + $expectedDate = new \DateTime('sunday next week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php index 54fafaf21d7..90ac4ba1a8c 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Week/DateWeekThisTest.php @@ -20,17 +20,17 @@ class DateWeekThisTest extends \PHPUnit_Framework_TestCase { /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getOperator + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -38,40 +38,32 @@ public function testGetOperatorBetween() } /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getOperator + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getOperator */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('=<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); - $this->assertEquals('==<<', $filterDecorator->getOperator($contactSegmentFilterCrate)); + $this->assertEquals('=<', $filterDecorator->getOperator($contactSegmentFilterCrate)); } /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getParameterValue + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +71,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -96,15 +92,11 @@ public function testGetParameterValueBetween() } /** - * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekThis::getParameterValue + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -113,9 +105,10 @@ public function testGetParameterValueSingle() ->willReturn($date); $filter = [ - 'operator' => '<', + 'operator' => 'lt', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); @@ -123,4 +116,54 @@ public function testGetParameterValueSingle() $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueforGreaterOperatorIncludesSunday() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('', null, 'local'); + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => 'gt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + + $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); + + $expectedDate = new \DateTime('sunday this week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\Week\DateWeekLast::getParameterValue + */ + public function testGetParameterValueForLessThanOperatorIncludesSunday() + { + $dateDecorator = $this->createMock(DateDecorator::class); + + $date = new DateTimeHelper('', null, 'local'); + $dateDecorator->method('getDefaultDate') + ->with() + ->willReturn($date); + + $filter = [ + 'operator' => 'lte', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); + + $filterDecorator = new DateWeekThis($dateDecorator, $dateOptionParameters); + + $expectedDate = new \DateTime('sunday this week'); + + $this->assertEquals($expectedDate->format('Y-m-d'), $filterDecorator->getParameterValue($contactSegmentFilterCrate)); + } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php index ee156d725fa..7afeb6d8cdf 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearLastTest.php @@ -24,13 +24,13 @@ class DateYearLastTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); @@ -42,20 +42,17 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('==<<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); @@ -93,11 +90,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -105,7 +98,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearLast($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php index a0f8e786b7b..3505132ef80 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearNextTest.php @@ -24,13 +24,13 @@ class DateYearNextTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); @@ -42,20 +42,17 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('==<<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); @@ -93,11 +90,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -105,7 +98,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearNext($dateDecorator, $dateOptionParameters); diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php index e891c9fb3d7..90838428eae 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/Year/DateYearThisTest.php @@ -24,13 +24,13 @@ class DateYearThisTest extends \PHPUnit_Framework_TestCase */ public function testGetOperatorBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); - - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); @@ -42,20 +42,17 @@ public function testGetOperatorBetween() */ public function testGetOperatorLessOrEqual() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); + $dateDecorator = $this->createMock(DateDecorator::class); $dateDecorator->method('getOperator') ->with() - ->willReturn('==<<'); //Test that value is really returned from Decorator - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + ->willReturn('==<<'); $filter = [ - 'operator' => '=<', + 'operator' => 'lte', ]; $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); @@ -67,11 +64,7 @@ public function testGetOperatorLessOrEqual() */ public function testGetParameterValueBetween() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(true); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -79,7 +72,11 @@ public function testGetParameterValueBetween() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => '!=', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); @@ -93,11 +90,7 @@ public function testGetParameterValueBetween() */ public function testGetParameterValueSingle() { - $dateDecorator = $this->createMock(DateDecorator::class); - $dateOptionParameters = $this->createMock(DateOptionParameters::class); - - $dateOptionParameters->method('isBetweenRequired') - ->willReturn(false); + $dateDecorator = $this->createMock(DateDecorator::class); $date = new DateTimeHelper('', null, 'local'); @@ -105,7 +98,11 @@ public function testGetParameterValueSingle() ->with() ->willReturn($date); - $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + $filter = [ + 'operator' => 'lt', + ]; + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + $dateOptionParameters = new DateOptionParameters($contactSegmentFilterCrate, []); $filterDecorator = new DateYearThis($dateDecorator, $dateOptionParameters); From 9685dc9143ec8e285564baf0e17891032ce540b2 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 13 Mar 2018 15:07:51 +0100 Subject: [PATCH 180/778] Cleaning of ContactSegmentFilters --- .../Segment/ContactSegmentFilters.php | 26 ------------------- .../ContactSegmentFilterFactoryTest.php | 26 +++++++++++++++---- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php index 4aafea6ab4a..b9c5592bdd4 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilters.php @@ -26,16 +26,6 @@ class ContactSegmentFilters implements \Iterator, \Countable */ private $contactSegmentFilters = []; - /** - * @var bool - */ - private $hasCompanyFilter = false; - - /** - * @var bool - */ - private $listFiltersInnerJoinCompany = false; - /** * @param ContactSegmentFilter $contactSegmentFilter * @@ -115,20 +105,4 @@ public function count() { return count($this->contactSegmentFilters); } - - /** - * @return bool - */ - public function isHasCompanyFilter() - { - return $this->hasCompanyFilter; - } - - /** - * @return bool - */ - public function isListFiltersInnerJoinCompany() - { - return $this->listFiltersInnerJoinCompany; - } } diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php index 5e479856177..2ed84d5b85f 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterFactoryTest.php @@ -25,23 +25,23 @@ class ContactSegmentFilterFactoryTest extends \PHPUnit_Framework_TestCase /** * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterFactory */ - public function testEmptyFilter() + public function testLeadFilter() { $tableSchemaColumnsCache = $this->createMock(TableSchemaColumnsCache::class); $container = $this->createMock(Container::class); $decoratorFactory = $this->createMock(DecoratorFactory::class); $filterDecorator = $this->createMock(FilterDecoratorInterface::class); - $decoratorFactory->expects($this->once()) + $decoratorFactory->expects($this->exactly(3)) ->method('getDecoratorForFilter') ->willReturn($filterDecorator); - $filterDecorator->expects($this->once()) + $filterDecorator->expects($this->exactly(3)) ->method('getQueryType') ->willReturn('MyQueryTypeId'); $filterQueryBuilder = $this->createMock(FilterQueryBuilderInterface::class); - $container->expects($this->once()) + $container->expects($this->exactly(3)) ->method('get') ->with('MyQueryTypeId') ->willReturn($filterQueryBuilder); @@ -59,11 +59,27 @@ public function testEmptyFilter() 'display' => null, 'operator' => '!empty', ], + [ + 'glue' => 'and', + 'type' => 'text', + 'field' => 'hit_url', + 'operator' => 'like', + 'filter' => 'test.com', + 'display' => '', + ], + [ + 'glue' => 'or', + 'type' => 'lookup', + 'field' => 'state', + 'operator' => '=', + 'filter' => 'QLD', + 'display' => '', + ], ]); $contactSegmentFilters = $contactSegmentFilterFactory->getSegmentFilters($leadList); $this->assertInstanceOf(ContactSegmentFilters::class, $contactSegmentFilters); - $this->assertCount(1, $contactSegmentFilters); + $this->assertCount(3, $contactSegmentFilters); } } From 865efc0287b38b3aa76878fbcba6e6a1c64242a4 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 13 Mar 2018 16:57:02 +0100 Subject: [PATCH 181/778] Tests for baseDecorator --- .../Segment/Decorator/BaseDecoratorTest.php | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php new file mode 100644 index 00000000000..13766a0884e --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php @@ -0,0 +1,209 @@ +createMock(ContactSegmentFilterOperator::class); + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'glue' => 'and', + 'field' => 'date_identified', + 'object' => 'lead', + 'type' => 'datetime', + 'filter' => null, + 'display' => null, + 'operator' => '!empty', + ]); + + $this->assertSame('date_identified', $baseDecorator->getField($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getTable + */ + public function testGetTableLead() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'object' => 'lead', + ]); + + $this->assertSame(MAUTIC_TABLE_PREFIX.'leads', $baseDecorator->getTable($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getTable + */ + public function testGetTableCompany() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'object' => 'company', + ]); + + $this->assertSame(MAUTIC_TABLE_PREFIX.'companies', $baseDecorator->getTable($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getOperator + */ + public function testGetOperatorEqual() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $contactSegmentFilterOperator->expects($this->once()) + ->method('fixOperator') + ->with('=') + ->willReturn('eq'); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'operator' => '=', + ]); + + $this->assertSame('eq', $baseDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getOperator + */ + public function testGetOperatorStartsWith() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $contactSegmentFilterOperator->expects($this->once()) + ->method('fixOperator') + ->with('startsWith') + ->willReturn('startsWith'); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'operator' => 'startsWith', + ]); + + $this->assertSame('like', $baseDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getOperator + */ + public function testGetOperatorEndsWith() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $contactSegmentFilterOperator->expects($this->once()) + ->method('fixOperator') + ->with('endsWith') + ->willReturn('endsWith'); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'operator' => 'endsWith', + ]); + + $this->assertSame('like', $baseDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getOperator + */ + public function testGetOperatorContainsWith() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $contactSegmentFilterOperator->expects($this->once()) + ->method('fixOperator') + ->with('contains') + ->willReturn('contains'); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'operator' => 'contains', + ]); + + $this->assertSame('like', $baseDecorator->getOperator($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getQueryType + */ + public function testGetQueryType() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $this->assertSame('mautic.lead.query.builder.basic', $baseDecorator->getQueryType($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterHolder + */ + public function testGetParameterHolderSingle() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $this->assertSame(':argument', $baseDecorator->getParameterHolder($contactSegmentFilterCrate, 'argument')); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterHolder + */ + public function testGetParameterHolderArray() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); + + $argument = [ + 'argument1', + 'argument2', + 'argument3', + ]; + + $expected = [ + ':argument1', + ':argument2', + ':argument3', + ]; + $this->assertSame($expected, $baseDecorator->getParameterHolder($contactSegmentFilterCrate, $argument)); + } +} From b603b04f6dc0d6597c473f8e7e9764b8e1ab82ef Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 14 Mar 2018 17:47:25 +0100 Subject: [PATCH 182/778] Get parameter value fix - correct value for contains, starts and ends with --- app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 84db4cddc82..e431aa974f5 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -127,7 +127,7 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte return $filter; } - switch ($this->getOperator($contactSegmentFilterCrate)) { + switch ($contactSegmentFilterCrate->getOperator()) { case 'like': case 'notLike': return strpos($filter, '%') === false ? '%'.$filter.'%' : $filter; From f29dd022ddf552caf5352119337acc54179eeda1 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 14 Mar 2018 17:47:37 +0100 Subject: [PATCH 183/778] Base decorator test --- .../Segment/Decorator/BaseDecoratorTest.php | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php index 13766a0884e..8b0cfd7cd4f 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php @@ -206,4 +206,171 @@ public function testGetParameterHolderArray() ]; $this->assertSame($expected, $baseDecorator->getParameterHolder($contactSegmentFilterCrate, $argument)); } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueBoolean() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'boolean', + 'filter' => '1', + ]); + + $this->assertTrue($baseDecorator->getParameterValue($contactSegmentFilterCrate)); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'boolean', + 'filter' => '0', + ]); + + $this->assertFalse($baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueNumber() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'number', + 'filter' => '1', + ]); + + $this->assertSame(1.0, $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueLikeNoPercent() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'like', + 'filter' => 'Test string', + ]); + + $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueLikeWithOnePercent() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'like', + 'filter' => '%Test string', + ]); + + $this->assertSame('%Test string', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueLikeWithTwoPercent() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'like', + 'filter' => '%Test string%', + ]); + + $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueStartsWith() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'startsWith', + 'filter' => 'Test string', + ]); + + $this->assertSame('Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueEndsWith() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'endsWith', + 'filter' => 'Test string', + ]); + + $this->assertSame('%Test string', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueContains() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'contains', + 'filter' => 'Test string', + ]); + + $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueRegex() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'contains', + 'filter' => 'Test string', + ]); + + $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } } From 8fddc5255a0e53fb7dca8a1991a5c91a7a4e98a7 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Mar 2018 13:09:44 +0100 Subject: [PATCH 184/778] Base decorator test - escaping values --- .../Segment/Decorator/BaseDecoratorTest.php | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php index 8b0cfd7cd4f..c2525300cce 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php @@ -28,8 +28,7 @@ public function setUp() */ public function testGetField() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'glue' => 'and', @@ -49,8 +48,7 @@ public function testGetField() */ public function testGetTableLead() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'object' => 'lead', @@ -64,8 +62,7 @@ public function testGetTableLead() */ public function testGetTableCompany() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'object' => 'company', @@ -159,9 +156,7 @@ public function testGetOperatorContainsWith() */ public function testGetQueryType() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); @@ -173,9 +168,7 @@ public function testGetQueryType() */ public function testGetParameterHolderSingle() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); @@ -187,9 +180,7 @@ public function testGetParameterHolderSingle() */ public function testGetParameterHolderArray() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([]); @@ -212,9 +203,7 @@ public function testGetParameterHolderArray() */ public function testGetParameterValueBoolean() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'boolean', @@ -236,9 +225,7 @@ public function testGetParameterValueBoolean() */ public function testGetParameterValueNumber() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'number', @@ -253,9 +240,7 @@ public function testGetParameterValueNumber() */ public function testGetParameterValueLikeNoPercent() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', @@ -271,9 +256,7 @@ public function testGetParameterValueLikeNoPercent() */ public function testGetParameterValueLikeWithOnePercent() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', @@ -289,9 +272,7 @@ public function testGetParameterValueLikeWithOnePercent() */ public function testGetParameterValueLikeWithTwoPercent() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', @@ -307,9 +288,7 @@ public function testGetParameterValueLikeWithTwoPercent() */ public function testGetParameterValueStartsWith() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', @@ -325,9 +304,7 @@ public function testGetParameterValueStartsWith() */ public function testGetParameterValueEndsWith() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', @@ -343,9 +320,7 @@ public function testGetParameterValueEndsWith() */ public function testGetParameterValueContains() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', @@ -359,18 +334,43 @@ public function testGetParameterValueContains() /** * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue */ - public function testGetParameterValueRegex() + public function testGetParameterValueContainsShouldNotBeEscaped() { - $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); - - $baseDecorator = new BaseDecorator($contactSegmentFilterOperator); + $baseDecorator = $this->getDecorator(); $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', 'operator' => 'contains', - 'filter' => 'Test string', + 'filter' => 'Test with % and special characters \% should not be escaped %', ]); - $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + $expected = '%Test with % and special characters \% should not be escaped %%'; + $this->assertSame($expected, $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueRegex() + { + $baseDecorator = $this->getDecorator(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => 'regexp', + 'filter' => 'Test \\s string', + ]); + + $this->assertSame('Test \s string', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @return BaseDecorator + */ + private function getDecorator() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + + return new BaseDecorator($contactSegmentFilterOperator); } } From a98271b99762f973b9532d296aa7fca47d72ecbb Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Mar 2018 14:55:45 +0100 Subject: [PATCH 185/778] Tests for Decorators --- .../Decorator/CustomMappedDecoratorTest.php | 79 ++++++++++++++ .../Decorator/DecoratorFactoryTest.php | 100 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/CustomMappedDecoratorTest.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/Decorator/DecoratorFactoryTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/CustomMappedDecoratorTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/CustomMappedDecoratorTest.php new file mode 100644 index 00000000000..e8d700bf206 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/CustomMappedDecoratorTest.php @@ -0,0 +1,79 @@ +getDecorator(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'field' => 'lead_email_read_count', + ]); + + $this->assertSame('open_count', $customMappedDecorator->getField($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator::getTable + */ + public function testGetTable() + { + $customMappedDecorator = $this->getDecorator(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'field' => 'lead_email_read_count', + ]); + + $this->assertSame(MAUTIC_TABLE_PREFIX.'email_stats', $customMappedDecorator->getTable($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\CustomMappedDecorator::getQueryType + */ + public function testGetQueryType() + { + $customMappedDecorator = $this->getDecorator(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'field' => 'dnc_bounced', + ]); + + $this->assertSame('mautic.lead.query.builder.special.dnc', $customMappedDecorator->getQueryType($contactSegmentFilterCrate)); + } + + /** + * @return CustomMappedDecorator + */ + private function getDecorator() + { + $contactSegmentFilterOperator = $this->createMock(ContactSegmentFilterOperator::class); + $contactSegmentFilterDictionary = new ContactSegmentFilterDictionary(); + + return new CustomMappedDecorator($contactSegmentFilterOperator, $contactSegmentFilterDictionary); + } +} diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/DecoratorFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/DecoratorFactoryTest.php new file mode 100644 index 00000000000..6bc5d874c97 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/DecoratorFactoryTest.php @@ -0,0 +1,100 @@ +getDecoratorFactory(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'field' => 'date_identified', + 'type' => 'number', + ]); + + $this->assertInstanceOf(BaseDecorator::class, $decoratorFactory->getDecoratorForFilter($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\DecoratorFactory::getDecoratorForFilter + */ + public function testCustomMappedDecorator() + { + $decoratorFactory = $this->getDecoratorFactory(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'field' => 'hit_url_count', + 'type' => 'number', + ]); + + $this->assertInstanceOf(CustomMappedDecorator::class, $decoratorFactory->getDecoratorForFilter($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\DecoratorFactory::getDecoratorForFilter + */ + public function testDateDecorator() + { + $contactSegmentFilterDictionary = new ContactSegmentFilterDictionary(); + $baseDecorator = $this->createMock(BaseDecorator::class); + $customMappedDecorator = $this->createMock(CustomMappedDecorator::class); + $dateOptionFactory = $this->createMock(DateOptionFactory::class); + $filterDecoratorInterface = $this->createMock(FilterDecoratorInterface::class); + + $decoratorFactory = new DecoratorFactory($contactSegmentFilterDictionary, $baseDecorator, $customMappedDecorator, $dateOptionFactory); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'date', + ]); + + $dateOptionFactory->expects($this->once()) + ->method('getDateOption') + ->with($contactSegmentFilterCrate) + ->willReturn($filterDecoratorInterface); + + $filterDecorator = $decoratorFactory->getDecoratorForFilter($contactSegmentFilterCrate); + + $this->assertInstanceOf(FilterDecoratorInterface::class, $filterDecorator); + $this->assertSame($filterDecoratorInterface, $filterDecorator); + } + + /** + * @return DecoratorFactory + */ + private function getDecoratorFactory() + { + $contactSegmentFilterDictionary = new ContactSegmentFilterDictionary(); + $baseDecorator = $this->createMock(BaseDecorator::class); + $customMappedDecorator = $this->createMock(CustomMappedDecorator::class); + $dateOptionFactory = $this->createMock(DateOptionFactory::class); + + return new DecoratorFactory($contactSegmentFilterDictionary, $baseDecorator, $customMappedDecorator, $dateOptionFactory); + } +} From 4395fe0fe3f143c4430f436fdb25558f584f0a57 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Mar 2018 14:56:01 +0100 Subject: [PATCH 186/778] Do not contact parser test --- .../DoNotContact/DoNotContactPartsTest.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 app/bundles/LeadBundle/Tests/Segment/DoNotContact/DoNotContactPartsTest.php diff --git a/app/bundles/LeadBundle/Tests/Segment/DoNotContact/DoNotContactPartsTest.php b/app/bundles/LeadBundle/Tests/Segment/DoNotContact/DoNotContactPartsTest.php new file mode 100644 index 00000000000..70ec5e9eb67 --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/DoNotContact/DoNotContactPartsTest.php @@ -0,0 +1,72 @@ +assertSame('email', $doNotContactParts->getChannel()); + $this->assertSame( + DoNotContact::BOUNCED, + $doNotContactParts->getParameterType(), + 'Type for dnc_bounced should be bounced' + ); + + $field = 'dnc_unsubscribed'; + $doNotContactParts = new DoNotContactParts($field); + + $this->assertSame('email', $doNotContactParts->getChannel()); + $this->assertSame( + DoNotContact::UNSUBSCRIBED, + $doNotContactParts->getParameterType(), + 'Type for dnc_unsubscribed should be unsubscribed' + ); + } + + /** + * @covers \Mautic\LeadBundle\Segment\DoNotContact\DoNotContactParts::getChannel + * @covers \Mautic\LeadBundle\Segment\DoNotContact\DoNotContactParts::getParameterType + */ + public function testDncBouncedSms() + { + $field = 'dnc_bounced_sms'; + $doNotContactParts = new DoNotContactParts($field); + + $this->assertSame('sms', $doNotContactParts->getChannel()); + $this->assertSame( + DoNotContact::BOUNCED, + $doNotContactParts->getParameterType(), + 'Type for dnc_bounced_sms should be bounced' + ); + + $field = 'dnc_unsubscribed_sms'; + $doNotContactParts = new DoNotContactParts($field); + + $this->assertSame('sms', $doNotContactParts->getChannel()); + $this->assertSame( + DoNotContact::UNSUBSCRIBED, + $doNotContactParts->getParameterType(), + 'Type for dnc_unsubscribed_sms should be unsubscribed' + ); + } +} From 428e690c4261f0ce2cbfcdbb2090a887db36d299 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 15 Mar 2018 19:04:03 +0100 Subject: [PATCH 187/778] Tests fixed - correct process NOT LIKE and NOT REGEX --- .../Segment/Decorator/BaseDecorator.php | 4 +-- .../Segment/Decorator/BaseDecoratorTest.php | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index e431aa974f5..6c970429272 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -129,7 +129,7 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte switch ($contactSegmentFilterCrate->getOperator()) { case 'like': - case 'notLike': + case '!like': return strpos($filter, '%') === false ? '%'.$filter.'%' : $filter; case 'contains': return '%'.$filter.'%'; @@ -138,7 +138,7 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte case 'endsWith': return '%'.$filter; case 'regexp': - case 'notRegexp': + case '!regexp': return $this->prepareRegex($filter); } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php index c2525300cce..f7e4a93d5b9 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php @@ -251,6 +251,22 @@ public function testGetParameterValueLikeNoPercent() $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); } + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueNotLike() + { + $baseDecorator = $this->getDecorator(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => '!like', + 'filter' => 'Test string', + ]); + + $this->assertSame('%Test string%', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + /** * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue */ @@ -358,7 +374,23 @@ public function testGetParameterValueRegex() $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ 'type' => 'string', 'operator' => 'regexp', - 'filter' => 'Test \\s string', + 'filter' => 'Test \\\s string', + ]); + + $this->assertSame('Test \s string', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueNotRegex() + { + $baseDecorator = $this->getDecorator(); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'string', + 'operator' => '!regexp', + 'filter' => 'Test \\\s string', ]); $this->assertSame('Test \s string', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); From c7789145ab3e72ee50e6ee4ab95bdddec8cf02ea Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Mon, 19 Mar 2018 09:04:44 +0100 Subject: [PATCH 188/778] fix todo's --- .../Segment/ContactSegmentFilter.php | 2 - .../Segment/ContactSegmentService.php | 3 +- .../Segment/Decorator/DateDecorator.php | 4 +- .../Segment/Exception/InvalidUseException.php | 18 +++++ .../LeadBundle/Segment/OperatorOptions.php | 6 +- .../Query/ContactSegmentQueryBuilder.php | 50 +++---------- .../Query/Filter/BaseFilterQueryBuilder.php | 47 ++++-------- .../Filter/ForeignFuncFilterQueryBuilder.php | 73 ++++++------------- .../LeadBundle/Segment/Query/QueryBuilder.php | 39 +--------- 9 files changed, 69 insertions(+), 173 deletions(-) create mode 100644 app/bundles/LeadBundle/Segment/Exception/InvalidUseException.php diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index f61a061d39d..77f21b53a20 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -174,8 +174,6 @@ public function applyQuery(QueryBuilder $queryBuilder) /** * Whether the filter references another ContactSegment. * - * @TODO replace if not used - * * @return bool */ public function isContactSegmentReference() diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 76ec58bcf06..b500f5f1cb4 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -151,7 +151,8 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters * * @throws \Exception * - * @todo This is almost copy of getNewLeadListLeadsCount method. Only difference is that it calls getTotalSegmentContactsQuery + * @nottotodo This is almost copy of getNewLeadListLeadsCount method. Only difference is that it calls getTotalSegmentContactsQuery + * @answer Yes it is, it's just a facade */ public function getTotalLeadListLeadsCount(LeadList $segment) { diff --git a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php index 93e7e31be8c..eab30d1b01f 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/DateDecorator.php @@ -22,13 +22,11 @@ class DateDecorator extends CustomMappedDecorator /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * - * @TODO @petr please check this method - * * @throws \Exception */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { - throw new \Exception('Instance of Date option need to implement this function'); + throw new \Exception('Instance of Date option needs to implement this function'); } public function getDefaultDate() diff --git a/app/bundles/LeadBundle/Segment/Exception/InvalidUseException.php b/app/bundles/LeadBundle/Segment/Exception/InvalidUseException.php new file mode 100644 index 00000000000..2ec84916956 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Exception/InvalidUseException.php @@ -0,0 +1,18 @@ + 'mautic.lead.list.form.operator.between', 'expr' => 'between', //special case 'negate_expr' => 'notBetween', - // @TODO implement in list UI - 'hide' => true, + 'hide' => true, ], '!between' => [ 'label' => 'mautic.lead.list.form.operator.notbetween', 'expr' => 'notBetween', //special case 'negate_expr' => 'between', - // @TODO implement in list UI - 'hide' => true, + 'hide' => true, ], 'in' => [ 'label' => 'mautic.lead.list.form.operator.in', diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 882b75c8da0..69b0776997b 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -19,8 +19,6 @@ /** * Class ContactSegmentQueryBuilder is responsible for building queries for segments. - * - * @TODO add exceptions, remove related segments */ class ContactSegmentQueryBuilder { @@ -30,12 +28,6 @@ class ContactSegmentQueryBuilder /** @var RandomParameterName */ private $randomParameterName; - /** @var \Doctrine\DBAL\Schema\AbstractSchemaManager */ - private $schema; - - /** @var array */ - private $relatedSegments = []; - /** * ContactSegmentQueryBuilder constructor. * @@ -46,7 +38,6 @@ public function __construct(EntityManager $entityManager, RandomParameterName $r { $this->entityManager = $entityManager; $this->randomParameterName = $randomParameterName; - $this->schema = $this->entityManager->getConnection()->getSchemaManager(); } /** @@ -122,6 +113,7 @@ public function wrapInCount(QueryBuilder $qb) { // Add count functions to the query $queryBuilder = new QueryBuilder($this->entityManager->getConnection()); + // If there is any right join in the query we need to select its it $primary = $qb->guessPrimaryLeadContactIdColumn(); @@ -149,11 +141,13 @@ public function wrapInCount(QueryBuilder $qb) * * @param QueryBuilder $queryBuilder * @param $segmentId - * @param $whatever @TODO document this field + * @param $batchRestrictions * * @return QueryBuilder + * + * @throws QueryException */ - public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentId, $whatever) + public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentId, $batchRestrictions) { $parts = $queryBuilder->getQueryParts(); $setHaving = (count($parts['groupBy']) || !is_null($parts['having'])); @@ -164,7 +158,7 @@ public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentI $expression = $queryBuilder->expr()->andX( $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId), - $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$whatever['dateTime']."'") + $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$batchRestrictions['dateTime']."'") ); $queryBuilder->addJoinCondition($tableAlias, $expression); @@ -185,6 +179,8 @@ public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentI * @param $leadListId * * @return QueryBuilder + * + * @throws QueryException */ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadListId) { @@ -193,10 +189,6 @@ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadList 'l.id = '.$tableAlias.'.lead_id and '.$tableAlias.'.leadlist_id = '.intval($leadListId)); $queryBuilder->addJoinCondition($tableAlias, $queryBuilder->expr()->andX( -// $queryBuilder->expr()->orX( -// $queryBuilder->expr()->isNull($tableAlias.'.manually_removed'), -// $queryBuilder->expr()->eq($tableAlias.'.manually_removed', 0) -// ), $queryBuilder->expr()->eq($tableAlias.'.manually_added', 1) ) ); @@ -210,6 +202,8 @@ public function addManuallySubscribedQuery(QueryBuilder $queryBuilder, $leadList * @param $leadListId * * @return QueryBuilder + * + * @throws QueryException */ public function addManuallyUnsubscribedQuery(QueryBuilder $queryBuilder, $leadListId) { @@ -231,28 +225,4 @@ private function generateRandomParameterName() { return $this->randomParameterName->generateRandomParameterName(); } - - /** - * @return \Doctrine\DBAL\Schema\AbstractSchemaManager - * - * @TODO Remove this function - */ - public function getSchema() - { - return $this->schema; - } - - /** - * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema - * - * @return ContactSegmentQueryBuilder - * - * @TODO Remove this function - */ - public function setSchema($schema) - { - $this->schema = $schema; - - return $this; - } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 973b3c4c580..36dd97c877a 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; use Mautic\LeadBundle\Segment\ContactSegmentFilter; +use Mautic\LeadBundle\Segment\Exception\InvalidUseException; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -79,49 +80,29 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); - - switch ($filterOperator) { - case 'between': - case 'notBetween': - case 'notLike': - case 'notIn': - case 'empty': - case 'startsWith': - case 'gt': - case 'eq': - case 'neq': - case 'gte': - case 'like': - case 'lt': - case 'lte': - case 'in': - case 'regexp': - case 'notRegexp': - //@TODO this logic needs to - if ($filterAggr) { - $queryBuilder->leftJoin( + if ($filterAggr) { + if ($filter->getTable() != MAUTIC_TABLE_PREFIX.'leads') { + throw new InvalidUseException('You should use ForeignFuncFilterQueryBuilder instead.'); + } + $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); - } else { - if ($filter->getTable() == MAUTIC_TABLE_PREFIX.'companies') { - $relTable = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); - $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); - } else { - $queryBuilder->leftJoin( + } else { + if ($filter->getTable() == MAUTIC_TABLE_PREFIX.'companies') { + $relTable = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); + $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + } else { + $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); - } - } - break; - default: - throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + } } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 2e06fd6de18..06da7e65e54 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -67,44 +67,26 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); - - switch ($filterOperator) { - case 'notLike': - case 'notIn': - case 'startsWith': - case 'gt': - case 'eq': - case 'neq': - case 'gte': - case 'like': - case 'lt': - case 'lte': - case 'in': - //@TODO this logic needs to - if ($filterAggr) { - $queryBuilder->leftJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) - ); - } else { - if ($filter->getTable() == 'companies') { - $relTable = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); - $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); - } else { - $queryBuilder->leftJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) - ); - } - } - break; - default: - throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + if ($filterAggr) { + $queryBuilder->leftJoin( + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) + ); + } else { + if ($filter->getTable() == 'companies') { + $relTable = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); + $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + } else { + $queryBuilder->leftJoin( + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) + ); + } } } @@ -123,18 +105,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil ); $queryBuilder->setParameter($emptyParameter, ''); break; - case 'startsWith': - case 'endsWith': - case 'gt': - case 'eq': - case 'neq': - case 'gte': - case 'like': - case 'notLike': - case 'lt': - case 'lte': - case 'notIn': - case 'in': + default: if ($filterAggr) { $expression = $queryBuilder->expr()->$filterOperator( sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), @@ -147,8 +118,6 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil ); } break; - default: - throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } if ($queryBuilder->isJoinTable($filter->getTable())) { diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index a50ecfd9726..bed24441bc3 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1410,8 +1410,7 @@ public function getJoinCondition($alias) } /** - * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts - * @TODO Throwing QueryException - Functions calling this method do not handle exception, is it neccessary? + * Add AND condition to existing table alias. * * @param $alias * @param $expr @@ -1442,40 +1441,6 @@ public function addJoinCondition($alias, $expr) return $this; } - /** - * @TODO I need to rewrite it, it's no longer necessary like this, we have direct access to query parts - * @TODO This function seems not used at all - * @TODO Throwing QueryException - Functions calling this method do not handle exception, is it neccessary? - * - * @param $alias - * @param $expr - * - * @return $this - * - * @throws QueryException - */ - public function addOrJoinCondition($alias, $expr) - { - $result = $parts = $this->getQueryPart('join'); - - foreach ($parts as $tbl => $joins) { - foreach ($joins as $key => $join) { - if ($join['joinAlias'] == $alias) { - $result[$tbl][$key]['joinCondition'] = $this->expr()->orX($join['joinCondition'], $expr); - $inserted = true; - } - } - } - - if (!isset($inserted)) { - throw new QueryException('Inserting condition to nonexistent join '.$alias); - } - - $this->setQueryPart('join', $result); - - return $this; - } - /** * @param $alias * @param $expr @@ -1686,8 +1651,6 @@ public function addLogicStack($expression) * This function assembles correct logic for segment processing, this is to replace andWhere and orWhere (virtualy * as they need to be kept). * - * @TODO make this readable and explain - * * @param $expression * @param $glue * From 18bf23c9a22c478bf36068d82d88afc9abf540e8 Mon Sep 17 00:00:00 2001 From: Guillaume Dufour Date: Wed, 21 Mar 2018 15:55:53 +0100 Subject: [PATCH 189/778] remove the hidden-xs class from the DnC issue --- app/bundles/LeadBundle/Views/Lead/list.html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Views/Lead/list.html.php b/app/bundles/LeadBundle/Views/Lead/list.html.php index c6e97a3f45f..6cce2fb6514 100644 --- a/app/bundles/LeadBundle/Views/Lead/list.html.php +++ b/app/bundles/LeadBundle/Views/Lead/list.html.php @@ -61,7 +61,7 @@ ], [ 'attr' => [ - 'class' => 'hidden-xs btn btn-default btn-sm btn-nospin', + 'class' => 'btn btn-default btn-sm btn-nospin', 'data-toggle' => 'ajaxmodal', 'data-target' => '#MauticSharedModal', 'href' => $view['router']->path('mautic_contact_action', ['objectAction' => 'batchDnc']), From 932f86ed68e39f85ee1fdbb560062e7b2a770d43 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Wed, 21 Mar 2018 18:15:17 +0100 Subject: [PATCH 190/778] Init commit --- .../LeadBundle/Entity/LeadRepository.php | 3 + .../EventListener/SearchSubscriber.php | 77 ++++++++++++++++++- .../Translations/en_US/messages.ini | 3 + .../Views/Trackable/click_counts.html.php | 24 +++++- 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index b69f0e26334..7b3857f5cc0 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -919,6 +919,9 @@ public function getSearchCommands() 'mautic.lead.lead.searchcommand.email_read', 'mautic.lead.lead.searchcommand.email_queued', 'mautic.lead.lead.searchcommand.email_pending', + 'mautic.lead.lead.searchcommand.clicks_channel', + 'mautic.lead.lead.searchcommand.clicks_channel_id', + 'mautic.lead.lead.searchcommand.clicks_url', 'mautic.lead.lead.searchcommand.sms_sent', 'mautic.lead.lead.searchcommand.web_sent', 'mautic.lead.lead.searchcommand.mobile_sent', diff --git a/app/bundles/LeadBundle/EventListener/SearchSubscriber.php b/app/bundles/LeadBundle/EventListener/SearchSubscriber.php index 71a789afba5..ad96108ebea 100644 --- a/app/bundles/LeadBundle/EventListener/SearchSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/SearchSubscriber.php @@ -172,6 +172,20 @@ public function onBuildSearchCommands(LeadBuildSearchEvent $event) case $this->translator->trans('mautic.lead.lead.searchcommand.email_pending', [], null, 'en_US'): $this->buildEmailPendingQuery($event); break; + case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel'): + case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel', [], null, 'en_US'): + $this->buildClicksChannelQuery($event); + break; + + case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel_id'): + case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel_id', [], null, 'en_US'): + $this->buildClicksChannelIdQuery($event); + break; + case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_url'): + case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_url', [], null, 'en_US'): + die(print_r('test')); + $this->buildClicksUrlQuery($event); + break; case $this->translator->trans('mautic.lead.lead.searchcommand.sms_sent'): case $this->translator->trans('mautic.lead.lead.searchcommand.sms_sent', [], null, 'en_US'): $this->buildSmsSentQuery($event); @@ -234,6 +248,68 @@ private function buildEmailPendingQuery(LeadBuildSearchEvent $event) $this->buildJoinQuery($event, $tables, $config); } + /** + * @param LeadBuildSearchEvent $event + */ + private function buildClicksChannelQuery(LeadBuildSearchEvent $event) + { + $tables = [ + [ + 'from_alias' => 'l', + 'table' => 'page_hits', + 'alias' => 'ph_channel', + 'condition' => 'l.id = ph_channel.lead_id', + ], + ]; + + $config = [ + 'column' => 'ph_channel.source', + ]; + + $this->buildJoinQuery($event, $tables, $config); + } + + /** + * @param LeadBuildSearchEvent $event + */ + private function buildClicksChannelIdQuery(LeadBuildSearchEvent $event) + { + $tables = [ + [ + 'from_alias' => 'l', + 'table' => 'page_hits', + 'alias' => 'ph_id', + 'condition' => 'l.id = ph_id.lead_id', + ], + ]; + + $config = [ + 'column' => 'ph_id.source_id', + ]; + + $this->buildJoinQuery($event, $tables, $config); + } + + /** + * @param LeadBuildSearchEvent $event + */ + private function buildClicksUrlQuery(LeadBuildSearchEvent $event) + { + $tables = [ + [ + 'from_alias' => 'l', + 'table' => 'page_hits', + 'alias' => 'ph_url', + 'condition' => 'l.id = ph_url.lead_id', + ], + ]; + + $config = [ + 'column' => 'ph_url.url', + ]; + $this->buildJoinQuery($event, $tables, $config); + } + /** * @param LeadBuildSearchEvent $event */ @@ -396,7 +472,6 @@ private function buildJoinQuery(LeadBuildSearchEvent $event, array $tables, arra } $this->leadRepo->applySearchQueryRelationship($q, $tables, true, $expr); - $event->setReturnParameters(true); // replace search string $event->setStrict(true); // don't use like $event->setSearchStatus(true); // finish searching diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index fecf15da70e..d3a83c0f522 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -299,6 +299,9 @@ mautic.lead.lead.searchcommand.email_sent="email_sent" mautic.lead.lead.searchcommand.email_read="email_read" mautic.lead.lead.searchcommand.email_queued="email_queued" mautic.lead.lead.searchcommand.email_pending="email_pending" +mautic.lead.lead.searchcommand.clicks_channel="clicks_channel" +mautic.lead.lead.searchcommand.clicks_channel_id="clicks_channel_id" +mautic.lead.lead.searchcommand.clicks_url="clicks_url" mautic.lead.lead.searchcommand.mobile_sent="mobile_sent" mautic.lead.lead.searchcommand.web_sent="web_sent" mautic.lead.lead.searchcommand.sms_sent="sms_sent" diff --git a/app/bundles/PageBundle/Views/Trackable/click_counts.html.php b/app/bundles/PageBundle/Views/Trackable/click_counts.html.php index d0beeb7443c..563ddc3d6bd 100644 --- a/app/bundles/PageBundle/Views/Trackable/click_counts.html.php +++ b/app/bundles/PageBundle/Views/Trackable/click_counts.html.php @@ -14,13 +14,23 @@ $totalClicks = 0; $totalUniqueClicks = 0; foreach ($trackables as $link): + die(print_r($link)); $totalClicks += $link['hits']; $totalUniqueClicks += $link['unique_hits']; ?> - + + + + + + @@ -28,7 +38,17 @@ trans('mautic.trackable.total_clicks'); ?> - + + + + + + + From 3e06c00a2879617fe0d07c7e5d73d6efce5a4803 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Wed, 21 Mar 2018 22:42:39 +0100 Subject: [PATCH 191/778] Final version --- .../Translations/en_US/messages.ini | 1 + .../EmailBundle/Views/Email/details.html.php | 5 ++- .../LeadBundle/Entity/LeadRepository.php | 28 ++++++------ .../EventListener/SearchSubscriber.php | 43 +++++++++---------- .../Translations/en_US/messages.ini | 6 +-- .../PageBundle/Entity/TrackableRepository.php | 2 +- .../Views/Trackable/click_counts.html.php | 9 ++-- 7 files changed, 48 insertions(+), 46 deletions(-) diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index ab93166edd8..5b5f369c7b0 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -421,4 +421,5 @@ mautic.email.campaign.event.reply="Replies to email" mautic.email.campaign.event.reply_descr="Trigger action when contact replies to an email" mautic.email.config.monitored_email.reply_folder="Contact Replies" mautic.email.stat.tooltip="Details may not match summary numbers if the contact no longer exists in your Mautic Account or if a contact was sent or read an email multiple times" +mautic.email.stat.simple.tooltip="Details may not match summary numbers if the contact no longer exists in your Mautic Account" mautic.email.associated.contacts="Contacts" diff --git a/app/bundles/EmailBundle/Views/Email/details.html.php b/app/bundles/EmailBundle/Views/Email/details.html.php index 7e0860d4f6e..581183e08ec 100644 --- a/app/bundles/EmailBundle/Views/Email/details.html.php +++ b/app/bundles/EmailBundle/Views/Email/details.html.php @@ -259,7 +259,10 @@
- render('MauticPageBundle:Trackable:click_counts.html.php', ['trackables' => $trackables]); ?> + render('MauticPageBundle:Trackable:click_counts.html.php', [ + 'trackables' => $trackables, + 'email' => $email, + ]); ?>
diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index 7b3857f5cc0..e0ac24582db 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -919,9 +919,9 @@ public function getSearchCommands() 'mautic.lead.lead.searchcommand.email_read', 'mautic.lead.lead.searchcommand.email_queued', 'mautic.lead.lead.searchcommand.email_pending', - 'mautic.lead.lead.searchcommand.clicks_channel', - 'mautic.lead.lead.searchcommand.clicks_channel_id', - 'mautic.lead.lead.searchcommand.clicks_url', + 'mautic.lead.lead.searchcommand.page_source', + 'mautic.lead.lead.searchcommand.page_source_id', + 'mautic.lead.lead.searchcommand.page_id', 'mautic.lead.lead.searchcommand.sms_sent', 'mautic.lead.lead.searchcommand.web_sent', 'mautic.lead.lead.searchcommand.mobile_sent', @@ -1087,26 +1087,26 @@ public function applySearchQueryRelationship(QueryBuilder $q, array $tables, $in $this->useDistinctCount = true; $joins = $q->getQueryPart('join'); - if (!array_key_exists($primaryTable['alias'], $joins)) { + if (!preg_match('/"'.preg_quote($primaryTable['alias'], '/').'"/i', json_encode($joins))) { $q->$joinType( $primaryTable['from_alias'], MAUTIC_TABLE_PREFIX.$primaryTable['table'], $primaryTable['alias'], $primaryTable['condition'] ); - foreach ($tables as $table) { - $q->$joinType($table['from_alias'], MAUTIC_TABLE_PREFIX.$table['table'], $table['alias'], $table['condition']); - } + } + foreach ($tables as $table) { + $q->$joinType($table['from_alias'], MAUTIC_TABLE_PREFIX.$table['table'], $table['alias'], $table['condition']); + } - if ($whereExpression) { - $q->andWhere($whereExpression); - } + if ($whereExpression) { + $q->andWhere($whereExpression); + } - if ($having) { - $q->andHaving($having); - } - $q->groupBy('l.id'); + if ($having) { + $q->andHaving($having); } + $q->groupBy('l.id'); } /** diff --git a/app/bundles/LeadBundle/EventListener/SearchSubscriber.php b/app/bundles/LeadBundle/EventListener/SearchSubscriber.php index ad96108ebea..7fb5db232e2 100644 --- a/app/bundles/LeadBundle/EventListener/SearchSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/SearchSubscriber.php @@ -172,19 +172,18 @@ public function onBuildSearchCommands(LeadBuildSearchEvent $event) case $this->translator->trans('mautic.lead.lead.searchcommand.email_pending', [], null, 'en_US'): $this->buildEmailPendingQuery($event); break; - case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel'): - case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel', [], null, 'en_US'): - $this->buildClicksChannelQuery($event); + case $this->translator->trans('mautic.lead.lead.searchcommand.page_source'): + case $this->translator->trans('mautic.lead.lead.searchcommand.page_source', [], null, 'en_US'): + $this->buildPageHitSourceQuery($event); break; - case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel_id'): - case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_channel_id', [], null, 'en_US'): - $this->buildClicksChannelIdQuery($event); + case $this->translator->trans('mautic.lead.lead.searchcommand.page_source_id'): + case $this->translator->trans('mautic.lead.lead.searchcommand.page_source_id', [], null, 'en_US'): + $this->buildPageHitSourceIdQuery($event); break; - case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_url'): - case $this->translator->trans('mautic.lead.lead.searchcommand.clicks_url', [], null, 'en_US'): - die(print_r('test')); - $this->buildClicksUrlQuery($event); + case $this->translator->trans('mautic.lead.lead.searchcommand.page_id'): + case $this->translator->trans('mautic.lead.lead.searchcommand.page_id', [], null, 'en_US'): + $this->buildPageHitIdQuery($event); break; case $this->translator->trans('mautic.lead.lead.searchcommand.sms_sent'): case $this->translator->trans('mautic.lead.lead.searchcommand.sms_sent', [], null, 'en_US'): @@ -251,19 +250,19 @@ private function buildEmailPendingQuery(LeadBuildSearchEvent $event) /** * @param LeadBuildSearchEvent $event */ - private function buildClicksChannelQuery(LeadBuildSearchEvent $event) + private function buildPageHitSourceQuery(LeadBuildSearchEvent $event) { $tables = [ [ 'from_alias' => 'l', 'table' => 'page_hits', - 'alias' => 'ph_channel', - 'condition' => 'l.id = ph_channel.lead_id', + 'alias' => 'ph', + 'condition' => 'l.id = ph.lead_id', ], ]; $config = [ - 'column' => 'ph_channel.source', + 'column' => 'ph.source', ]; $this->buildJoinQuery($event, $tables, $config); @@ -272,19 +271,19 @@ private function buildClicksChannelQuery(LeadBuildSearchEvent $event) /** * @param LeadBuildSearchEvent $event */ - private function buildClicksChannelIdQuery(LeadBuildSearchEvent $event) + private function buildPageHitSourceIdQuery(LeadBuildSearchEvent $event) { $tables = [ [ 'from_alias' => 'l', 'table' => 'page_hits', - 'alias' => 'ph_id', - 'condition' => 'l.id = ph_id.lead_id', + 'alias' => 'ph', + 'condition' => 'l.id = ph.lead_id', ], ]; $config = [ - 'column' => 'ph_id.source_id', + 'column' => 'ph.source_id', ]; $this->buildJoinQuery($event, $tables, $config); @@ -293,19 +292,19 @@ private function buildClicksChannelIdQuery(LeadBuildSearchEvent $event) /** * @param LeadBuildSearchEvent $event */ - private function buildClicksUrlQuery(LeadBuildSearchEvent $event) + private function buildPageHitIdQuery(LeadBuildSearchEvent $event) { $tables = [ [ 'from_alias' => 'l', 'table' => 'page_hits', - 'alias' => 'ph_url', - 'condition' => 'l.id = ph_url.lead_id', + 'alias' => 'ph', + 'condition' => 'l.id = ph.lead_id', ], ]; $config = [ - 'column' => 'ph_url.url', + 'column' => 'ph.redirect_id', ]; $this->buildJoinQuery($event, $tables, $config); } diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index d3a83c0f522..3679e3366e3 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -299,9 +299,9 @@ mautic.lead.lead.searchcommand.email_sent="email_sent" mautic.lead.lead.searchcommand.email_read="email_read" mautic.lead.lead.searchcommand.email_queued="email_queued" mautic.lead.lead.searchcommand.email_pending="email_pending" -mautic.lead.lead.searchcommand.clicks_channel="clicks_channel" -mautic.lead.lead.searchcommand.clicks_channel_id="clicks_channel_id" -mautic.lead.lead.searchcommand.clicks_url="clicks_url" +mautic.lead.lead.searchcommand.page_source="page_source" +mautic.lead.lead.searchcommand.page_source_id="page_source_id" +mautic.lead.lead.searchcommand.page_id="page_id" mautic.lead.lead.searchcommand.mobile_sent="mobile_sent" mautic.lead.lead.searchcommand.web_sent="web_sent" mautic.lead.lead.searchcommand.sms_sent="sms_sent" diff --git a/app/bundles/PageBundle/Entity/TrackableRepository.php b/app/bundles/PageBundle/Entity/TrackableRepository.php index 2d7fe173224..1a6d0cf8168 100644 --- a/app/bundles/PageBundle/Entity/TrackableRepository.php +++ b/app/bundles/PageBundle/Entity/TrackableRepository.php @@ -32,7 +32,7 @@ public function findByChannel($channel, $channelId) $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); $tableAlias = $this->getTableAlias(); - return $q->select('r.redirect_id, r.url, '.$tableAlias.'.hits, '.$tableAlias.'.unique_hits') + return $q->select('r.redirect_id, r.url, r.id, '.$tableAlias.'.hits, '.$tableAlias.'.unique_hits') ->from(MAUTIC_TABLE_PREFIX.'page_redirects', 'r') ->innerJoin('r', MAUTIC_TABLE_PREFIX.'channel_url_trackables', $tableAlias, $q->expr()->andX( diff --git a/app/bundles/PageBundle/Views/Trackable/click_counts.html.php b/app/bundles/PageBundle/Views/Trackable/click_counts.html.php index 563ddc3d6bd..bd0db3cc22f 100644 --- a/app/bundles/PageBundle/Views/Trackable/click_counts.html.php +++ b/app/bundles/PageBundle/Views/Trackable/click_counts.html.php @@ -14,7 +14,6 @@ $totalClicks = 0; $totalUniqueClicks = 0; foreach ($trackables as $link): - die(print_r($link)); $totalClicks += $link['hits']; $totalUniqueClicks += $link['unique_hits']; ?> @@ -25,9 +24,9 @@ + title="trans('mautic.email.stat.simple.tooltip'); ?>"> @@ -42,9 +41,9 @@ + title="trans('mautic.email.stat.simple.tooltip'); ?>"> From 9317b25c92dc5095de417d72fc4ef5d3041bc98c Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Wed, 21 Mar 2018 22:56:45 +0100 Subject: [PATCH 192/778] Minor --- app/bundles/LeadBundle/EventListener/SearchSubscriber.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/bundles/LeadBundle/EventListener/SearchSubscriber.php b/app/bundles/LeadBundle/EventListener/SearchSubscriber.php index 7fb5db232e2..19bce6a328b 100644 --- a/app/bundles/LeadBundle/EventListener/SearchSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/SearchSubscriber.php @@ -471,6 +471,7 @@ private function buildJoinQuery(LeadBuildSearchEvent $event, array $tables, arra } $this->leadRepo->applySearchQueryRelationship($q, $tables, true, $expr); + $event->setReturnParameters(true); // replace search string $event->setStrict(true); // don't use like $event->setSearchStatus(true); // finish searching From 0800938068fcabbbfbef2ffc93d27e1b3bf90324 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Fri, 23 Mar 2018 08:29:13 +0100 Subject: [PATCH 193/778] Extend click_counts.html.php template --- .../EmailBundle/Views/Email/details.html.php | 5 +++-- .../Views/Trackable/click_counts.html.php | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/Views/Email/details.html.php b/app/bundles/EmailBundle/Views/Email/details.html.php index 581183e08ec..643be76b74f 100644 --- a/app/bundles/EmailBundle/Views/Email/details.html.php +++ b/app/bundles/EmailBundle/Views/Email/details.html.php @@ -260,8 +260,9 @@
render('MauticPageBundle:Trackable:click_counts.html.php', [ - 'trackables' => $trackables, - 'email' => $email, + 'trackables' => $trackables, + 'entity' => $email, + 'channel' => 'email', ]); ?>
diff --git a/app/bundles/PageBundle/Views/Trackable/click_counts.html.php b/app/bundles/PageBundle/Views/Trackable/click_counts.html.php index bd0db3cc22f..57ecc214bc3 100644 --- a/app/bundles/PageBundle/Views/Trackable/click_counts.html.php +++ b/app/bundles/PageBundle/Views/Trackable/click_counts.html.php @@ -22,13 +22,17 @@ + + + + @@ -39,13 +43,17 @@ - + + + + + From c57e766910e0aa92fdcaa1cbe80f839639497bfa Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Fri, 23 Mar 2018 08:36:05 +0100 Subject: [PATCH 194/778] Extend clickable click stats to all channels (sms, notification, dynamicContent) --- .../Views/DynamicContent/details.html.php | 7 ++++++- .../Views/MobileNotification/details.html.php | 5 ++++- .../NotificationBundle/Views/Notification/details.html.php | 5 ++++- app/bundles/SmsBundle/Views/Sms/details.html.php | 5 ++++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/bundles/DynamicContentBundle/Views/DynamicContent/details.html.php b/app/bundles/DynamicContentBundle/Views/DynamicContent/details.html.php index 5d756b1a22f..a3a58b643bf 100644 --- a/app/bundles/DynamicContentBundle/Views/DynamicContent/details.html.php +++ b/app/bundles/DynamicContentBundle/Views/DynamicContent/details.html.php @@ -190,7 +190,12 @@
- render('MauticPageBundle:Trackable:click_counts.html.php', ['trackables' => $trackables]); ?> + render('MauticPageBundle:Trackable:click_counts.html.php', [ + 'trackables' => $trackables, + 'entity' => $entity, + 'channel' => 'dynamicContent', + ]); ?> +
diff --git a/app/bundles/NotificationBundle/Views/MobileNotification/details.html.php b/app/bundles/NotificationBundle/Views/MobileNotification/details.html.php index cb6f853db67..16fc638775e 100644 --- a/app/bundles/NotificationBundle/Views/MobileNotification/details.html.php +++ b/app/bundles/NotificationBundle/Views/MobileNotification/details.html.php @@ -106,7 +106,10 @@
- render('MauticPageBundle:Trackable:click_counts.html.php', ['trackables' => $trackables]); ?> + render('MauticPageBundle:Trackable:click_counts.html.php', [ + 'trackables' => $trackables, + 'entity' => $notification, + 'channel' => 'notification', ]); ?>
diff --git a/app/bundles/NotificationBundle/Views/Notification/details.html.php b/app/bundles/NotificationBundle/Views/Notification/details.html.php index c3b650d4d94..ed12e9097c0 100644 --- a/app/bundles/NotificationBundle/Views/Notification/details.html.php +++ b/app/bundles/NotificationBundle/Views/Notification/details.html.php @@ -106,7 +106,10 @@
- render('MauticPageBundle:Trackable:click_counts.html.php', ['trackables' => $trackables]); ?> + render('MauticPageBundle:Trackable:click_counts.html.php', [ + 'trackables' => $trackables, + 'entity' => $notification, + 'channel' => 'notification', ]); ?>
diff --git a/app/bundles/SmsBundle/Views/Sms/details.html.php b/app/bundles/SmsBundle/Views/Sms/details.html.php index 6b150f12cba..05997cba2f8 100644 --- a/app/bundles/SmsBundle/Views/Sms/details.html.php +++ b/app/bundles/SmsBundle/Views/Sms/details.html.php @@ -140,7 +140,10 @@
- render('MauticPageBundle:Trackable:click_counts.html.php', ['trackables' => $trackables]); ?> + render('MauticPageBundle:Trackable:click_counts.html.php', [ + 'trackables' => $trackables, + 'entity' => $sms, + 'channel' => 'sms', ]); ?>
From 15b27a9b055e8f292624064cf2920874516cd5f6 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 26 Mar 2018 17:35:25 +0200 Subject: [PATCH 195/778] Minor to translate --- app/bundles/SmsBundle/Translations/en_US/messages.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/SmsBundle/Translations/en_US/messages.ini b/app/bundles/SmsBundle/Translations/en_US/messages.ini index c38c82bc346..297c273ccc5 100644 --- a/app/bundles/SmsBundle/Translations/en_US/messages.ini +++ b/app/bundles/SmsBundle/Translations/en_US/messages.ini @@ -13,8 +13,8 @@ mautic.sms.config.form.sms.password="Auth Token" mautic.sms.config.form.sms.password.tooltip="Twilio Auth Token" mautic.sms.config.form.sms.sending_phone_number="Sending Phone Number" mautic.sms.config.form.sms.sending_phone_number.tooltip="The phone number given by your provider that you use to send and receive Text Message messages." -mautic.sms.config.form.sms.disable_trackable_urls="Disable trackable urls" -mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option disable trackable Urls and your SMS click stats will not work." +mautic.sms.config.form.sms.disable_trackable_urls="Disable trackable URLs" +mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option disable trackable URLs and your SMS click stats will not work." mautic.sms.sms="Text Message" mautic.sms.smses="Text Messages" From 64913d25572f4e44272c0d29698c2eeb98ab6be7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 26 Mar 2018 15:58:59 -0500 Subject: [PATCH 196/778] Updated the language file --- app/bundles/SmsBundle/Translations/en_US/messages.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/SmsBundle/Translations/en_US/messages.ini b/app/bundles/SmsBundle/Translations/en_US/messages.ini index 297c273ccc5..bfafe2966bb 100644 --- a/app/bundles/SmsBundle/Translations/en_US/messages.ini +++ b/app/bundles/SmsBundle/Translations/en_US/messages.ini @@ -13,8 +13,8 @@ mautic.sms.config.form.sms.password="Auth Token" mautic.sms.config.form.sms.password.tooltip="Twilio Auth Token" mautic.sms.config.form.sms.sending_phone_number="Sending Phone Number" mautic.sms.config.form.sms.sending_phone_number.tooltip="The phone number given by your provider that you use to send and receive Text Message messages." -mautic.sms.config.form.sms.disable_trackable_urls="Disable trackable URLs" -mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option disable trackable URLs and your SMS click stats will not work." +mautic.sms.config.form.sms.disable_trackable_urls="Disable click tracking" +mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option will disable click tracking for URLs in the text message." mautic.sms.sms="Text Message" mautic.sms.smses="Text Messages" From a046d7221c9726f34bae6e3c74d64ffe9ad2da00 Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Wed, 28 Mar 2018 10:06:46 -0400 Subject: [PATCH 197/778] defining inset previewUrl variable in form.php --- app/bundles/EmailBundle/Views/Email/form.html.php | 4 ++++ app/bundles/PageBundle/Views/Page/form.html.php | 4 ++++ media/js/app.js | 5 +++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/Views/Email/form.html.php b/app/bundles/EmailBundle/Views/Email/form.html.php index be8d8a28eb9..bb506d879a0 100644 --- a/app/bundles/EmailBundle/Views/Email/form.html.php +++ b/app/bundles/EmailBundle/Views/Email/form.html.php @@ -62,6 +62,10 @@ $isCodeMode = ($email->getTemplate() === 'mautic_code_mode'); +if (!isset($previewUrl)) { + $previewUrl = ''; +} + ?> start($form, ['attr' => $attr]); ?> diff --git a/app/bundles/PageBundle/Views/Page/form.html.php b/app/bundles/PageBundle/Views/Page/form.html.php index 16155129477..d6c4f50af49 100644 --- a/app/bundles/PageBundle/Views/Page/form.html.php +++ b/app/bundles/PageBundle/Views/Page/form.html.php @@ -37,6 +37,10 @@ $isCodeMode = ($activePage->getTemplate() === 'mautic_code_mode'); +if (!isset($previewUrl)) { + $previewUrl = ''; +} + ?> start($form, ['attr' => $attr]); ?> diff --git a/media/js/app.js b/media/js/app.js index ca4e3fc12fc..5de9c1b363e 100644 --- a/media/js/app.js +++ b/media/js/app.js @@ -492,7 +492,7 @@ prototype=mQuery(prototype);if(fieldObject=='company'){prototype.find('.object-i var filterBase="emailform[dynamicContent]["+dynamicContentIndex+"][filters]["+dynamicContentFilterIndex+"][filters]["+filterNum+"]";var filterIdBase="emailform_dynamicContent_"+dynamicContentIndex+"_filters_"+dynamicContentFilterIndex+"_filters_"+filterNum;if(isSpecial){var templateField=fieldType;if(fieldType=='boolean'||fieldType=='multiselect'){templateField='select';} var template=mQuery('#templates .'+templateField+'-template').clone();var $template=mQuery(template);var templateNameAttr=$template.attr('name').replace(/__name__/g,filterNum).replace(/__dynamicContentIndex__/g,dynamicContentIndex).replace(/__dynamicContentFilterIndex__/g,dynamicContentFilterIndex);var templateIdAttr=$template.attr('id').replace(/__name__/g,filterNum).replace(/__dynamicContentIndex__/g,dynamicContentIndex).replace(/__dynamicContentFilterIndex__/g,dynamicContentFilterIndex);$template.attr('name',templateNameAttr);$template.attr('id',templateIdAttr);prototype.find('input[name="'+filterBase+'[filter]"]').replaceWith(template);} if(activeDynamicContentFilterContainer.find('.panel').length==0){prototype.find(".panel-footer").addClass('hide');} -prototype.find("input[name='"+filterBase+"[field]']").val(selectedFilter);prototype.find("input[name='"+filterBase+"[type]']").val(fieldType);prototype.find("input[name='"+filterBase+"[object]']").val(fieldObject);var filterEl=(isSpecial)?"select[name='"+filterBase+"[filter]']":"input[name='"+filterBase+"[filter]']";activeDynamicContentFilterContainer.append(prototype);Mautic.initRemoveEvents(activeDynamicContentFilterContainer.find("a.remove-selected"),mQuery);var filter='#'+filterIdBase+'_filter';var fieldOptions=fieldCallback='';if(isSpecial){if(fieldType=='select'||fieldType=='boolean'||fieldType=='multiselect'){fieldOptions=selectedOption.data("field-list");mQuery.each(fieldOptions,function(index,val){mQuery('");} if(mQuery(filterId+'_chosen').length){mQuery(filterId).chosen('destroy');} -mQuery(filterId).attr('data-placeholder',placeholder);Mautic.activateChosenSelect(mQuery(filterId));}};Mautic.updateLookupListFilter=function(field,datum){if(datum&&datum.id){var filterField='#'+field.replace('_display','_filter');mQuery(filterField).val(datum.id);}};Mautic.activateSegmentFilterTypeahead=function(displayId,filterId,fieldOptions){mQuery('#'+displayId).attr('data-lookup-callback','updateLookupListFilter');Mautic.activateFieldTypeahead(displayId,filterId,[],'lead:fieldList')};Mautic.addLeadListFilter=function(elId){var filterId='#available_'+elId;var label=mQuery(filterId).text();var filterNum=parseInt(mQuery('.available-filters').data('index'));mQuery('.available-filters').data('index',filterNum+1);var prototype=mQuery('.available-filters').data('prototype');var fieldType=mQuery(filterId).data('field-type');var fieldObject=mQuery(filterId).data('field-object');var isSpecial=(mQuery.inArray(fieldType,['leadlist','device_type','device_brand','device_os','lead_email_received','lead_email_sent','tags','multiselect','boolean','select','country','timezone','region','stage','locale','globalcategory'])!=-1);prototype=prototype.replace(/__name__/g,filterNum);prototype=prototype.replace(/__label__/g,label);prototype=mQuery(prototype);var prefix='leadlist';var parent=mQuery(filterId).parents('.dynamic-content-filter, .dwc-filter');if(parent.length){prefix=parent.attr('id');} +mQuery(filterId).attr('data-placeholder',placeholder);Mautic.activateChosenSelect(mQuery(filterId));}};Mautic.updateLookupListFilter=function(field,datum){if(datum&&datum.id){var filterField='#'+field.replace('_display','_filter');mQuery(filterField).val(datum.id);}};Mautic.activateSegmentFilterTypeahead=function(displayId,filterId,fieldOptions,mQueryObject){if(typeof mQueryObject=='function'){mQuery=mQueryObject;} +mQuery('#'+displayId).attr('data-lookup-callback','updateLookupListFilter');Mautic.activateFieldTypeahead(displayId,filterId,[],'lead:fieldList')};Mautic.addLeadListFilter=function(elId){var filterId='#available_'+elId;var label=mQuery(filterId).text();var filterNum=parseInt(mQuery('.available-filters').data('index'));mQuery('.available-filters').data('index',filterNum+1);var prototype=mQuery('.available-filters').data('prototype');var fieldType=mQuery(filterId).data('field-type');var fieldObject=mQuery(filterId).data('field-object');var isSpecial=(mQuery.inArray(fieldType,['leadlist','device_type','device_brand','device_os','lead_email_received','lead_email_sent','tags','multiselect','boolean','select','country','timezone','region','stage','locale','globalcategory'])!=-1);prototype=prototype.replace(/__name__/g,filterNum);prototype=prototype.replace(/__label__/g,label);prototype=mQuery(prototype);var prefix='leadlist';var parent=mQuery(filterId).parents('.dynamic-content-filter, .dwc-filter');if(parent.length){prefix=parent.attr('id');} var filterBase=prefix+"[filters]["+filterNum+"]";var filterIdBase=prefix+"_filters_"+filterNum+"_";if(isSpecial){var templateField=fieldType;if(fieldType=='boolean'||fieldType=='multiselect'){templateField='select';} var template=mQuery('#templates .'+templateField+'-template').clone();mQuery(template).attr('name',mQuery(template).attr('name').replace(/__name__/g,filterNum));mQuery(template).attr('id',mQuery(template).attr('id').replace(/__name__/g,filterNum));mQuery(prototype).find('input[name="'+filterBase+'[filter]"]').replaceWith(template);} if(mQuery('#'+prefix+'_filters div.panel').length==0){mQuery(prototype).find(".panel-heading").addClass('hide');} From 6e1ba249818eda2416cf7dcfe2f107dd283537fc Mon Sep 17 00:00:00 2001 From: heathdutton Date: Wed, 28 Mar 2018 13:21:20 -0400 Subject: [PATCH 198/778] Prevent null field selection. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For the “Form field value” condition screen. --- .../Form/Type/CampaignEventFormFieldValueType.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php b/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php index f68822d34b6..c42761002af 100644 --- a/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php +++ b/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php @@ -123,6 +123,12 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'onchange' => 'Mautic.updateFormFieldValues(this)', 'data-field-options' => json_encode($options), ], + 'required' => true, + 'constraints' => [ + new NotBlank( + ['message' => 'mautic.core.value.required'] + ), + ], ] ); From 393c8d2b62c3162863aa5a4ff5059563f13c874f Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Thu, 29 Mar 2018 14:00:43 -0400 Subject: [PATCH 199/778] ran cs-fixer --- .../filemanager/connectors/php/filemanager.class.php | 4 ++-- app/bundles/CoreBundle/Menu/MenuHelper.php | 2 +- .../MonitoredEmail/Processor/Bounce/BodyParser.php | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/connectors/php/filemanager.class.php b/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/connectors/php/filemanager.class.php index 55b8901f244..71625974a0a 100644 --- a/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/connectors/php/filemanager.class.php +++ b/app/bundles/CoreBundle/Assets/js/libraries/ckeditor/filemanager/connectors/php/filemanager.class.php @@ -476,7 +476,7 @@ public function move() // dynamic fileroot dir must be used when enabled if ($this->dynamic_fileroot != '') { $rootDir = $this->dynamic_fileroot; - //$rootDir = str_replace($_SERVER['DOCUMENT_ROOT'], '', $this->path_to_files); // instruction could replace the line above + //$rootDir = str_replace($_SERVER['DOCUMENT_ROOT'], '', $this->path_to_files); // instruction could replace the line above } else { $rootDir = $this->get['root']; } @@ -1090,7 +1090,7 @@ private function get_file_info($path = '', $thumbnail = false) $this->item['properties']['Height'] = $height; $this->item['properties']['Width'] = $width; $this->item['properties']['Size'] = filesize($this->getFullPath($current_path)); - //} + //} } elseif (file_exists($this->root.$this->config['icons']['path'].strtolower($this->item['filetype']).'.png')) { $this->item['preview'] = $this->config['icons']['path'].strtolower($this->item['filetype']).'.png'; $this->item['properties']['Size'] = filesize($this->getFullPath($current_path)); diff --git a/app/bundles/CoreBundle/Menu/MenuHelper.php b/app/bundles/CoreBundle/Menu/MenuHelper.php index 2cda6f122e0..e5315876638 100644 --- a/app/bundles/CoreBundle/Menu/MenuHelper.php +++ b/app/bundles/CoreBundle/Menu/MenuHelper.php @@ -149,7 +149,7 @@ public function createMenuStructure(&$items, $depth = 0, $defaultPriority = 9999 unset($items[$k]); - // Don't set a default priority here as it'll assume that of it's parent + // Don't set a default priority here as it'll assume that of it's parent } elseif (!isset($i['priority'])) { // Ensure a priority for non-orphans $i['priority'] = $defaultPriority; diff --git a/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/BodyParser.php b/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/BodyParser.php index fd5d1d8dc82..3c957ca1580 100644 --- a/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/BodyParser.php +++ b/app/bundles/EmailBundle/MonitoredEmail/Processor/Bounce/BodyParser.php @@ -420,12 +420,12 @@ public function parse($body, $knownEmail = '') $result['rule_no'] = '0166'; $result['email'] = $match[1]; - /* - * rule: mailbox full; - * sample: - * name@domain.com - * Delay reason: LMTP error after end of data: 452 4.2.2 Mailbox is full / Blocks limit exceeded / Inode limit exceeded - */ + /* + * rule: mailbox full; + * sample: + * name@domain.com + * Delay reason: LMTP error after end of data: 452 4.2.2 Mailbox is full / Blocks limit exceeded / Inode limit exceeded + */ } elseif (preg_match("/\s<(\S+@\S+\w)>\sMailbox.*full/i", $body, $match)) { $result['rule_cat'] = Category::FULL; $result['rule_no'] = '0166'; From 92466500b0870e43a26d288f57f07049c29b626d Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Wed, 11 Apr 2018 09:15:28 +0200 Subject: [PATCH 200/778] Revert token replace and still disable trackable urls --- .../SmsBundle/EventListener/SmsSubscriber.php | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/app/bundles/SmsBundle/EventListener/SmsSubscriber.php b/app/bundles/SmsBundle/EventListener/SmsSubscriber.php index aaac89b579f..669d3291740 100644 --- a/app/bundles/SmsBundle/EventListener/SmsSubscriber.php +++ b/app/bundles/SmsBundle/EventListener/SmsSubscriber.php @@ -132,11 +132,6 @@ public function onDelete(SmsEvent $event) */ public function onTokenReplacement(TokenReplacementEvent $event) { - // Disable trackable urls - if ($this->smsHelper->getDisableTrackableUrls()) { - return; - } - /** @var Lead $lead */ $lead = $event->getLead(); $content = $event->getContent(); @@ -149,19 +144,20 @@ public function onTokenReplacement(TokenReplacementEvent $event) $this->assetTokenHelper->findAssetTokens($content, $clickthrough) ); - list($content, $trackables) = $this->trackableModel->parseContentForTrackables( - $content, - $tokens, - 'sms', - $clickthrough['channel'][1] - ); - - /** - * @var string - * @var Trackable $trackable - */ - foreach ($trackables as $token => $trackable) { - $tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, $clickthrough, true); + // Disable trackable urls + if (!$this->smsHelper->getDisableTrackableUrls()) { + list($content, $trackables) = $this->trackableModel->parseContentForTrackables( + $content, + $tokens, + 'sms', + $clickthrough['channel'][1] + ); + /** + * @var Trackable + */ + foreach ($trackables as $token => $trackable) { + $tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, $clickthrough, true); + } } $content = str_replace(array_keys($tokens), array_values($tokens), $content); From e482ab33fb8d0564cfa0f90a12d5b01d513318e4 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Wed, 11 Apr 2018 09:18:09 +0200 Subject: [PATCH 201/778] revert comments --- app/bundles/SmsBundle/EventListener/SmsSubscriber.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/SmsBundle/EventListener/SmsSubscriber.php b/app/bundles/SmsBundle/EventListener/SmsSubscriber.php index 669d3291740..79188581f24 100644 --- a/app/bundles/SmsBundle/EventListener/SmsSubscriber.php +++ b/app/bundles/SmsBundle/EventListener/SmsSubscriber.php @@ -153,7 +153,8 @@ public function onTokenReplacement(TokenReplacementEvent $event) $clickthrough['channel'][1] ); /** - * @var Trackable + * @var string + * @var Trackable $trackable */ foreach ($trackables as $token => $trackable) { $tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, $clickthrough, true); From 104b841eb5cdd038c9634f1ee6891cdc87af86f1 Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Wed, 11 Apr 2018 09:42:49 -0400 Subject: [PATCH 202/778] removing preview section if preview doesn't exist --- .../CoreBundle/Views/Helper/builder.html.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app/bundles/CoreBundle/Views/Helper/builder.html.php b/app/bundles/CoreBundle/Views/Helper/builder.html.php index 1ed0d73ee1c..7da58a68979 100644 --- a/app/bundles/CoreBundle/Views/Helper/builder.html.php +++ b/app/bundles/CoreBundle/Views/Helper/builder.html.php @@ -38,25 +38,27 @@ trans('mautic.core.code.mode.token.dropdown.hint'); ?>
-
-
-

trans('mautic.email.urlvariant'); ?>

-
-
-
-
- - - - + +
+
+

trans('mautic.email.urlvariant'); ?>

+
+
+
+
+ + + + +
-
+

trans('mautic.core.slot.types'); ?>

From ace97b87329c38286d5bdff67b79f50ff1bf0caa Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Wed, 11 Apr 2018 09:46:23 -0400 Subject: [PATCH 203/778] adding preview section to code mode --- .../CoreBundle/Views/Helper/builder.html.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/bundles/CoreBundle/Views/Helper/builder.html.php b/app/bundles/CoreBundle/Views/Helper/builder.html.php index 7da58a68979..e2eb7151f9d 100644 --- a/app/bundles/CoreBundle/Views/Helper/builder.html.php +++ b/app/bundles/CoreBundle/Views/Helper/builder.html.php @@ -33,32 +33,32 @@
+ +
+
+

trans('mautic.email.urlvariant'); ?>

+
+
+
+
+ + + + +
+
+
+
+
trans('mautic.core.code.mode.token.dropdown.hint'); ?>
- -
-
-

trans('mautic.email.urlvariant'); ?>

-
-
-
-
- - - - -
-
-
-
-

trans('mautic.core.slot.types'); ?>

From ffd6ede62a26ac3b0b98dfb5868a72311b8f32b8 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 20:14:31 +0200 Subject: [PATCH 204/778] Fixing issues found with PHPSTAN l=0 --- .../DashboardBundle/Controller/Api/WidgetApiController.php | 6 +++--- app/bundles/DashboardBundle/Event/WidgetDetailEvent.php | 2 +- app/bundles/DashboardBundle/Form/Type/WidgetType.php | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php index e36ca5aa363..c4f845a1f23 100644 --- a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php +++ b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php @@ -80,12 +80,12 @@ public function getDataAction($type) 'dateFormat' => InputHelper::clean($this->request->get('dateFormat', null)), 'dateFrom' => $fromDate, 'dateTo' => $toDate, - 'limit' => InputHelper::int($this->request->get('limit', null)), + 'limit' => (int) $this->request->get('limit', null), 'filter' => $this->request->get('filter', []), ]; - $cacheTimeout = InputHelper::int($this->request->get('cacheTimeout', null)); - $widgetHeight = InputHelper::int($this->request->get('height', 300)); + $cacheTimeout = (int) $this->request->get('cacheTimeout', null); + $widgetHeight = (int) $this->request->get('height', 300); $widget = new Widget(); $widget->setParams($params); diff --git a/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php b/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php index 0c0509ec9f7..d5bf9ecfa72 100644 --- a/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php +++ b/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php @@ -254,7 +254,7 @@ public function isCached() /** * Get the Translator object. * - * @return Translator $translator + * @return TranslatorInterface */ public function getTranslator() { diff --git a/app/bundles/DashboardBundle/Form/Type/WidgetType.php b/app/bundles/DashboardBundle/Form/Type/WidgetType.php index e418ede9494..29ac1086e2b 100644 --- a/app/bundles/DashboardBundle/Form/Type/WidgetType.php +++ b/app/bundles/DashboardBundle/Form/Type/WidgetType.php @@ -94,10 +94,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ]); - $ff = $builder->getFormFactory(); - // function to add a form for specific widget type dynamically - $func = function (FormEvent $e) use ($ff, $dispatcher) { + $func = function (FormEvent $e) use ($dispatcher) { $data = $e->getData(); $form = $e->getForm(); $event = new WidgetFormEvent(); From 6e80a5ea571bf37dcc2b09f9b1640780ffa9f2ee Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 20:16:57 +0200 Subject: [PATCH 205/778] Adding DashboardBundle to PHPSTAN Travis check --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5417a572512..11ff8e7f1b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ script: - bin/phpunit -d memory_limit=2048M --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist --fail-on-warning # Run PHPSTAN analysis for PHP 7+ - - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/DashboardBundle app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi # Check if the code standards weren't broken. # Run it only on PHP 7.1 which should be the fastest. No need to run it for all PHP versions From 9133814a09f56318e29989539b5dd7160eafa2b9 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 21:08:04 +0200 Subject: [PATCH 206/778] Removing getParameters which was not called anywhere and if it would have been then it's calling getBase64EncodedFields which does not exist --- .../ConfigBundle/Event/ConfigBuilderEvent.php | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php b/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php index 8a917373083..2eaa5266f53 100644 --- a/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php +++ b/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php @@ -115,37 +115,6 @@ public function getFormThemes() return $this->formThemes; } - /** - * Helper method can load $parameters array from a config file. - * - * @param string $path (relative from the root dir) - * - * @return array - */ - public function getParameters($path = null) - { - $paramsFile = $this->pathsHelper->getSystemPath('app').$path; - - if (file_exists($paramsFile)) { - // Import the bundle configuration, $parameters is defined in this file - include $paramsFile; - } - - if (!isset($parameters)) { - $parameters = []; - } - - $fields = $this->getBase64EncodedFields(); - $checkThese = array_intersect(array_keys($parameters), $fields); - foreach ($checkThese as $checkMe) { - if (!empty($parameters[$checkMe])) { - $parameters[$checkMe] = base64_decode($parameters[$checkMe]); - } - } - - return $parameters; - } - /** * @param $bundle * From 306e15390c24931975aa9f39e4309ad737122d8b Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 21:08:53 +0200 Subject: [PATCH 207/778] Fixing PHPSTAN l 0 for ConfigBundle --- app/bundles/ConfigBundle/Controller/SysinfoController.php | 1 + app/bundles/ConfigBundle/Form/Type/ConfigType.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/ConfigBundle/Controller/SysinfoController.php b/app/bundles/ConfigBundle/Controller/SysinfoController.php index 58147b599c5..e049bb6433e 100644 --- a/app/bundles/ConfigBundle/Controller/SysinfoController.php +++ b/app/bundles/ConfigBundle/Controller/SysinfoController.php @@ -12,6 +12,7 @@ namespace Mautic\ConfigBundle\Controller; use Mautic\CoreBundle\Controller\FormController; +use Symfony\Component\HttpFoundation\JsonResponse; /** * Class SysinfoController. diff --git a/app/bundles/ConfigBundle/Form/Type/ConfigType.php b/app/bundles/ConfigBundle/Form/Type/ConfigType.php index 1f78e4dfcf8..19078299dec 100644 --- a/app/bundles/ConfigBundle/Form/Type/ConfigType.php +++ b/app/bundles/ConfigBundle/Form/Type/ConfigType.php @@ -62,7 +62,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $builder->addEventListener( FormEvents::PRE_SET_DATA, - function (FormEvent $event) use ($options) { + function (FormEvent $event) { $form = $event->getForm(); foreach ($form as $config => $configForm) { From 968b6f887315af7d11947204c36626713b286b42 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Fri, 20 Apr 2018 20:22:01 +0200 Subject: [PATCH 208/778] Removing duplicated attr param from an array --- app/bundles/DashboardBundle/Form/Type/WidgetType.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/bundles/DashboardBundle/Form/Type/WidgetType.php b/app/bundles/DashboardBundle/Form/Type/WidgetType.php index 29ac1086e2b..c94c33f1c4f 100644 --- a/app/bundles/DashboardBundle/Form/Type/WidgetType.php +++ b/app/bundles/DashboardBundle/Form/Type/WidgetType.php @@ -57,7 +57,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label' => 'mautic.dashboard.widget.form.type', 'choices' => $event->getTypes(), 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], 'empty_value' => 'mautic.core.select', 'attr' => [ 'class' => 'form-control', From f6ac747ee019300598ae5541cf341e9b2707b9e4 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Fri, 20 Apr 2018 20:29:56 +0200 Subject: [PATCH 209/778] Adding ConfigBundle to the PHPSTAN Travis check --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 11ff8e7f1b8..264a0de5488 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ script: - bin/phpunit -d memory_limit=2048M --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist --fail-on-warning # Run PHPSTAN analysis for PHP 7+ - - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/DashboardBundle app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/DashboardBundle app/bundles/ConfigBundle app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi # Check if the code standards weren't broken. # Run it only on PHP 7.1 which should be the fastest. No need to run it for all PHP versions From d3ae6373b632fd1de730b6ad0366689a2179db9d Mon Sep 17 00:00:00 2001 From: enguerr Date: Mon, 23 Apr 2018 17:02:11 +0200 Subject: [PATCH 210/778] resolve bug #5759 --- app/bundles/ApiBundle/Controller/CommonApiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/ApiBundle/Controller/CommonApiController.php b/app/bundles/ApiBundle/Controller/CommonApiController.php index a8e888002f7..2ceecc132ff 100644 --- a/app/bundles/ApiBundle/Controller/CommonApiController.php +++ b/app/bundles/ApiBundle/Controller/CommonApiController.php @@ -377,7 +377,7 @@ public function getEntitiesAction() if ($this->security->checkPermissionExists($this->permissionBase.':viewother') && !$this->security->isGranted($this->permissionBase.':viewother') ) { - $this->listFilters = [ + $this->listFilters[] = [ 'column' => $tableAlias.'.createdBy', 'expr' => 'eq', 'value' => $this->user->getId(), From 9fa37e126df1d36e99febd274934200291ce5d4e Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 23 Apr 2018 22:36:13 +0200 Subject: [PATCH 211/778] Close from edit to view controller --- .../LeadBundle/Controller/ListController.php | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index cecbdee4724..ff41c1661ab 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -268,7 +268,7 @@ public function cloneAction($objectId, $ignorePost = false) */ public function editAction($objectId, $ignorePost = false) { - $postActionVars = $this->getPostActionVars(); + $postActionVars = $this->getPostActionVars($objectId); try { $segment = $this->getSegment($objectId); @@ -418,20 +418,30 @@ private function getSegment($segmentId) /** * Get variables for POST action. * + * @param null $objectId + * * @return array */ - private function getPostActionVars() + private function getPostActionVars($objectId = null) { //set the page we came from $page = $this->get('session')->get('mautic.segment.page', 1); //set the return URL - $returnUrl = $this->generateUrl('mautic_segment_index', ['page' => $page]); + if ($objectId != null) { + $returnUrl = $this->generateUrl('mautic_segment_action', ['objectAction' => 'view', 'objectId'=> $objectId]); + $viewParameters = ['objectAction' => 'view', 'objectId'=> $objectId]; + $contentTemplate = 'MauticLeadBundle:List:view'; + } else { + $returnUrl = $this->generateUrl('mautic_segment_index', ['page' => $page]); + $viewParameters = ['page' => $page]; + $contentTemplate = 'MauticLeadBundle:List:index'; + } return [ 'returnUrl' => $returnUrl, - 'viewParameters' => ['page' => $page], - 'contentTemplate' => 'MauticLeadBundle:List:index', + 'viewParameters' => $viewParameters, + 'contentTemplate' => $contentTemplate, 'passthroughVars' => [ 'activeLink' => '#mautic_segment_index', 'mauticContent' => 'leadlist', From 20b6aae2bf741dabc1507528874f2c1ffcbc3ba2 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 23 Apr 2018 22:40:55 +0200 Subject: [PATCH 212/778] $page moved to right place --- app/bundles/LeadBundle/Controller/ListController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index ff41c1661ab..17e31990234 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -424,15 +424,14 @@ private function getSegment($segmentId) */ private function getPostActionVars($objectId = null) { - //set the page we came from - $page = $this->get('session')->get('mautic.segment.page', 1); - //set the return URL if ($objectId != null) { $returnUrl = $this->generateUrl('mautic_segment_action', ['objectAction' => 'view', 'objectId'=> $objectId]); $viewParameters = ['objectAction' => 'view', 'objectId'=> $objectId]; $contentTemplate = 'MauticLeadBundle:List:view'; } else { + //set the page we came from + $page = $this->get('session')->get('mautic.segment.page', 1); $returnUrl = $this->generateUrl('mautic_segment_index', ['page' => $page]); $viewParameters = ['page' => $page]; $contentTemplate = 'MauticLeadBundle:List:index'; From 3075b467663d11d190a3e9be6be1970f98e38261 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 23 Apr 2018 22:42:45 +0200 Subject: [PATCH 213/778] Minor --- app/bundles/LeadBundle/Controller/ListController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 17e31990234..c4823e3ab83 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -425,7 +425,7 @@ private function getSegment($segmentId) private function getPostActionVars($objectId = null) { //set the return URL - if ($objectId != null) { + if ($objectId) { $returnUrl = $this->generateUrl('mautic_segment_action', ['objectAction' => 'view', 'objectId'=> $objectId]); $viewParameters = ['objectAction' => 'view', 'objectId'=> $objectId]; $contentTemplate = 'MauticLeadBundle:List:view'; From 897ca0e2f4f3598d80c3218b87e18a5533f13e3d Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 24 Apr 2018 12:40:27 +0200 Subject: [PATCH 214/778] Allow filtering contacts by UTM data for segments --- app/bundles/LeadBundle/Model/ListModel.php | 40 +++++++++++++++++++ .../ContactSegmentFilterDictionary.php | 25 ++++++++++++ .../Translations/en_US/messages.ini | 5 +++ 3 files changed, 70 insertions(+) diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 5cf4f094bda..3a5f2fe10a1 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -676,6 +676,46 @@ public function getChoiceFields() 'operators' => $this->getOperatorsForFieldType('multiselect'), 'object' => 'lead', ], + 'utm_campaign' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmcampaign'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_content' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmcontent'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_medium' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmmedium'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_source' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmsource'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_term' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmterm'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], ]; // Add custom choices diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index 9f0c08d38ab..b6e8997066b 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -192,6 +192,31 @@ public function __construct() 'type' => SessionsFilterQueryBuilder::getServiceId(), ]; + $this->translations['utm_campaign'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_utmtags', + ]; + + $this->translations['utm_content'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_utmtags', + ]; + + $this->translations['utm_medium'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_utmtags', + ]; + + $this->translations['utm_source'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_utmtags', + ]; + + $this->translations['utm_term'] = [ + 'type' => ForeignValueFilterQueryBuilder::getServiceId(), + 'foreign_table' => 'lead_utmtags', + ]; + parent::__construct($this->translations); } } diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index fecf15da70e..5aea329b3c8 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -641,6 +641,11 @@ mautic.lead.lead.events.changecompanyscore="Add to company's score" mautic.lead.lead.events.changecompanyscore_descr="This action will add the specified value to the company's existing score" mautic.lead.timeline.displaying_events_for_contact="for contact: %contact% (%id%)" mautic.lead.list.filter.categories="Subscribed Categories" +mautic.lead.list.filter.utmcampaign="UTM Campaign" +mautic.lead.list.filter.utmcontent="UTM Content" +mautic.lead.list.filter.utmmedium="UTM Medium" +mautic.lead.list.filter.utmsource="UTM Source" +mautic.lead.list.filter.utmterm="UTM Term" mautic.lead.audit.created="The contact was created." mautic.lead.audit.deleted="The contact was deleted." mautic.lead.audit.updated="The contact was updated." From cbe32813ea59b4d9079c2a2a7e8147aadf510eaa Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 24 Apr 2018 18:35:45 +0200 Subject: [PATCH 215/778] CompanyController had duplicated code - removed and was adding fields that started on 'field_' only for some reason. --- .../Controller/CompanyController.php | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/bundles/LeadBundle/Controller/CompanyController.php b/app/bundles/LeadBundle/Controller/CompanyController.php index 6e4bb5f0ff0..29096c7b01f 100644 --- a/app/bundles/LeadBundle/Controller/CompanyController.php +++ b/app/bundles/LeadBundle/Controller/CompanyController.php @@ -351,24 +351,12 @@ public function editAction($objectId, $ignorePost = false) $data = $this->request->request->get('company'); //pull the data from the form in order to apply the form's formatting foreach ($form as $f) { - $name = $f->getName(); - if (strpos($name, 'field_') === 0) { - $data[$name] = $f->getData(); - } - } - $model->setFieldValues($entity, $data, true); - //form is valid so process the data - $data = $this->request->request->get('company'); - - //pull the data from the form in order to apply the form's formatting - foreach ($form as $f) { - $name = $f->getName(); - if (strpos($name, 'field_') === 0) { - $data[$name] = $f->getData(); - } + $data[$f->getName()] = $f->getData(); } $model->setFieldValues($entity, $data, true); + + //form is valid so process the data $model->saveEntity($entity, $form->get('buttons')->get('save')->isClicked()); $this->addFlash( From 6c5bcf1e1ba943c0a5c3847a1a0f0f10a74d2401 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 24 Apr 2018 17:48:27 -0500 Subject: [PATCH 216/778] Dev version bump --- app/AppKernel.php | 4 ++-- app/version.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/AppKernel.php b/app/AppKernel.php index 67ab9ae1981..3e396c3e28a 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -41,7 +41,7 @@ class AppKernel extends Kernel * * @const integer */ - const PATCH_VERSION = 1; + const PATCH_VERSION = 2; /** * Extra version identifier. @@ -51,7 +51,7 @@ class AppKernel extends Kernel * * @const string */ - const EXTRA_VERSION = ''; + const EXTRA_VERSION = '-dev'; /** * @var array diff --git a/app/version.txt b/app/version.txt index 94f15e9cc30..1e462c61d86 100644 --- a/app/version.txt +++ b/app/version.txt @@ -1 +1 @@ -2.13.1 +2.13.2-dev From ca76a16476cbb9165a0996d2a6972233fbfe8289 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Wed, 25 Apr 2018 09:39:26 +0200 Subject: [PATCH 217/778] Make pre-comit hook executable --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 47a38b95556..8b9d5fd7b84 100644 --- a/composer.json +++ b/composer.json @@ -138,12 +138,14 @@ "post-install-cmd": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", - "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"" + "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" ], "post-update-cmd": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", - "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"" + "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" ], "test": "bin/phpunit --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist" }, From 82c2bd8685367a8a86b57594368803ff1abf35ab Mon Sep 17 00:00:00 2001 From: John Linhart Date: Wed, 25 Apr 2018 12:22:18 +0200 Subject: [PATCH 218/778] Path and permission fixed for the pre-commit hook permission fix --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8b9d5fd7b84..6519cf78b5b 100644 --- a/composer.json +++ b/composer.json @@ -139,13 +139,13 @@ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", - "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./.git/hooks/pre-commit',0755);}\"" ], "post-update-cmd": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", - "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./.git/hooks/pre-commit',0755);}\"" ], "test": "bin/phpunit --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist" }, From 59081d326ea0e0727bd39a5c1071e6bad678274c Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:49:17 +0200 Subject: [PATCH 219/778] Support syncList form in Focus --- .../FormBundle/Views/Field/field_helper.php | 1 - .../FormBundle/Views/Field/select.html.php | 1 - plugins/MauticFocusBundle/Config/config.php | 1 + .../MauticFocusBundle/Model/FocusModel.php | 20 ++++++++++++++----- .../Views/Builder/form.html.php | 13 ++++++------ 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 0c5c3c04110..983718ed92b 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -114,7 +114,6 @@ } $appendAttribute($containerAttr, 'class', $defaultContainerClass); - // Setup list parsing if (isset($list) || isset($properties['syncList']) || isset($properties['list']) || isset($properties['optionlist'])) { $parseList = []; diff --git a/app/bundles/FormBundle/Views/Field/select.html.php b/app/bundles/FormBundle/Views/Field/select.html.php index d4594da0218..f94a004f908 100644 --- a/app/bundles/FormBundle/Views/Field/select.html.php +++ b/app/bundles/FormBundle/Views/Field/select.html.php @@ -13,7 +13,6 @@ $containerType = 'select'; include __DIR__.'/field_helper.php'; - if (!empty($properties['multiple'])) { $inputAttr .= ' multiple="multiple"'; } diff --git a/plugins/MauticFocusBundle/Config/config.php b/plugins/MauticFocusBundle/Config/config.php index be561bfdb20..e69b9dca1ba 100644 --- a/plugins/MauticFocusBundle/Config/config.php +++ b/plugins/MauticFocusBundle/Config/config.php @@ -143,6 +143,7 @@ 'mautic.helper.templating', 'event_dispatcher', 'mautic.lead.model.lead', + 'mautic.lead.model.field', ], ], ], diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 17b823f20b0..5d4f28a9e10 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -17,6 +17,7 @@ use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\TemplatingHelper; use Mautic\CoreBundle\Model\FormModel; +use Mautic\LeadBundle\Model\FieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use MauticPlugin\MauticFocusBundle\Entity\Focus; @@ -56,6 +57,11 @@ class FocusModel extends FormModel */ protected $leadModel; + /** + * @var FieldModel + */ + protected $leadFieldModel; + /** * FocusModel constructor. * @@ -64,14 +70,16 @@ class FocusModel extends FormModel * @param TemplatingHelper $templating * @param EventDispatcherInterface $dispatcher * @param LeadModel $leadModel + * @param FieldModel $leadFieldModel */ - public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel) + public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel, FieldModel $leadFieldModel) { $this->formModel = $formModel; $this->trackableModel = $trackableModel; $this->templating = $templating; $this->dispatcher = $dispatcher; $this->leadModel = $leadModel; + $this->leadFieldModel = $leadFieldModel; } /** @@ -266,10 +274,12 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, + 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), + 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), ] ) : ''; diff --git a/plugins/MauticFocusBundle/Views/Builder/form.html.php b/plugins/MauticFocusBundle/Views/Builder/form.html.php index 327ad27bb6b..806aae27abf 100644 --- a/plugins/MauticFocusBundle/Views/Builder/form.html.php +++ b/plugins/MauticFocusBundle/Views/Builder/form.html.php @@ -25,7 +25,7 @@ render('MauticFormBundle:Builder:script.html.php', ['form' => $form, 'formName' => $formName]); ?> - EXTRA; echo $view->render('MauticFormBundle:Builder:form.html.php', [ - 'form' => $form, - 'formExtra' => $formExtra, - 'action' => ($preview) ? '#' : null, - 'suffix' => '_focus', + 'form' => $form, + 'formExtra' => $formExtra, + 'action' => ($preview) ? '#' : null, + 'suffix' => '_focus', + 'contactFields' => $contactFields, + 'companyFields' => $companyFields, ] ); ?> From e30224c33ab7ab4f43ecd83de5588cebc938afe7 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:54:51 +0200 Subject: [PATCH 220/778] R --- .../FormBundle/Views/Field/field_helper.php | 2 +- commands.php | 80 +++++ progress.json | 1 + tester.php | 324 ++++++++++++++++++ 4 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 commands.php create mode 100644 progress.json create mode 100644 tester.php diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 983718ed92b..1e186bc5f49 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -80,7 +80,7 @@ if ($field['inputAttributes']) { $inputAttr .= ' '.htmlspecialchars_decode($field['inputAttributes']); - } + } $appendAttribute($inputAttr, 'class', $defaultInputClass); } diff --git a/commands.php b/commands.php new file mode 100644 index 00000000000..0fe70b623b7 --- /dev/null +++ b/commands.php @@ -0,0 +1,80 @@ +'; + echo '

Specify what task to run. You can run these:'; + echo '

    '; + foreach ($allowedTasks as $task) { + $href = $link . '&task=' . urlencode($task); + echo '
  • ' . $task . '
  • '; + } + echo '

Read more'; + echo '
Please, backup your database before executing the doctrine commands!

'; + die; +} + +$task = urldecode($_GET['task']); +if (!in_array($task, $allowedTasks)) { + echo 'Task ' . $task . ' is not allowed.'; + die; +} +$fullCommand = explode(' ', $task); +$command = $fullCommand[0]; +$argsCount = count($fullCommand) - 1; +$args = array('console', $command); +if ($argsCount) { + for ($i = 1; $i <= $argsCount; $i++) { + $args[] = $fullCommand[$i]; + } +} +echo ''; +echo '

Executing ' . implode(' ', $args) . '

'; + +require_once __DIR__.'/app/autoload.php'; +// require_once __DIR__.'/app/bootstrap.php.cache'; +require_once __DIR__.'/app/AppKernel.php'; +require __DIR__.'/vendor/autoload.php'; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\BufferedOutput; + +defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); +try { + $input = new ArgvInput($args); + $output = new BufferedOutput(); + $kernel = new AppKernel('prod', false); + $app = new Application($kernel); + $app->setAutoExit(false); + $result = $app->run($input, $output); + echo "
\n".$output->fetch().'
'; +} catch (\Exception $exception) { + echo $exception->getMessage(); +} \ No newline at end of file diff --git a/progress.json b/progress.json new file mode 100644 index 00000000000..b2422db8246 --- /dev/null +++ b/progress.json @@ -0,0 +1 @@ +{"progress":100} \ No newline at end of file diff --git a/tester.php b/tester.php new file mode 100644 index 00000000000..b1864131306 --- /dev/null +++ b/tester.php @@ -0,0 +1,324 @@ +testerfilename = basename(__FILE__); + $allowedTasks = array( + 'apply', + 'remove' + ); + + + + $task = isset($_REQUEST['task']) ? $_REQUEST['task'] : ''; + $patch = isset($_REQUEST['patch']) ? $_REQUEST['patch'] : ''; + $cmd = isset($_REQUEST['cmd']) ? $_REQUEST['cmd'] : ''; + + // Controller + if (in_array($task, $allowedTasks)) { + @set_time_limit(9999); + if(empty($cmd)) { + $cmd = []; + } + $count = 2 + count($cmd); + $counter = 1; + file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); + $this->$task($patch); + foreach ($cmd as $c => $value) { + $counter++; + file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); + $this->executeCommand($c); + } + file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>100])); + } + else + { + $this->start(); + file_put_contents(__DIR__ . '/progress.json', null); + } + } + + function apply($patch = null) + { + if ($patch) { + $patchUrl = $this->url.$patch.'.patch'; + echo $patchUrl; + $result = exec('curl ' . $patchUrl . ' | git apply'); + print_R($result); + } else { + echo 'Apply with no Patch ID'; + } + } + + function remove($patch = null) + { + if ($patch) { + $patchUrl = $this->url.$patch.'.patch'; + $result = exec('curl ' . $patchUrl . ' | git apply -R'); + + return $result; + } else { + echo 'Could not remove Patch'; + } + } + + function executeCommand($cmd){ + + // cache clear by remove dir + if($cmd == "cache:clear"){ + return exec('rm -r app/cache/prod/'); + } + + $fullCommand = explode(' ', $cmd); + $command = $fullCommand[0]; + $argsCount = count($fullCommand) - 1; + $args = array('console', $command); + if ($argsCount) { + for ($i = 1; $i <= $argsCount; $i++) { + $args[] = $fullCommand[$i]; + } + } + defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); + try { + $input = new ArgvInput($args); + $output = new BufferedOutput(); + $kernel = new AppKernel('prod', false); + $app = new Application($kernel); + $app->setAutoExit(false); + $result = $app->run($input, $output); + echo "
\n".$output->fetch().'
'; + } catch (\Exception $exception) { + echo $exception->getMessage(); + } + } + + + // View + function start() + { ?> + + + + + Mautic Patch Tester + + + + + + +
+ + + localfile))) + { + $dir_class = "alert alert-danger"; + $msg = "

This path is not writable. Please change permissions before continuing.

"; + // $continue = "disabled"; + } + else + { + $dir_class = "alert alert-secondary"; + $msg = ""; + $continue = ""; + } + ?> + + + +
+

Start Testing!

+

This app will allow you to immediately begin testing pull requests against your Mautic installation. You will need to make sure this file is in the root of your Mautic test instance.

+ +
Current Path
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+

Apply Pull Request

+
+
+
+ + + e.g. Simply enter 3456 for PR #3456 +
+
+
Run after apply pull request
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+

Remove Pull Request

+
+
+
+ + + e.g. Simply enter 3456 for PR #3456 +
+
+
Run after remove pull request
+
+ +
+
+
+
+ +
+ + +
+
+
+ +
+
+
+
*This app does not yet take into account any pull requests that require database changes.
+ + +
+ + + + + + + + + Date: Mon, 9 Apr 2018 15:56:05 +0200 Subject: [PATCH 221/778] Revert --- app/bundles/FormBundle/Views/Field/field_helper.php | 2 +- app/bundles/FormBundle/Views/Field/select.html.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 1e186bc5f49..983718ed92b 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -80,7 +80,7 @@ if ($field['inputAttributes']) { $inputAttr .= ' '.htmlspecialchars_decode($field['inputAttributes']); - } + } $appendAttribute($inputAttr, 'class', $defaultInputClass); } diff --git a/app/bundles/FormBundle/Views/Field/select.html.php b/app/bundles/FormBundle/Views/Field/select.html.php index f94a004f908..d4594da0218 100644 --- a/app/bundles/FormBundle/Views/Field/select.html.php +++ b/app/bundles/FormBundle/Views/Field/select.html.php @@ -13,6 +13,7 @@ $containerType = 'select'; include __DIR__.'/field_helper.php'; + if (!empty($properties['multiple'])) { $inputAttr .= ' multiple="multiple"'; } From cb673eaef4bafe964123370370c9ec83c246208e Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:57:03 +0200 Subject: [PATCH 222/778] Revert "Support syncList form in Focus" This reverts commit 4cf84d6e1ca94374399a10849589b3f25bb52085. --- .../FormBundle/Views/Field/field_helper.php | 1 + plugins/MauticFocusBundle/Config/config.php | 1 - .../MauticFocusBundle/Model/FocusModel.php | 20 +++++-------------- .../Views/Builder/form.html.php | 13 ++++++------ 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 983718ed92b..0c5c3c04110 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -114,6 +114,7 @@ } $appendAttribute($containerAttr, 'class', $defaultContainerClass); + // Setup list parsing if (isset($list) || isset($properties['syncList']) || isset($properties['list']) || isset($properties['optionlist'])) { $parseList = []; diff --git a/plugins/MauticFocusBundle/Config/config.php b/plugins/MauticFocusBundle/Config/config.php index e69b9dca1ba..be561bfdb20 100644 --- a/plugins/MauticFocusBundle/Config/config.php +++ b/plugins/MauticFocusBundle/Config/config.php @@ -143,7 +143,6 @@ 'mautic.helper.templating', 'event_dispatcher', 'mautic.lead.model.lead', - 'mautic.lead.model.field', ], ], ], diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 5d4f28a9e10..17b823f20b0 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -17,7 +17,6 @@ use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\TemplatingHelper; use Mautic\CoreBundle\Model\FormModel; -use Mautic\LeadBundle\Model\FieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use MauticPlugin\MauticFocusBundle\Entity\Focus; @@ -57,11 +56,6 @@ class FocusModel extends FormModel */ protected $leadModel; - /** - * @var FieldModel - */ - protected $leadFieldModel; - /** * FocusModel constructor. * @@ -70,16 +64,14 @@ class FocusModel extends FormModel * @param TemplatingHelper $templating * @param EventDispatcherInterface $dispatcher * @param LeadModel $leadModel - * @param FieldModel $leadFieldModel */ - public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel, FieldModel $leadFieldModel) + public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel) { $this->formModel = $formModel; $this->trackableModel = $trackableModel; $this->templating = $templating; $this->dispatcher = $dispatcher; $this->leadModel = $leadModel; - $this->leadFieldModel = $leadFieldModel; } /** @@ -274,12 +266,10 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, - 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), - 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, ] ) : ''; diff --git a/plugins/MauticFocusBundle/Views/Builder/form.html.php b/plugins/MauticFocusBundle/Views/Builder/form.html.php index 806aae27abf..327ad27bb6b 100644 --- a/plugins/MauticFocusBundle/Views/Builder/form.html.php +++ b/plugins/MauticFocusBundle/Views/Builder/form.html.php @@ -25,7 +25,7 @@ render('MauticFormBundle:Builder:script.html.php', ['form' => $form, 'formName' => $formName]); ?> + EXTRA; echo $view->render('MauticFormBundle:Builder:form.html.php', [ - 'form' => $form, - 'formExtra' => $formExtra, - 'action' => ($preview) ? '#' : null, - 'suffix' => '_focus', - 'contactFields' => $contactFields, - 'companyFields' => $companyFields, + 'form' => $form, + 'formExtra' => $formExtra, + 'action' => ($preview) ? '#' : null, + 'suffix' => '_focus', ] ); ?> From 49ddfdefbf833af4ddf9d0a51eb1349cb573f9ee Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:58:57 +0200 Subject: [PATCH 223/778] Revert "R" This reverts commit 4e18bb0c42445ead58f3849abdba6ac708368fc4. --- commands.php | 80 ------------- progress.json | 1 - tester.php | 324 -------------------------------------------------- 3 files changed, 405 deletions(-) delete mode 100644 commands.php delete mode 100644 progress.json delete mode 100644 tester.php diff --git a/commands.php b/commands.php deleted file mode 100644 index 0fe70b623b7..00000000000 --- a/commands.php +++ /dev/null @@ -1,80 +0,0 @@ -'; - echo '

Specify what task to run. You can run these:'; - echo '

    '; - foreach ($allowedTasks as $task) { - $href = $link . '&task=' . urlencode($task); - echo '
  • ' . $task . '
  • '; - } - echo '

Read more'; - echo '
Please, backup your database before executing the doctrine commands!

'; - die; -} - -$task = urldecode($_GET['task']); -if (!in_array($task, $allowedTasks)) { - echo 'Task ' . $task . ' is not allowed.'; - die; -} -$fullCommand = explode(' ', $task); -$command = $fullCommand[0]; -$argsCount = count($fullCommand) - 1; -$args = array('console', $command); -if ($argsCount) { - for ($i = 1; $i <= $argsCount; $i++) { - $args[] = $fullCommand[$i]; - } -} -echo ''; -echo '

Executing ' . implode(' ', $args) . '

'; - -require_once __DIR__.'/app/autoload.php'; -// require_once __DIR__.'/app/bootstrap.php.cache'; -require_once __DIR__.'/app/AppKernel.php'; -require __DIR__.'/vendor/autoload.php'; -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Output\BufferedOutput; - -defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); -try { - $input = new ArgvInput($args); - $output = new BufferedOutput(); - $kernel = new AppKernel('prod', false); - $app = new Application($kernel); - $app->setAutoExit(false); - $result = $app->run($input, $output); - echo "
\n".$output->fetch().'
'; -} catch (\Exception $exception) { - echo $exception->getMessage(); -} \ No newline at end of file diff --git a/progress.json b/progress.json deleted file mode 100644 index b2422db8246..00000000000 --- a/progress.json +++ /dev/null @@ -1 +0,0 @@ -{"progress":100} \ No newline at end of file diff --git a/tester.php b/tester.php deleted file mode 100644 index b1864131306..00000000000 --- a/tester.php +++ /dev/null @@ -1,324 +0,0 @@ -testerfilename = basename(__FILE__); - $allowedTasks = array( - 'apply', - 'remove' - ); - - - - $task = isset($_REQUEST['task']) ? $_REQUEST['task'] : ''; - $patch = isset($_REQUEST['patch']) ? $_REQUEST['patch'] : ''; - $cmd = isset($_REQUEST['cmd']) ? $_REQUEST['cmd'] : ''; - - // Controller - if (in_array($task, $allowedTasks)) { - @set_time_limit(9999); - if(empty($cmd)) { - $cmd = []; - } - $count = 2 + count($cmd); - $counter = 1; - file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); - $this->$task($patch); - foreach ($cmd as $c => $value) { - $counter++; - file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); - $this->executeCommand($c); - } - file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>100])); - } - else - { - $this->start(); - file_put_contents(__DIR__ . '/progress.json', null); - } - } - - function apply($patch = null) - { - if ($patch) { - $patchUrl = $this->url.$patch.'.patch'; - echo $patchUrl; - $result = exec('curl ' . $patchUrl . ' | git apply'); - print_R($result); - } else { - echo 'Apply with no Patch ID'; - } - } - - function remove($patch = null) - { - if ($patch) { - $patchUrl = $this->url.$patch.'.patch'; - $result = exec('curl ' . $patchUrl . ' | git apply -R'); - - return $result; - } else { - echo 'Could not remove Patch'; - } - } - - function executeCommand($cmd){ - - // cache clear by remove dir - if($cmd == "cache:clear"){ - return exec('rm -r app/cache/prod/'); - } - - $fullCommand = explode(' ', $cmd); - $command = $fullCommand[0]; - $argsCount = count($fullCommand) - 1; - $args = array('console', $command); - if ($argsCount) { - for ($i = 1; $i <= $argsCount; $i++) { - $args[] = $fullCommand[$i]; - } - } - defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); - try { - $input = new ArgvInput($args); - $output = new BufferedOutput(); - $kernel = new AppKernel('prod', false); - $app = new Application($kernel); - $app->setAutoExit(false); - $result = $app->run($input, $output); - echo "
\n".$output->fetch().'
'; - } catch (\Exception $exception) { - echo $exception->getMessage(); - } - } - - - // View - function start() - { ?> - - - - - Mautic Patch Tester - - - - - - -
- - - localfile))) - { - $dir_class = "alert alert-danger"; - $msg = "

This path is not writable. Please change permissions before continuing.

"; - // $continue = "disabled"; - } - else - { - $dir_class = "alert alert-secondary"; - $msg = ""; - $continue = ""; - } - ?> - - - -
-

Start Testing!

-

This app will allow you to immediately begin testing pull requests against your Mautic installation. You will need to make sure this file is in the root of your Mautic test instance.

- -
Current Path
-
- - -
-
-
-
- -
-
-
- -
-
-
-

Apply Pull Request

-
-
-
- - - e.g. Simply enter 3456 for PR #3456 -
-
-
Run after apply pull request
-
- -
-
-
-
- -
-
- - -
-
-
-
-
-
-
- - -
-
-
-

Remove Pull Request

-
-
-
- - - e.g. Simply enter 3456 for PR #3456 -
-
-
Run after remove pull request
-
- -
-
-
-
- -
- - -
-
-
- -
-
-
-
*This app does not yet take into account any pull requests that require database changes.
- - -
- - - - - - - - - Date: Mon, 9 Apr 2018 16:04:09 +0200 Subject: [PATCH 224/778] Init commit --- plugins/MauticFocusBundle/Config/config.php | 1 + .../MauticFocusBundle/Model/FocusModel.php | 20 ++++++++++++++----- .../Views/Builder/form.html.php | 10 ++++++---- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/plugins/MauticFocusBundle/Config/config.php b/plugins/MauticFocusBundle/Config/config.php index be561bfdb20..e69b9dca1ba 100644 --- a/plugins/MauticFocusBundle/Config/config.php +++ b/plugins/MauticFocusBundle/Config/config.php @@ -143,6 +143,7 @@ 'mautic.helper.templating', 'event_dispatcher', 'mautic.lead.model.lead', + 'mautic.lead.model.field', ], ], ], diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 17b823f20b0..5d4f28a9e10 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -17,6 +17,7 @@ use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\TemplatingHelper; use Mautic\CoreBundle\Model\FormModel; +use Mautic\LeadBundle\Model\FieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use MauticPlugin\MauticFocusBundle\Entity\Focus; @@ -56,6 +57,11 @@ class FocusModel extends FormModel */ protected $leadModel; + /** + * @var FieldModel + */ + protected $leadFieldModel; + /** * FocusModel constructor. * @@ -64,14 +70,16 @@ class FocusModel extends FormModel * @param TemplatingHelper $templating * @param EventDispatcherInterface $dispatcher * @param LeadModel $leadModel + * @param FieldModel $leadFieldModel */ - public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel) + public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel, FieldModel $leadFieldModel) { $this->formModel = $formModel; $this->trackableModel = $trackableModel; $this->templating = $templating; $this->dispatcher = $dispatcher; $this->leadModel = $leadModel; + $this->leadFieldModel = $leadFieldModel; } /** @@ -266,10 +274,12 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, + 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), + 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), ] ) : ''; diff --git a/plugins/MauticFocusBundle/Views/Builder/form.html.php b/plugins/MauticFocusBundle/Views/Builder/form.html.php index 327ad27bb6b..4c19821d99a 100644 --- a/plugins/MauticFocusBundle/Views/Builder/form.html.php +++ b/plugins/MauticFocusBundle/Views/Builder/form.html.php @@ -118,10 +118,12 @@ EXTRA; echo $view->render('MauticFormBundle:Builder:form.html.php', [ - 'form' => $form, - 'formExtra' => $formExtra, - 'action' => ($preview) ? '#' : null, - 'suffix' => '_focus', + 'form' => $form, + 'formExtra' => $formExtra, + 'action' => ($preview) ? '#' : null, + 'suffix' => '_focus', + 'contactFields' => $contactFields, + 'companyFields' => $companyFields, ] ); ?> From a8991adedddb42544c4e5ffa1c14bfa0534902ec Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 16:07:10 +0200 Subject: [PATCH 225/778] Fic CS --- plugins/MauticFocusBundle/Model/FocusModel.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 5d4f28a9e10..2b5253018c5 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -274,12 +274,12 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, - 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), - 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, + 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), + 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), ] ) : ''; From 7fb6ee8d78533f22daa7fe13a79a2a87bd7d8663 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Sat, 7 Apr 2018 16:07:20 +0200 Subject: [PATCH 226/778] Update company fields if company exist after form submit --- app/bundles/FormBundle/Model/SubmissionModel.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/bundles/FormBundle/Model/SubmissionModel.php b/app/bundles/FormBundle/Model/SubmissionModel.php index f5b6ca48701..289edc79d93 100644 --- a/app/bundles/FormBundle/Model/SubmissionModel.php +++ b/app/bundles/FormBundle/Model/SubmissionModel.php @@ -1035,6 +1035,9 @@ protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $le list($company, $leadAdded, $companyEntity) = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $lead, $this->companyModel); if ($leadAdded) { $lead->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']); + } elseif ($companyEntity instanceof Company) { + $this->companyModel->setFieldValues($companyEntity, $companyFieldMatches); + $this->companyModel->saveEntity($companyEntity); } if (!empty($company) and $companyEntity instanceof Company) { From 5c0aa9bf7cd2ad2d79c9f557f5578418d11fb06e Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 17:15:49 +0200 Subject: [PATCH 227/778] Add company to check all company fields --- app/bundles/FormBundle/Model/SubmissionModel.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/FormBundle/Model/SubmissionModel.php b/app/bundles/FormBundle/Model/SubmissionModel.php index 289edc79d93..d4f2d94f77f 100644 --- a/app/bundles/FormBundle/Model/SubmissionModel.php +++ b/app/bundles/FormBundle/Model/SubmissionModel.php @@ -885,6 +885,8 @@ protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $le // Closure to get data and unique fields $getCompanyData = function ($currentFields) use ($companyFields) { $companyData = []; + // force add company contact field to company fields check + $companyFields = array_merge($companyFields, ['company'=> 'company']); foreach ($companyFields as $alias => $properties) { if (isset($currentFields[$alias])) { $value = $currentFields[$alias]; From 6d3cdea9c0cf406a7b828959e0ce0939368c444d Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 16 Nov 2017 17:58:00 -0600 Subject: [PATCH 228/778] Command to deduplicate contacts --- .../LeadBundle/Command/DedupCommand.php | 50 ++++ app/bundles/LeadBundle/Config/config.php | 18 ++ .../LeadBundle/Entity/LeadRepository.php | 47 ++- app/bundles/LeadBundle/Model/DedupModel.php | 175 +++++++++++ app/bundles/LeadBundle/Model/LeadModel.php | 226 +++++++------- app/bundles/LeadBundle/Model/MergeModel.php | 281 ++++++++++++++++++ 6 files changed, 683 insertions(+), 114 deletions(-) create mode 100644 app/bundles/LeadBundle/Command/DedupCommand.php create mode 100644 app/bundles/LeadBundle/Model/DedupModel.php create mode 100644 app/bundles/LeadBundle/Model/MergeModel.php diff --git a/app/bundles/LeadBundle/Command/DedupCommand.php b/app/bundles/LeadBundle/Command/DedupCommand.php new file mode 100644 index 00000000000..36096c21888 --- /dev/null +++ b/app/bundles/LeadBundle/Command/DedupCommand.php @@ -0,0 +1,50 @@ +setName('mautic:contacts:dedup') + ->setDescription('Merge contacts based on same unique identifiers') + ->addOption('--newer-into-older', null, InputOption::VALUE_NONE, 'By default, this command will merge older contacts and activity into the newer. Use this flag to reverse that behavior.') + ->setHelp( + <<<'EOT' +The %command.name% command will dedpulicate contacts based on unique identifier values. + +php %command.full_name% +EOT + ); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + /** @var DedupModel $dedupModel */ + $dedupModel = $this->getContainer()->get('mautic.lead.model.dedup'); + $newerIntoOlder = (bool) $input->getOption('newer-into-older'); + + $dedupModel->dedup($newerIntoOlder, $output); + } +} diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1d092ca201b..d7034c2849a 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -834,6 +834,24 @@ 'mautic.lead.model.company', ], ], + 'mautic.lead.model.dedup' => [ + 'class' => Mautic\LeadBundle\Model\DedupModel::class, + 'arguments' => [ + 'mautic.lead.model.field', + 'mautic.lead.model.merge', + 'mautic.lead.repository.lead', + 'doctrine.orm.entity_manager', + ], + ], + 'mautic.lead.model.merge' => [ + 'class' => Mautic\LeadBundle\Model\MergeModel::class, + 'arguments' => [ + 'mautic.lead.model.lead', + 'mautic.lead.repository.merged_records', + 'event_dispatcher', + 'monolog.logger.mautic', + ], + ], 'mautic.lead.model.tag' => [ 'class' => \Mautic\LeadBundle\Model\TagModel::class, ], diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index b69f0e26334..f421b65759e 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -219,7 +219,8 @@ public function getLeadsByUniqueFields($uniqueFieldsWithData, $leadId = null, $l $q->expr()->in('l.id', ':ids') ) ->setParameter('ids', array_keys($leads)) - ->orderBy('l.dateAdded', 'DESC'); + ->orderBy('l.dateAdded', 'DESC') + ->addOrderBy('l.id', 'DESC'); $entities = $q->getQuery()->getResult(); /** @var Lead $lead */ @@ -253,7 +254,7 @@ public function getLeadIdsByUniqueFields($uniqueFieldsWithData, $leadId = null) // loop through the fields and foreach ($uniqueFieldsWithData as $col => $val) { - $q->orWhere("l.$col = :".$col) + $q->andWhere("l.$col = :".$col) ->setParameter($col, $val); } @@ -1068,6 +1069,48 @@ public function getTableAlias() return 'l'; } + /** + * Get the count of identified contacts. + * + * @return int + */ + public function getIdentifiedContactCount() + { + $qb = $this->getEntityManager()->getConnection()->createQueryBuilder() + ->select('count(*)') + ->from($this->getTableName(), $this->getTableAlias()); + + $qb->where( + $qb->expr()->isNotNull($this->getTableAlias().'.date_identified') + ); + + return (int) $qb->execute()->fetchColumn(); + } + + /** + * Get the next contact after an specific ID; mainly used in deduplication. + * + * @return Lead + */ + public function getNextIdentifiedContact($lastId) + { + $alias = $this->getTableAlias(); + $qb = $this->getEntityManager()->getConnection()->createQueryBuilder() + ->select("$alias.id") + ->from($this->getTableName(), $this->getTableAlias()); + + $qb->where( + $qb->expr()->andX( + $qb->expr()->gt("$alias.id", (int) $lastId), + $qb->expr()->isNotNull("$alias.date_identified") + ) + ) + ->orderBy("$alias.id") + ->setMaxResults(1); + + return ($next = $qb->execute()->fetchColumn()) ? $this->getEntity($next) : null; + } + /** * @param QueryBuilder $q * @param array $tables $tables[0] should be primary table diff --git a/app/bundles/LeadBundle/Model/DedupModel.php b/app/bundles/LeadBundle/Model/DedupModel.php new file mode 100644 index 00000000000..364ce689ef5 --- /dev/null +++ b/app/bundles/LeadBundle/Model/DedupModel.php @@ -0,0 +1,175 @@ +fieldModel = $fieldModel; + $this->mergeModel = $mergeModel; + $this->repository = $repository; + $this->em = $entityManager; + } + + /** + * @param bool $mergeNewerIntoOlder + * @param OutputInterface|null $output + */ + public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = null) + { + $this->mergeNewerIntoOlder = $mergeNewerIntoOlder; + $lastContactId = 0; + $totalContacts = $this->repository->getIdentifiedContactCount(); + $progress = null; + + if ($output) { + $progress = new ProgressBar($output, $totalContacts); + } + + while ($contact = $this->repository->getNextIdentifiedContact($lastContactId)) { + $lastContactId = $contact->getId(); + $fields = $contact->getProfileFields(); + $duplicates = $this->checkForDuplicateContacts($fields); + + if ($progress) { + $progress->advance(); + } + + // Were duplicates found? + if (count($duplicates) > 1) { + $loser = reset($duplicates); + while ($winner = next($duplicates)) { + $this->mergeModel->merge($loser, $winner); + + if ($progress) { + // Advance the progress bar for the deleted contacts that are no longer in the total count + $progress->advance(); + } + + $loser = $winner; + } + } + + // Clear all entities in memory for RAM control + $this->em->clear(); + } + } + + /** + * @param array $queryFields + * @param bool $returnWithQueryFields + * + * @return array|Lead + */ + public function checkForDuplicateContacts(array $queryFields) + { + $duplicates = []; + if ($uniqueData = $this->getUniqueData($queryFields)) { + $duplicates = $this->repository->getLeadsByUniqueFields($uniqueData); + + // By default, duplicates are ordered by newest first + if (!$this->mergeNewerIntoOlder) { + // Reverse the array so that oldeset are on "top" in order to merge oldest into the next until they all have been merged into the + // the newest record + $duplicates = array_reverse($duplicates); + } + } + + return $duplicates; + } + + /** + * @param array $queryFields + * + * @return array + */ + protected function getUniqueData(array $queryFields) + { + $uniqueLeadFields = $this->fieldModel->getUniqueIdentifierFields(); + $uniqueLeadFieldData = []; + $inQuery = array_intersect_key($queryFields, $this->getAvailableFields()); + foreach ($inQuery as $k => $v) { + if (empty($queryFields[$k])) { + unset($inQuery[$k]); + } + + if (array_key_exists($k, $uniqueLeadFields)) { + $uniqueLeadFieldData[$k] = $v; + } + } + + return $uniqueLeadFieldData; + } + + /** + * @return array + */ + protected function getAvailableFields() + { + if (null === $this->availableFields) { + $this->availableFields = $this->fieldModel->getFieldList( + false, + false, + [ + 'isPublished' => true, + ] + ); + } + + return $this->availableFields; + } +} diff --git a/app/bundles/LeadBundle/Model/LeadModel.php b/app/bundles/LeadBundle/Model/LeadModel.php index 558b8c71f51..003a9698a16 100644 --- a/app/bundles/LeadBundle/Model/LeadModel.php +++ b/app/bundles/LeadBundle/Model/LeadModel.php @@ -1080,118 +1080,6 @@ public function removeFromStages($lead, $stage, $manuallyRemoved = true) return $this; } - /** - * Merge two leads; if a conflict of data occurs, the newest lead will get precedence. - * - * @param Lead $lead - * @param Lead $lead2 - * @param bool $autoMode If true, the newest lead will be merged into the oldes then deleted; otherwise, $lead will be merged into $lead2 then deleted - * - * @return Lead - */ - public function mergeLeads(Lead $lead, Lead $lead2, $autoMode = true) - { - $this->logger->debug('LEAD: Merging leads'); - - $leadId = $lead->getId(); - $lead2Id = $lead2->getId(); - - //if they are the same lead, then just return one - if ($leadId === $lead2Id) { - $this->logger->debug('LEAD: Leads are the same'); - - return $lead; - } - - if ($autoMode) { - //which lead is the oldest? - $mergeWith = ($lead->getDateAdded() < $lead2->getDateAdded()) ? $lead : $lead2; - $mergeFrom = ($mergeWith->getId() === $leadId) ? $lead2 : $lead; - } else { - $mergeWith = $lead2; - $mergeFrom = $lead; - } - $this->logger->debug('LEAD: Lead ID# '.$mergeFrom->getId().' will be merged into ID# '.$mergeWith->getId()); - - //dispatch pre merge event - $event = new LeadMergeEvent($mergeWith, $mergeFrom); - if ($this->dispatcher->hasListeners(LeadEvents::LEAD_PRE_MERGE)) { - $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); - } - - //merge IP addresses - $ipAddresses = $mergeFrom->getIpAddresses(); - foreach ($ipAddresses as $ip) { - $mergeWith->addIpAddress($ip); - - $this->logger->debug('LEAD: Associating with IP '.$ip->getIpAddress()); - } - - //merge fields - $mergeFromFields = $mergeFrom->getFields(); - foreach ($mergeFromFields as $group => $groupFields) { - foreach ($groupFields as $alias => $details) { - if ('points' === $alias) { - // We have to ignore this as it's a special field and it will reset the points for the contact - continue; - } - - //overwrite old lead's data with new lead's if new lead's is not empty - if (!empty($details['value'])) { - $mergeWith->addUpdatedField($alias, $details['value']); - - $this->logger->debug('LEAD: Updated '.$alias.' = '.$details['value']); - } - } - } - - //merge owner - $oldOwner = $mergeWith->getOwner(); - $newOwner = $mergeFrom->getOwner(); - - if ($oldOwner === null && $newOwner !== null) { - $mergeWith->setOwner($newOwner); - - $this->logger->debug('LEAD: New owner is '.$newOwner->getId()); - } - - // Sum points - $mergeFromPoints = $mergeFrom->getPoints(); - $mergeWithPoints = $mergeWith->getPoints(); - $mergeWith->adjustPoints($mergeFromPoints); - $this->logger->debug('LEAD: Adding '.$mergeFromPoints.' points from lead ID #'.$mergeFrom->getId().' to lead ID #'.$mergeWith->getId().' with '.$mergeWithPoints.' points'); - - //merge tags - $mergeFromTags = $mergeFrom->getTags(); - $addTags = $mergeFromTags->getKeys(); - $this->modifyTags($mergeWith, $addTags, null, false); - - //save the updated lead - $this->saveEntity($mergeWith, false); - - // Update merge records for the lead about to be deleted - $this->getMergeRecordRepository()->moveMergeRecord($mergeFrom->getId(), $mergeWith->getId()); - - // Create an entry this contact was merged - $mergeRecord = new MergeRecord(); - $mergeRecord->setContact($mergeWith) - ->setDateAdded() - ->setName($mergeFrom->getPrimaryIdentifier()) - ->setMergedId($mergeFrom->getId()); - $this->getMergeRecordRepository()->saveEntity($mergeRecord); - - //post merge events - if ($this->dispatcher->hasListeners(LeadEvents::LEAD_POST_MERGE)) { - $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); - } - - //delete the old - $this->deleteEntity($mergeFrom); - - //return the merged lead - return $mergeWith; - } - /** * @depreacated 2.6.0 to be removed in 3.0; use getFrequencyRules() instead * @@ -2796,4 +2684,118 @@ public function setSystemCurrentLead(Lead $lead = null) $this->contactTracker->setSystemContact($lead); } + + /** + * Merge two leads; if a conflict of data occurs, the newest lead will get precedence. + * + * @deprecated 2.13.0; to be removed in 3.0. Use \Mautic\LeadBundle\Model\MergeModel instead + * + * @param Lead $lead + * @param Lead $lead2 + * @param bool $autoMode If true, the newest lead will be merged into the oldes then deleted; otherwise, $lead will be merged into $lead2 then deleted + * + * @return Lead + */ + public function mergeLeads(Lead $lead, Lead $lead2, $autoMode = true) + { + $this->logger->debug('LEAD: Merging leads'); + + $leadId = $lead->getId(); + $lead2Id = $lead2->getId(); + + //if they are the same lead, then just return one + if ($leadId === $lead2Id) { + $this->logger->debug('LEAD: Leads are the same'); + + return $lead; + } + + if ($autoMode) { + //which lead is the oldest? + $mergeWith = ($lead->getDateAdded() < $lead2->getDateAdded()) ? $lead : $lead2; + $mergeFrom = ($mergeWith->getId() === $leadId) ? $lead2 : $lead; + } else { + $mergeWith = $lead2; + $mergeFrom = $lead; + } + $this->logger->debug('LEAD: Lead ID# '.$mergeFrom->getId().' will be merged into ID# '.$mergeWith->getId()); + + //dispatch pre merge event + $event = new LeadMergeEvent($mergeWith, $mergeFrom); + if ($this->dispatcher->hasListeners(LeadEvents::LEAD_PRE_MERGE)) { + $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); + } + + //merge IP addresses + $ipAddresses = $mergeFrom->getIpAddresses(); + foreach ($ipAddresses as $ip) { + $mergeWith->addIpAddress($ip); + + $this->logger->debug('LEAD: Associating with IP '.$ip->getIpAddress()); + } + + //merge fields + $mergeFromFields = $mergeFrom->getFields(); + foreach ($mergeFromFields as $group => $groupFields) { + foreach ($groupFields as $alias => $details) { + if ('points' === $alias) { + // We have to ignore this as it's a special field and it will reset the points for the contact + continue; + } + + //overwrite old lead's data with new lead's if new lead's is not empty + if (!empty($details['value'])) { + $mergeWith->addUpdatedField($alias, $details['value']); + + $this->logger->debug('LEAD: Updated '.$alias.' = '.$details['value']); + } + } + } + + //merge owner + $oldOwner = $mergeWith->getOwner(); + $newOwner = $mergeFrom->getOwner(); + + if ($oldOwner === null && $newOwner !== null) { + $mergeWith->setOwner($newOwner); + + $this->logger->debug('LEAD: New owner is '.$newOwner->getId()); + } + + // Sum points + $mergeFromPoints = $mergeFrom->getPoints(); + $mergeWithPoints = $mergeWith->getPoints(); + $mergeWith->adjustPoints($mergeFromPoints); + $this->logger->debug('LEAD: Adding '.$mergeFromPoints.' points from lead ID #'.$mergeFrom->getId().' to lead ID #'.$mergeWith->getId().' with '.$mergeWithPoints.' points'); + + //merge tags + $mergeFromTags = $mergeFrom->getTags(); + $addTags = $mergeFromTags->getKeys(); + $this->modifyTags($mergeWith, $addTags, null, false); + + //save the updated lead + $this->saveEntity($mergeWith, false); + + // Update merge records for the lead about to be deleted + $this->getMergeRecordRepository()->moveMergeRecord($mergeFrom->getId(), $mergeWith->getId()); + + // Create an entry this contact was merged + $mergeRecord = new MergeRecord(); + $mergeRecord->setContact($mergeWith) + ->setDateAdded() + ->setName($mergeFrom->getPrimaryIdentifier()) + ->setMergedId($mergeFrom->getId()); + $this->getMergeRecordRepository()->saveEntity($mergeRecord); + + //post merge events + if ($this->dispatcher->hasListeners(LeadEvents::LEAD_POST_MERGE)) { + $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); + } + + //delete the old + $this->deleteEntity($mergeFrom); + + //return the merged lead + return $mergeWith; + } } diff --git a/app/bundles/LeadBundle/Model/MergeModel.php b/app/bundles/LeadBundle/Model/MergeModel.php new file mode 100644 index 00000000000..4eef30b8951 --- /dev/null +++ b/app/bundles/LeadBundle/Model/MergeModel.php @@ -0,0 +1,281 @@ +leadModel = $leadModel; + $this->repo = $repo; + $this->logger = $logger; + $this->dispatcher = $dispatcher; + } + + /** + * @param Lead $lead + * @param Lead $lead2 + * + * @return Lead + */ + public function mergeOldIntoNew(Lead $lead, Lead $lead2) + { + $winner = ($lead->getDateAdded() > $lead2->getDateAdded()) ? $lead : $lead2; + $loser = ($winner->getId() === $lead->getId()) ? $lead2 : $lead; + + return $this->merge($loser, $winner); + } + + /** + * @param Lead $lead + * @param Lead $lead2 + * + * @return Lead + */ + public function mergeNewIntoOld(Lead $lead, Lead $lead2) + { + $winner = ($lead->getDateAdded() < $lead2->getDateAdded()) ? $lead : $lead2; + $loser = ($winner->getId() === $lead->getId()) ? $lead2 : $lead; + + return $this->merge($loser, $winner); + } + + /** + * @param Lead $loser + * @param Lead $winner + * + * @return Lead + */ + public function merge(Lead $loser, Lead $winner) + { + $this->loser = $loser; + $this->winner = $winner; + + //if they are the same lead, then just return one + if ($loser === $winner) { + $this->logger->debug('CONTACT: Contacts are the same'); + + return $loser; + } + + $this->logger->debug('CONTACT: ID# '.$loser->getId().' will be merged into ID# '.$winner->getId()); + + // Dispatch pre merge event + $event = new LeadMergeEvent($winner, $loser); + if ($this->dispatcher->hasListeners(LeadEvents::LEAD_PRE_MERGE)) { + $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); + } + + // Merge everything + $this->updateMergeRecords(); + $this->mergeTimestamps(); + $this->mergeIpAddressHistory(); + $this->mergeFieldData(); + $this->mergeOwners(); + $this->mergePoints(); + $this->mergeTags(); + + // Save the updated contact + $this->leadModel->saveEntity($winner, false); + + // Dispatch post merge event + if ($this->dispatcher->hasListeners(LeadEvents::LEAD_POST_MERGE)) { + $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); + } + + // Delete the loser + $this->leadModel->deleteEntity($loser); + + return $winner; + } + + /** + * Merge timestamps. + */ + protected function mergeTimestamps() + { + // The winner should keep the most recent last active timestamp of the two + if ($this->loser->getLastActive() > $this->winner->getLastActive()) { + $this->winner->setLastActive($this->loser->getLastActive()); + } + + // The winner should keep the oldest date identified timestamp + if ($this->loser->getDateIdentified() < $this->winner->getDateIdentified()) { + $this->winner->setDateIdentified($this->loser->getDateIdentified()); + } + } + + /** + * Merge past merge records into the winner. + */ + protected function updateMergeRecords() + { + // Update merge records for the lead about to be deleted + $this->repo->moveMergeRecord($this->loser->getId(), $this->winner->getId()); + + // Create an entry this contact was merged + $mergeRecord = new MergeRecord(); + $mergeRecord->setContact($this->winner) + ->setDateAdded() + ->setName($this->loser->getPrimaryIdentifier()) + ->setMergedId($this->loser->getId()); + + $this->repo->saveEntity($mergeRecord); + } + + /** + * Merge IP history into the winner. + */ + protected function mergeIpAddressHistory() + { + $ipAddresses = $this->loser->getIpAddresses(); + foreach ($ipAddresses as $ip) { + $this->winner->addIpAddress($ip); + + $this->logger->debug('CONTACT: Associating '.$this->winner->getId().' with IP '.$ip->getIpAddress()); + } + } + + /** + * Merge custom field data into winner. + */ + protected function mergeFieldData() + { + // Use the modified date if applicable or date added if the contact has never been edited + $loserDate = ($this->loser->getDateModified()) ? $this->loser->getDateModified() : $this->loser->getDateAdded(); + $winnerDate = ($this->winner->getDateModified()) ? $this->winner->getDateModified() : $this->winner->getDateAdded(); + + // When it comes to data, keep the newest value regardless of the winner/loser + $newest = ($loserDate > $winnerDate) ? $this->loser : $this->winner; + $oldest = ($newest->getId() === $this->winner->getId()) ? $this->loser : $this->winner; + + $newestFields = $newest->getProfileFields(); + $oldestFields = $oldest->getProfileFields(); + + $winnerFields = $this->winner->getProfileFields(); + + foreach (array_keys($winnerFields) as $field) { + if ('points' === $field) { + // Let mergePoints() take care of this + continue; + } + + // Don't overwrite with an empty value (error on the side of not losing any data) + if ($this->valueIsEmpty($winnerFields[$field])) { + // Give precedence to the newest value + $newValue = (!$this->valueIsEmpty($newestFields[$field])) ? $newestFields[$field] : $oldestFields[$field]; + } elseif (!$this->valueIsEmpty($newestFields[$field]) && $winnerFields[$field] !== $newestFields[$field]) { + $newValue = $newestFields[$field]; + } + + if (isset($newValue)) { + $this->winner->addUpdatedField($field, $newValue); + $fromValue = empty($winnerFields[$field]) ? 'empty' : $winnerFields[$field]; + $this->logger->debug("CONTACT: Updated $field from $fromValue to $newValue for {$this->winner->getId()}"); + } + } + } + + /** + * Merge owners if the winner isn't already assigned an owner. + */ + protected function mergeOwners() + { + $oldOwner = $this->winner->getOwner(); + $newOwner = $this->loser->getOwner(); + + if ($oldOwner === null && $newOwner !== null) { + $this->winner->setOwner($newOwner); + + $this->logger->debug("CONTACT: New owner of {$this->winner->getId()} is {$newOwner->getId()}"); + } + } + + /** + * Sum points from both contacts. + */ + protected function mergePoints() + { + $winnerPoints = (int) $this->winner->getPoints(); + $loserPoints = (int) $this->loser->getPoints(); + $this->winner->setPoints($winnerPoints + $loserPoints); + $this->logger->debug("CONTACT: Adding {$loserPoints} points to {$this->winner->getId()}"); + } + + /** + * Merge tags from loser into winner. + */ + protected function mergeTags() + { + $loserTags = $this->loser->getTags(); + $addTags = $loserTags->getKeys(); + + $this->leadModel->modifyTags($this->winner, $addTags, null, false); + } + + /** + * Check if value is empty but don't include false or 0. + * + * @param $value + * + * @return bool + */ + protected function valueIsEmpty($value) + { + return null === $value || '' === $value; + } +} From 8ce2aeba0f28a5df35f836922ab14a35e24380ae Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sat, 18 Nov 2017 11:56:01 -0600 Subject: [PATCH 229/778] Plugged a memory leak due to Doctrine query caching --- app/bundles/LeadBundle/Entity/LeadRepository.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index f421b65759e..bed496c1b86 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -195,7 +195,8 @@ public function getLeadsByUniqueFields($uniqueFieldsWithData, $leadId = null, $l // if we have a lead ID lets use it if (!empty($leadId)) { // make sure that its not the id we already have - $q->andWhere('l.id != '.$leadId); + $q->andWhere('l.id != :leadId') + ->setParameter('leadId', $leadId); } if ($limit) { @@ -221,7 +222,8 @@ public function getLeadsByUniqueFields($uniqueFieldsWithData, $leadId = null, $l ->setParameter('ids', array_keys($leads)) ->orderBy('l.dateAdded', 'DESC') ->addOrderBy('l.id', 'DESC'); - $entities = $q->getQuery()->getResult(); + $entities = $q->getQuery() + ->getResult(); /** @var Lead $lead */ foreach ($entities as $lead) { @@ -349,12 +351,13 @@ public function getEntity($id = 0) $this->buildSelectClause($q, $id); $contactId = (int) $id['id']; } else { - $q->select('l, u, i') + $q->select('l') ->leftJoin('l.ipAddresses', 'i') ->leftJoin('l.owner', 'u'); $contactId = $id; } - $q->andWhere($this->getTableAlias().'.id = '.(int) $contactId); + $q->andWhere($this->getTableAlias().'.id = :id') + ->setParameter('id', (int) $contactId); $entity = $q->getQuery()->getSingleResult(); } catch (\Exception $e) { $entity = null; @@ -1108,7 +1111,9 @@ public function getNextIdentifiedContact($lastId) ->orderBy("$alias.id") ->setMaxResults(1); - return ($next = $qb->execute()->fetchColumn()) ? $this->getEntity($next) : null; + $next = $qb->execute()->fetchColumn(); + + return ($next) ? $this->getEntity($next) : null; } /** From f4699a0fe2d6e6867092f4010b25b863401d2d30 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sat, 18 Nov 2017 11:56:55 -0600 Subject: [PATCH 230/778] Prevent a PHP notice with unpublished fields --- app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php b/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php index 0ad069a029e..883b6d6aa6e 100644 --- a/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php +++ b/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php @@ -122,6 +122,7 @@ public function getEntitiesWithCustomFields($object, $args, $resultsCallback = n $order .= ' ELSE '.$count.' END) AS HIDDEN ORD'; //ORM - generates lead entities + /** @var \Doctrine\ORM\QueryBuilder $q */ $q = $this->getEntitiesOrmQueryBuilder($order); $this->buildSelectClause($dq, $args); @@ -136,6 +137,7 @@ public function getEntitiesWithCustomFields($object, $args, $resultsCallback = n ->getResult(); //assign fields + /** @var Lead $r */ foreach ($results as $r) { $id = $r->getId(); $r->setFields($fieldValues[$id]); From 8fbd95fb93ce48105cea40284754a15de1649128 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sat, 18 Nov 2017 11:57:14 -0600 Subject: [PATCH 231/778] Don't allow overwriting ID --- app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php b/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php index 2b4c0a133ef..4f97da44e3b 100644 --- a/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php +++ b/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php @@ -121,6 +121,11 @@ public function getFields($ungroup = false) */ public function addUpdatedField($alias, $value, $oldValue = null) { + // Don't allow overriding ID + if ('id' === $alias) { + return $this; + } + $property = (defined('self::FIELD_ALIAS')) ? str_replace(self::FIELD_ALIAS, '', $alias) : $alias; $field = $this->getField($alias); $setter = 'set'.ucfirst($property); From 2d9152ad4ccf39f7ed7e2d1cbfefcc86bf968ff6 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sat, 18 Nov 2017 11:58:19 -0600 Subject: [PATCH 232/778] Don't merge id --- app/bundles/LeadBundle/Model/MergeModel.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/bundles/LeadBundle/Model/MergeModel.php b/app/bundles/LeadBundle/Model/MergeModel.php index 4eef30b8951..c4a68f9f6bb 100644 --- a/app/bundles/LeadBundle/Model/MergeModel.php +++ b/app/bundles/LeadBundle/Model/MergeModel.php @@ -116,9 +116,7 @@ public function merge(Lead $loser, Lead $winner) // Dispatch pre merge event $event = new LeadMergeEvent($winner, $loser); - if ($this->dispatcher->hasListeners(LeadEvents::LEAD_PRE_MERGE)) { - $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); - } + $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); // Merge everything $this->updateMergeRecords(); @@ -133,9 +131,7 @@ public function merge(Lead $loser, Lead $winner) $this->leadModel->saveEntity($winner, false); // Dispatch post merge event - if ($this->dispatcher->hasListeners(LeadEvents::LEAD_POST_MERGE)) { - $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); - } + $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); // Delete the loser $this->leadModel->deleteEntity($loser); @@ -175,6 +171,7 @@ protected function updateMergeRecords() ->setMergedId($this->loser->getId()); $this->repo->saveEntity($mergeRecord); + $this->repo->clear(); } /** @@ -207,9 +204,8 @@ protected function mergeFieldData() $oldestFields = $oldest->getProfileFields(); $winnerFields = $this->winner->getProfileFields(); - foreach (array_keys($winnerFields) as $field) { - if ('points' === $field) { + if (in_array($field, ['id', 'points'])) { // Let mergePoints() take care of this continue; } From cbe7d5a25f44eae7cf5dc8771ec2176183294710 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sat, 18 Nov 2017 11:58:45 -0600 Subject: [PATCH 233/778] Return a count of merged contacts --- app/bundles/LeadBundle/Command/DedupCommand.php | 13 +++++++++++-- app/bundles/LeadBundle/Model/DedupModel.php | 7 +++++++ .../LeadBundle/Translations/en_US/messages.ini | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Command/DedupCommand.php b/app/bundles/LeadBundle/Command/DedupCommand.php index 36096c21888..fa6cf92a5b7 100644 --- a/app/bundles/LeadBundle/Command/DedupCommand.php +++ b/app/bundles/LeadBundle/Command/DedupCommand.php @@ -44,7 +44,16 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var DedupModel $dedupModel */ $dedupModel = $this->getContainer()->get('mautic.lead.model.dedup'); $newerIntoOlder = (bool) $input->getOption('newer-into-older'); - - $dedupModel->dedup($newerIntoOlder, $output); + $count = $dedupModel->dedup($newerIntoOlder, $output); + + $output->writeln(''); + $output->writeln( + $this->getContainer()->get('translator')->trans( + 'mautic.lead.merge.count', + [ + '%count%' => $count, + ] + ) + ); } } diff --git a/app/bundles/LeadBundle/Model/DedupModel.php b/app/bundles/LeadBundle/Model/DedupModel.php index 364ce689ef5..95b05d0e9f4 100644 --- a/app/bundles/LeadBundle/Model/DedupModel.php +++ b/app/bundles/LeadBundle/Model/DedupModel.php @@ -68,6 +68,8 @@ public function __construct(FieldModel $fieldModel, MergeModel $mergeModel, Lead /** * @param bool $mergeNewerIntoOlder * @param OutputInterface|null $output + * + * @return int */ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = null) { @@ -80,6 +82,7 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu $progress = new ProgressBar($output, $totalContacts); } + $dupCount = 0; while ($contact = $this->repository->getNextIdentifiedContact($lastContactId)) { $lastContactId = $contact->getId(); $fields = $contact->getProfileFields(); @@ -91,6 +94,7 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu // Were duplicates found? if (count($duplicates) > 1) { + $dupCount += count($duplicates) - 1; $loser = reset($duplicates); while ($winner = next($duplicates)) { $this->mergeModel->merge($loser, $winner); @@ -106,7 +110,10 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu // Clear all entities in memory for RAM control $this->em->clear(); + gc_collect_cycles(); } + + return $dupCount; } /** diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index 4fa31050e67..886db635d39 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -438,6 +438,7 @@ mautic.lead.list.year_this="this year" mautic.lead.list.anniversary="anniversary" mautic.lead.list.checkall.help="If you select multiple contacts at once, a green drop-down arrow will appear at the top of the list. You can manage bulk actions from this drop-down list (ex. Change segments or Set Do Not Contact)." mautic.lead.merge="Merge" +mautic.lead.merge.count="%count% contacts were merged." mautic.lead.merge.select="Choose the contact to merge with:" mautic.lead.merge.select.tooltip="Choose the contact to merge with." mautic.lead.merge.select.modal.tooltip="Filter the options using the search field, then select the lead to merge." From 9ad559272e8d9eb84b77f549e7cb2409947bd6c1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sat, 18 Nov 2017 12:01:17 -0600 Subject: [PATCH 234/778] Docblock update --- app/bundles/LeadBundle/Model/DedupModel.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Model/DedupModel.php b/app/bundles/LeadBundle/Model/DedupModel.php index 95b05d0e9f4..f06efcd729c 100644 --- a/app/bundles/LeadBundle/Model/DedupModel.php +++ b/app/bundles/LeadBundle/Model/DedupModel.php @@ -118,9 +118,8 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu /** * @param array $queryFields - * @param bool $returnWithQueryFields * - * @return array|Lead + * @return Lead[] */ public function checkForDuplicateContacts(array $queryFields) { From 527719c40856d836cbbb39945303d189f3b938ef Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 23 Nov 2017 09:56:26 -0600 Subject: [PATCH 235/778] Prevent overwriting of values --- .../MissingMergeSubjectException.php | 18 ++ .../Exception/SameContactException.php | 18 ++ .../Exception/ValueNotMergeable.php | 26 +++ .../LeadBundle/Helper/MergeValueHelper.php | 53 +++++ app/bundles/LeadBundle/Model/MergeModel.php | 207 +++++++++++------- 5 files changed, 248 insertions(+), 74 deletions(-) create mode 100644 app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php create mode 100644 app/bundles/LeadBundle/Exception/SameContactException.php create mode 100644 app/bundles/LeadBundle/Exception/ValueNotMergeable.php create mode 100644 app/bundles/LeadBundle/Helper/MergeValueHelper.php diff --git a/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php b/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php new file mode 100644 index 00000000000..38ff0c25134 --- /dev/null +++ b/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php @@ -0,0 +1,18 @@ +getDateAdded() > $lead2->getDateAdded()) ? $lead : $lead2; - $loser = ($winner->getId() === $lead->getId()) ? $lead2 : $lead; + $this->winner = $winner; - return $this->merge($loser, $winner); + return $this; } /** - * @param Lead $lead - * @param Lead $lead2 + * @param Lead $loser * - * @return Lead + * @return MergeModel */ - public function mergeNewIntoOld(Lead $lead, Lead $lead2) + public function setLoser(Lead $loser) { - $winner = ($lead->getDateAdded() < $lead2->getDateAdded()) ? $lead : $lead2; - $loser = ($winner->getId() === $lead->getId()) ? $lead2 : $lead; + $this->loser = $loser; - return $this->merge($loser, $winner); + return $this; } /** @@ -99,51 +99,62 @@ public function mergeNewIntoOld(Lead $lead, Lead $lead2) * @param Lead $winner * * @return Lead + * @throws MissingMergeSubjectException */ - public function merge(Lead $loser, Lead $winner) + public function merge() { - $this->loser = $loser; - $this->winner = $winner; - - //if they are the same lead, then just return one - if ($loser === $winner) { + try { + $this->checkIfMergeable(); + } catch (SameContactException $exception) { $this->logger->debug('CONTACT: Contacts are the same'); - return $loser; + return $this->winner; } - $this->logger->debug('CONTACT: ID# '.$loser->getId().' will be merged into ID# '.$winner->getId()); + $this->logger->debug('CONTACT: ID# '.$this->loser->getId().' will be merged into ID# '.$this->winner->getId()); // Dispatch pre merge event - $event = new LeadMergeEvent($winner, $loser); + $event = new LeadMergeEvent($this->winner, $this->loser); $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); // Merge everything - $this->updateMergeRecords(); - $this->mergeTimestamps(); - $this->mergeIpAddressHistory(); - $this->mergeFieldData(); - $this->mergeOwners(); - $this->mergePoints(); - $this->mergeTags(); + try { + $this->updateMergeRecords() + ->mergeTimestamps() + ->mergeIpAddressHistory() + ->mergeFieldData() + ->mergeOwners() + ->mergePoints() + ->mergeTags(); + } catch (SameContactException $exception) { + // Already handled; this is to just to make IDE happy + } catch (MissingMergeSubjectException $exception) { + // Already handled; this is to just to make IDE happy + } // Save the updated contact - $this->leadModel->saveEntity($winner, false); + $this->leadModel->saveEntity($this->winner, false); // Dispatch post merge event $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); // Delete the loser - $this->leadModel->deleteEntity($loser); + $this->leadModel->deleteEntity($this->loser); - return $winner; + return $this->winner; } /** * Merge timestamps. + * + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function mergeTimestamps() + public function mergeTimestamps() { + $this->checkIfMergeable(); + // The winner should keep the most recent last active timestamp of the two if ($this->loser->getLastActive() > $this->winner->getLastActive()) { $this->winner->setLastActive($this->loser->getLastActive()); @@ -153,45 +164,42 @@ protected function mergeTimestamps() if ($this->loser->getDateIdentified() < $this->winner->getDateIdentified()) { $this->winner->setDateIdentified($this->loser->getDateIdentified()); } - } - - /** - * Merge past merge records into the winner. - */ - protected function updateMergeRecords() - { - // Update merge records for the lead about to be deleted - $this->repo->moveMergeRecord($this->loser->getId(), $this->winner->getId()); - // Create an entry this contact was merged - $mergeRecord = new MergeRecord(); - $mergeRecord->setContact($this->winner) - ->setDateAdded() - ->setName($this->loser->getPrimaryIdentifier()) - ->setMergedId($this->loser->getId()); - - $this->repo->saveEntity($mergeRecord); - $this->repo->clear(); + return $this; } /** * Merge IP history into the winner. + * + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function mergeIpAddressHistory() + public function mergeIpAddressHistory() { + $this->checkIfMergeable(); + $ipAddresses = $this->loser->getIpAddresses(); foreach ($ipAddresses as $ip) { $this->winner->addIpAddress($ip); $this->logger->debug('CONTACT: Associating '.$this->winner->getId().' with IP '.$ip->getIpAddress()); } + + return $this; } /** * Merge custom field data into winner. + * + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function mergeFieldData() + public function mergeFieldData() { + $this->checkIfMergeable(); + // Use the modified date if applicable or date added if the contact has never been edited $loserDate = ($this->loser->getDateModified()) ? $this->loser->getDateModified() : $this->loser->getDateAdded(); $winnerDate = ($this->winner->getDateModified()) ? $this->winner->getDateModified() : $this->winner->getDateAdded(); @@ -203,34 +211,37 @@ protected function mergeFieldData() $newestFields = $newest->getProfileFields(); $oldestFields = $oldest->getProfileFields(); - $winnerFields = $this->winner->getProfileFields(); - foreach (array_keys($winnerFields) as $field) { + foreach (array_keys($newestFields) as $field) { if (in_array($field, ['id', 'points'])) { // Let mergePoints() take care of this continue; } - // Don't overwrite with an empty value (error on the side of not losing any data) - if ($this->valueIsEmpty($winnerFields[$field])) { - // Give precedence to the newest value - $newValue = (!$this->valueIsEmpty($newestFields[$field])) ? $newestFields[$field] : $oldestFields[$field]; - } elseif (!$this->valueIsEmpty($newestFields[$field]) && $winnerFields[$field] !== $newestFields[$field]) { - $newValue = $newestFields[$field]; - } - - if (isset($newValue)) { + try { + $newValue = MergeValueHelper::getMergeValue($newestFields[$field], $oldestFields[$field]); $this->winner->addUpdatedField($field, $newValue); + $fromValue = empty($winnerFields[$field]) ? 'empty' : $winnerFields[$field]; $this->logger->debug("CONTACT: Updated $field from $fromValue to $newValue for {$this->winner->getId()}"); + } catch (ValueNotMergeable $exception) { + $this->logger->info("CONTACT: $field is not mergeable for {$this->winner->getId()} - ".$exception->getMessage()); } } + + return $this; } /** * Merge owners if the winner isn't already assigned an owner. + * + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function mergeOwners() + public function mergeOwners() { + $this->checkIfMergeable(); + $oldOwner = $this->winner->getOwner(); $newOwner = $this->loser->getOwner(); @@ -239,39 +250,87 @@ protected function mergeOwners() $this->logger->debug("CONTACT: New owner of {$this->winner->getId()} is {$newOwner->getId()}"); } + + return $this; } /** * Sum points from both contacts. + * + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function mergePoints() + public function mergePoints() { + $this->checkIfMergeable(); + $winnerPoints = (int) $this->winner->getPoints(); $loserPoints = (int) $this->loser->getPoints(); $this->winner->setPoints($winnerPoints + $loserPoints); $this->logger->debug("CONTACT: Adding {$loserPoints} points to {$this->winner->getId()}"); + + return $this; } /** * Merge tags from loser into winner. + * + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function mergeTags() + public function mergeTags() { + $this->checkIfMergeable(); + $loserTags = $this->loser->getTags(); $addTags = $loserTags->getKeys(); $this->leadModel->modifyTags($this->winner, $addTags, null, false); + + return $this; } /** - * Check if value is empty but don't include false or 0. - * - * @param $value + * Merge past merge records into the winner. * - * @return bool + * @return $this + * @throws SameContactException + * @throws MissingMergeSubjectException + */ + private function updateMergeRecords() + { + $this->checkIfMergeable(); + + // Update merge records for the lead about to be deleted + $this->repo->moveMergeRecord($this->loser->getId(), $this->winner->getId()); + + // Create an entry this contact was merged + $mergeRecord = new MergeRecord(); + $mergeRecord->setContact($this->winner) + ->setDateAdded() + ->setName($this->loser->getPrimaryIdentifier()) + ->setMergedId($this->loser->getId()); + + $this->repo->saveEntity($mergeRecord); + $this->repo->clear(); + + return $this; + } + + /** + * @throws SameContactException + * @throws MissingMergeSubjectException */ - protected function valueIsEmpty($value) + private function checkIfMergeable() { - return null === $value || '' === $value; + if (!$this->winner || !$this->loser) { + throw new MissingMergeSubjectException(); + } + + if ($this->winner->getId() === $this->loser->getId()) { + throw new SameContactException(); + } } } From 234e54e06c27d31d68df4c419d2ced48ef5dde75 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 23 Nov 2017 09:56:47 -0600 Subject: [PATCH 236/778] Don't use blank values for comparison when searching for dups --- app/bundles/LeadBundle/Model/DedupModel.php | 29 ++++++++++++--------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/bundles/LeadBundle/Model/DedupModel.php b/app/bundles/LeadBundle/Model/DedupModel.php index f06efcd729c..2de0970d6e1 100644 --- a/app/bundles/LeadBundle/Model/DedupModel.php +++ b/app/bundles/LeadBundle/Model/DedupModel.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; +use Mautic\LeadBundle\Exception\MissingMergeSubjectException; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; @@ -22,32 +23,32 @@ class DedupModel /** * @var FieldModel */ - protected $fieldModel; + private $fieldModel; /** * @var MergeModel */ - protected $mergeModel; + private $mergeModel; /** * @var LeadRepository */ - protected $repository; + private $repository; /** * @var EntityManager */ - protected $em; + private $em; /** * @var array */ - protected $availableFields; + private $availableFields; /** * @var bool */ - protected $mergeNewerIntoOlder = false; + private $mergeNewerIntoOlder = false; /** * DedupModel constructor. @@ -70,6 +71,7 @@ public function __construct(FieldModel $fieldModel, MergeModel $mergeModel, Lead * @param OutputInterface|null $output * * @return int + * @throws MissingMergeSubjectException */ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = null) { @@ -94,10 +96,12 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu // Were duplicates found? if (count($duplicates) > 1) { - $dupCount += count($duplicates) - 1; $loser = reset($duplicates); while ($winner = next($duplicates)) { - $this->mergeModel->merge($loser, $winner); + $this->mergeModel + ->setLoser($loser) + ->setWinner($winner) + ->merge(); if ($progress) { // Advance the progress bar for the deleted contacts that are no longer in the total count @@ -143,14 +147,15 @@ public function checkForDuplicateContacts(array $queryFields) * * @return array */ - protected function getUniqueData(array $queryFields) + public function getUniqueData(array $queryFields) { $uniqueLeadFields = $this->fieldModel->getUniqueIdentifierFields(); $uniqueLeadFieldData = []; $inQuery = array_intersect_key($queryFields, $this->getAvailableFields()); foreach ($inQuery as $k => $v) { - if (empty($queryFields[$k])) { - unset($inQuery[$k]); + // Don't use empty values when checking for duplicates + if (empty($v)) { + continue; } if (array_key_exists($k, $uniqueLeadFields)) { @@ -164,7 +169,7 @@ protected function getUniqueData(array $queryFields) /** * @return array */ - protected function getAvailableFields() + private function getAvailableFields() { if (null === $this->availableFields) { $this->availableFields = $this->fieldModel->getFieldList( From e95e0855202288b9b0ead357b3018c4a5e8ab44c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 23 Nov 2017 18:35:01 -0600 Subject: [PATCH 237/778] Increment duplicate count upon merge --- app/bundles/LeadBundle/Model/DedupModel.php | 23 ++++++++++++++------- app/bundles/LeadBundle/Model/MergeModel.php | 20 +++++++++--------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/app/bundles/LeadBundle/Model/DedupModel.php b/app/bundles/LeadBundle/Model/DedupModel.php index 2de0970d6e1..6d62acb6d55 100644 --- a/app/bundles/LeadBundle/Model/DedupModel.php +++ b/app/bundles/LeadBundle/Model/DedupModel.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; use Mautic\LeadBundle\Exception\MissingMergeSubjectException; +use Mautic\LeadBundle\Exception\SameContactException; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; @@ -71,6 +72,7 @@ public function __construct(FieldModel $fieldModel, MergeModel $mergeModel, Lead * @param OutputInterface|null $output * * @return int + * * @throws MissingMergeSubjectException */ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = null) @@ -98,14 +100,19 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu if (count($duplicates) > 1) { $loser = reset($duplicates); while ($winner = next($duplicates)) { - $this->mergeModel - ->setLoser($loser) - ->setWinner($winner) - ->merge(); - - if ($progress) { - // Advance the progress bar for the deleted contacts that are no longer in the total count - $progress->advance(); + try { + $this->mergeModel + ->setLoser($loser) + ->setWinner($winner) + ->merge(); + + ++$dupCount; + + if ($progress) { + // Advance the progress bar for the deleted contacts that are no longer in the total count + $progress->advance(); + } + } catch (SameContactException $exception) { } $loser = $winner; diff --git a/app/bundles/LeadBundle/Model/MergeModel.php b/app/bundles/LeadBundle/Model/MergeModel.php index 90c1bd1bddd..9673ffa89ee 100644 --- a/app/bundles/LeadBundle/Model/MergeModel.php +++ b/app/bundles/LeadBundle/Model/MergeModel.php @@ -95,21 +95,14 @@ public function setLoser(Lead $loser) } /** - * @param Lead $loser - * @param Lead $winner - * * @return Lead + * * @throws MissingMergeSubjectException + * @throws SameContactException */ public function merge() { - try { - $this->checkIfMergeable(); - } catch (SameContactException $exception) { - $this->logger->debug('CONTACT: Contacts are the same'); - - return $this->winner; - } + $this->checkIfMergeable(); $this->logger->debug('CONTACT: ID# '.$this->loser->getId().' will be merged into ID# '.$this->winner->getId()); @@ -148,6 +141,7 @@ public function merge() * Merge timestamps. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ @@ -172,6 +166,7 @@ public function mergeTimestamps() * Merge IP history into the winner. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ @@ -193,6 +188,7 @@ public function mergeIpAddressHistory() * Merge custom field data into winner. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ @@ -235,6 +231,7 @@ public function mergeFieldData() * Merge owners if the winner isn't already assigned an owner. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ @@ -258,6 +255,7 @@ public function mergeOwners() * Sum points from both contacts. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ @@ -277,6 +275,7 @@ public function mergePoints() * Merge tags from loser into winner. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ @@ -296,6 +295,7 @@ public function mergeTags() * Merge past merge records into the winner. * * @return $this + * * @throws SameContactException * @throws MissingMergeSubjectException */ From 2fe3ca42a96a93437274ba4a85518fd6aeacdf13 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 23 Nov 2017 19:23:51 -0600 Subject: [PATCH 238/778] Added back removed select --- app/bundles/LeadBundle/Entity/LeadRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index bed496c1b86..5b16a9ae317 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -351,7 +351,7 @@ public function getEntity($id = 0) $this->buildSelectClause($q, $id); $contactId = (int) $id['id']; } else { - $q->select('l') + $q->select('l, u, i') ->leftJoin('l.ipAddresses', 'i') ->leftJoin('l.owner', 'u'); $contactId = $id; From 03c32e125ef71b7609fc441801f012fba2c34e22 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 17 Apr 2018 19:00:18 -0600 Subject: [PATCH 239/778] CS fixes --- .../LeadBundle/Exception/MissingMergeSubjectException.php | 4 +--- app/bundles/LeadBundle/Exception/SameContactException.php | 4 +--- app/bundles/LeadBundle/Exception/ValueNotMergeable.php | 4 ++-- app/bundles/LeadBundle/Helper/MergeValueHelper.php | 7 ++----- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php b/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php index 38ff0c25134..62fa3965864 100644 --- a/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php +++ b/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php @@ -11,8 +11,6 @@ namespace Mautic\LeadBundle\Exception; - class MissingMergeSubjectException extends \Exception { - -} \ No newline at end of file +} diff --git a/app/bundles/LeadBundle/Exception/SameContactException.php b/app/bundles/LeadBundle/Exception/SameContactException.php index 90bc5bef53e..b5f4fc6dd2c 100644 --- a/app/bundles/LeadBundle/Exception/SameContactException.php +++ b/app/bundles/LeadBundle/Exception/SameContactException.php @@ -11,8 +11,6 @@ namespace Mautic\LeadBundle\Exception; - class SameContactException extends \Exception { - -} \ No newline at end of file +} diff --git a/app/bundles/LeadBundle/Exception/ValueNotMergeable.php b/app/bundles/LeadBundle/Exception/ValueNotMergeable.php index caf2b034fbc..2c69040a303 100644 --- a/app/bundles/LeadBundle/Exception/ValueNotMergeable.php +++ b/app/bundles/LeadBundle/Exception/ValueNotMergeable.php @@ -21,6 +21,6 @@ class ValueNotMergeable extends \Exception */ public function __construct($newerValue, $olderValue) { - parent::__construct(var_export($newerValue, true). ' / '.var_export($olderValue, true)); + parent::__construct(var_export($newerValue, true).' / '.var_export($olderValue, true)); } -} \ No newline at end of file +} diff --git a/app/bundles/LeadBundle/Helper/MergeValueHelper.php b/app/bundles/LeadBundle/Helper/MergeValueHelper.php index 2bc16aba9f6..5038ec39418 100644 --- a/app/bundles/LeadBundle/Helper/MergeValueHelper.php +++ b/app/bundles/LeadBundle/Helper/MergeValueHelper.php @@ -11,7 +11,6 @@ namespace Mautic\LeadBundle\Helper; - use Mautic\LeadBundle\Exception\ValueNotMergeable; class MergeValueHelper @@ -20,7 +19,6 @@ class MergeValueHelper * @param $newerValue * @param $olderValue * - * @return null * @throws ValueNotMergeable */ public static function getMergeValue($newerValue, $olderValue) @@ -47,7 +45,6 @@ public static function getMergeValue($newerValue, $olderValue) */ public static function isNotEmpty($value) { - return (null !== $value && '' !== $value); + return null !== $value && '' !== $value; } - -} \ No newline at end of file +} From c3754dd5bc7a771ed3e370297dc7c2995a0ba889 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 17 Apr 2018 19:06:53 -0600 Subject: [PATCH 240/778] Added debugging --- app/bundles/LeadBundle/Model/MergeModel.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Model/MergeModel.php b/app/bundles/LeadBundle/Model/MergeModel.php index 9673ffa89ee..93e244bd6de 100644 --- a/app/bundles/LeadBundle/Model/MergeModel.php +++ b/app/bundles/LeadBundle/Model/MergeModel.php @@ -265,8 +265,9 @@ public function mergePoints() $winnerPoints = (int) $this->winner->getPoints(); $loserPoints = (int) $this->loser->getPoints(); - $this->winner->setPoints($winnerPoints + $loserPoints); - $this->logger->debug("CONTACT: Adding {$loserPoints} points to {$this->winner->getId()}"); + $this->winner->adjustPoints($loserPoints); + + $this->logger->debug('CONTACT: Adding '.$loserPoints.' points from contact ID #'.$this->loser->getId().' to contact ID #'.$this->winner->getId().' with '.$winnerPoints.' points'); return $this; } From 6273cd5df479abc87702d4e28b5ef1ae17e6a517 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 18 Apr 2018 20:26:55 -0600 Subject: [PATCH 241/778] Refactored a bit and added tests --- app/bundles/CoreBundle/Entity/IpAddress.php | 10 + .../LeadBundle/Command/DedupCommand.php | 15 +- app/bundles/LeadBundle/Config/config.php | 35 +- .../ContactDeduper.php} | 36 +- .../LeadBundle/Deduplicate/ContactMerger.php | 286 ++++++++++ .../Exception/SameContactException.php | 2 +- .../Helper/MergeValueHelper.php | 14 +- .../MissingMergeSubjectException.php | 16 - app/bundles/LeadBundle/Model/MergeModel.php | 337 ------------ .../Tests/Deduplicate/ContactDeduperTest.php | 166 ++++++ .../Tests/Deduplicate/ContactMergerTest.php | 505 ++++++++++++++++++ 11 files changed, 1017 insertions(+), 405 deletions(-) rename app/bundles/LeadBundle/{Model/DedupModel.php => Deduplicate/ContactDeduper.php} (81%) create mode 100644 app/bundles/LeadBundle/Deduplicate/ContactMerger.php rename app/bundles/LeadBundle/{ => Deduplicate}/Exception/SameContactException.php (83%) rename app/bundles/LeadBundle/{ => Deduplicate}/Helper/MergeValueHelper.php (73%) delete mode 100644 app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php delete mode 100644 app/bundles/LeadBundle/Model/MergeModel.php create mode 100644 app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php create mode 100644 app/bundles/LeadBundle/Tests/Deduplicate/ContactMergerTest.php diff --git a/app/bundles/CoreBundle/Entity/IpAddress.php b/app/bundles/CoreBundle/Entity/IpAddress.php index 55f0083905c..e13cde73294 100644 --- a/app/bundles/CoreBundle/Entity/IpAddress.php +++ b/app/bundles/CoreBundle/Entity/IpAddress.php @@ -90,6 +90,16 @@ public static function loadApiMetadata(ApiMetadataDriver $metadata) ->build(); } + /** + * IpAddress constructor. + * + * @param null $ipAddress + */ + public function __construct($ipAddress = null) + { + $this->ipAddress = $ipAddress; + } + /** * Get id. * diff --git a/app/bundles/LeadBundle/Command/DedupCommand.php b/app/bundles/LeadBundle/Command/DedupCommand.php index fa6cf92a5b7..f8e9eccba5a 100644 --- a/app/bundles/LeadBundle/Command/DedupCommand.php +++ b/app/bundles/LeadBundle/Command/DedupCommand.php @@ -12,7 +12,7 @@ namespace Mautic\LeadBundle\Command; use Mautic\CoreBundle\Command\ModeratedCommand; -use Mautic\LeadBundle\Model\DedupModel; +use Mautic\LeadBundle\Deduplicate\ContactDeduper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -25,7 +25,12 @@ public function configure() $this->setName('mautic:contacts:dedup') ->setDescription('Merge contacts based on same unique identifiers') - ->addOption('--newer-into-older', null, InputOption::VALUE_NONE, 'By default, this command will merge older contacts and activity into the newer. Use this flag to reverse that behavior.') + ->addOption( + '--newer-into-older', + null, + InputOption::VALUE_NONE, + 'By default, this command will merge older contacts and activity into the newer. Use this flag to reverse that behavior.' + ) ->setHelp( <<<'EOT' The %command.name% command will dedpulicate contacts based on unique identifier values. @@ -41,10 +46,10 @@ public function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - /** @var DedupModel $dedupModel */ - $dedupModel = $this->getContainer()->get('mautic.lead.model.dedup'); + /** @var ContactDeduper $deduper */ + $deduper = $this->getContainer()->get('mautic.lead.deduper'); $newerIntoOlder = (bool) $input->getOption('newer-into-older'); - $count = $dedupModel->dedup($newerIntoOlder, $output); + $count = $deduper->dedup($newerIntoOlder, $output); $output->writeln(''); $output->writeln( diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index d7034c2849a..9d8bd735590 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -723,6 +723,23 @@ 'event_dispatcher', ], ], + 'mautic.lead.merger' => [ + 'class' => \Mautic\LeadBundle\Deduplicate\ContactMerger::class, + 'arguments' => [ + 'mautic.lead.model.lead', + 'mautic.lead.repository.merged_records', + 'event_dispatcher', + 'monolog.logger.mautic', + ], + ], + 'mautic.lead.deduper' => [ + 'class' => \Mautic\LeadBundle\Deduplicate\ContactDeduper::class, + 'arguments' => [ + 'mautic.lead.model.field', + 'mautic.lead.merger', + 'mautic.lead.repository.lead', + ], + ], ], 'repositories' => [ 'mautic.lead.repository.dnc' => [ @@ -834,24 +851,6 @@ 'mautic.lead.model.company', ], ], - 'mautic.lead.model.dedup' => [ - 'class' => Mautic\LeadBundle\Model\DedupModel::class, - 'arguments' => [ - 'mautic.lead.model.field', - 'mautic.lead.model.merge', - 'mautic.lead.repository.lead', - 'doctrine.orm.entity_manager', - ], - ], - 'mautic.lead.model.merge' => [ - 'class' => Mautic\LeadBundle\Model\MergeModel::class, - 'arguments' => [ - 'mautic.lead.model.lead', - 'mautic.lead.repository.merged_records', - 'event_dispatcher', - 'monolog.logger.mautic', - ], - ], 'mautic.lead.model.tag' => [ 'class' => \Mautic\LeadBundle\Model\TagModel::class, ], diff --git a/app/bundles/LeadBundle/Model/DedupModel.php b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php similarity index 81% rename from app/bundles/LeadBundle/Model/DedupModel.php rename to app/bundles/LeadBundle/Deduplicate/ContactDeduper.php index 6d62acb6d55..cf807a0c572 100644 --- a/app/bundles/LeadBundle/Model/DedupModel.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php @@ -9,17 +9,17 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Model; +namespace Mautic\LeadBundle\Deduplicate; use Doctrine\ORM\EntityManager; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; -use Mautic\LeadBundle\Exception\MissingMergeSubjectException; -use Mautic\LeadBundle\Exception\SameContactException; +use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; +use Mautic\LeadBundle\Model\FieldModel; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; -class DedupModel +class ContactDeduper { /** * @var FieldModel @@ -27,20 +27,15 @@ class DedupModel private $fieldModel; /** - * @var MergeModel + * @var ContactMerger */ - private $mergeModel; + private $merger; /** * @var LeadRepository */ private $repository; - /** - * @var EntityManager - */ - private $em; - /** * @var array */ @@ -55,16 +50,14 @@ class DedupModel * DedupModel constructor. * * @param FieldModel $fieldModel - * @param MergeModel $mergeModel + * @param ContactMerger $merger * @param LeadRepository $repository - * @param EntityManager $entityManager */ - public function __construct(FieldModel $fieldModel, MergeModel $mergeModel, LeadRepository $repository, EntityManager $entityManager) + public function __construct(FieldModel $fieldModel, ContactMerger $merger, LeadRepository $repository) { $this->fieldModel = $fieldModel; - $this->mergeModel = $mergeModel; + $this->merger = $merger; $this->repository = $repository; - $this->em = $entityManager; } /** @@ -72,8 +65,6 @@ public function __construct(FieldModel $fieldModel, MergeModel $mergeModel, Lead * @param OutputInterface|null $output * * @return int - * - * @throws MissingMergeSubjectException */ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = null) { @@ -101,10 +92,7 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu $loser = reset($duplicates); while ($winner = next($duplicates)) { try { - $this->mergeModel - ->setLoser($loser) - ->setWinner($winner) - ->merge(); + $this->merger->merge($winner, $loser); ++$dupCount; @@ -120,7 +108,7 @@ public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = nu } // Clear all entities in memory for RAM control - $this->em->clear(); + $this->repository->clear(); gc_collect_cycles(); } @@ -140,7 +128,7 @@ public function checkForDuplicateContacts(array $queryFields) // By default, duplicates are ordered by newest first if (!$this->mergeNewerIntoOlder) { - // Reverse the array so that oldeset are on "top" in order to merge oldest into the next until they all have been merged into the + // Reverse the array so that oldest are on "top" in order to merge oldest into the next until they all have been merged into the // the newest record $duplicates = array_reverse($duplicates); } diff --git a/app/bundles/LeadBundle/Deduplicate/ContactMerger.php b/app/bundles/LeadBundle/Deduplicate/ContactMerger.php new file mode 100644 index 00000000000..57ae497574d --- /dev/null +++ b/app/bundles/LeadBundle/Deduplicate/ContactMerger.php @@ -0,0 +1,286 @@ +leadModel = $leadModel; + $this->repo = $repo; + $this->logger = $logger; + $this->dispatcher = $dispatcher; + } + + /** + * @param Lead $winner + * @param Lead $loser + * + * @return Lead + * + * @throws SameContactException + */ + public function merge(Lead $winner, Lead $loser) + { + if ($winner->getId() === $loser->getId()) { + throw new SameContactException(); + } + + $this->logger->debug('CONTACT: ID# '.$loser->getId().' will be merged into ID# '.$winner->getId()); + + // Dispatch pre merge event + $event = new LeadMergeEvent($winner, $loser); + $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); + + // Merge everything + $this->updateMergeRecords($winner, $loser) + ->mergeTimestamps($winner, $loser) + ->mergeIpAddressHistory($winner, $loser) + ->mergeFieldData($winner, $loser) + ->mergeOwners($winner, $loser) + ->mergePoints($winner, $loser) + ->mergeTags($winner, $loser); + + // Save the updated contact + $this->leadModel->saveEntity($winner, false); + + // Dispatch post merge event + $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); + + // Delete the loser + $this->leadModel->deleteEntity($loser); + + return $winner; + } + + /** + * Merge timestamps. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + public function mergeTimestamps(Lead $winner, Lead $loser) + { + // The winner should keep the most recent last active timestamp of the two + if ($loser->getLastActive() > $winner->getLastActive()) { + $winner->setLastActive($loser->getLastActive()); + } + + // The winner should keep the oldest date identified timestamp + if ($loser->getDateIdentified() < $winner->getDateIdentified()) { + $winner->setDateIdentified($loser->getDateIdentified()); + } + + return $this; + } + + /** + * Merge IP history into the winner. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + public function mergeIpAddressHistory(Lead $winner, Lead $loser) + { + $ipAddresses = $loser->getIpAddresses(); + + foreach ($ipAddresses as $ip) { + $winner->addIpAddress($ip); + + $this->logger->debug('CONTACT: Associating '.$winner->getId().' with IP '.$ip->getIpAddress()); + } + + return $this; + } + + /** + * Merge custom field data into winner. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + public function mergeFieldData(Lead $winner, Lead $loser) + { + // Use the modified date if applicable or date added if the contact has never been edited + $loserDate = ($loser->getDateModified()) ? $loser->getDateModified() : $loser->getDateAdded(); + $winnerDate = ($winner->getDateModified()) ? $winner->getDateModified() : $winner->getDateAdded(); + + // When it comes to data, keep the newest value regardless of the winner/loser + $newest = ($loserDate > $winnerDate) ? $loser : $winner; + $oldest = ($newest->getId() === $winner->getId()) ? $loser : $winner; + + $newestFields = $newest->getProfileFields(); + $oldestFields = $oldest->getProfileFields(); + + foreach (array_keys($newestFields) as $field) { + if (in_array($field, ['id', 'points'])) { + // Let mergePoints() take care of this + continue; + } + + try { + $newValue = MergeValueHelper::getMergeValue($newestFields[$field], $oldestFields[$field], $winner->getFieldValue($field)); + $winner->addUpdatedField($field, $newValue); + + $fromValue = empty($winnerFields[$field]) ? 'empty' : $winnerFields[$field]; + $this->logger->debug("CONTACT: Updated $field from $fromValue to $newValue for {$winner->getId()}"); + } catch (ValueNotMergeable $exception) { + $this->logger->info("CONTACT: $field is not mergeable for {$winner->getId()} - ".$exception->getMessage()); + } + } + + return $this; + } + + /** + * Merge owners if the winner isn't already assigned an owner. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + public function mergeOwners(Lead $winner, Lead $loser) + { + $oldOwner = $winner->getOwner(); + $newOwner = $loser->getOwner(); + + if ($oldOwner === null && $newOwner !== null) { + $winner->setOwner($newOwner); + + $this->logger->debug("CONTACT: New owner of {$winner->getId()} is {$newOwner->getId()}"); + } + + return $this; + } + + /** + * Sum points from both contacts. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + public function mergePoints(Lead $winner, Lead $loser) + { + $winnerPoints = (int) $winner->getPoints(); + $loserPoints = (int) $loser->getPoints(); + $winner->adjustPoints($loserPoints); + + $this->logger->debug( + 'CONTACT: Adding '.$loserPoints.' points from contact ID #'.$loser->getId().' to contact ID #'.$winner->getId().' with '.$winnerPoints + .' points' + ); + + return $this; + } + + /** + * Merge tags from loser into winner. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + public function mergeTags(Lead $winner, Lead $loser) + { + $loserTags = $loser->getTags(); + $addTags = $loserTags->getKeys(); + + $this->leadModel->modifyTags($winner, $addTags, null, false); + + return $this; + } + + /** + * Merge past merge records into the winner. + * + * @param Lead $winner + * @param Lead $loser + * + * @return $this + */ + private function updateMergeRecords(Lead $winner, Lead $loser) + { + // Update merge records for the lead about to be deleted + $this->repo->moveMergeRecord($loser->getId(), $winner->getId()); + + // Create an entry this contact was merged + $mergeRecord = new MergeRecord(); + $mergeRecord->setContact($winner) + ->setDateAdded() + ->setName($loser->getPrimaryIdentifier()) + ->setMergedId($loser->getId()); + + $this->repo->saveEntity($mergeRecord); + $this->repo->clear(); + + return $this; + } +} diff --git a/app/bundles/LeadBundle/Exception/SameContactException.php b/app/bundles/LeadBundle/Deduplicate/Exception/SameContactException.php similarity index 83% rename from app/bundles/LeadBundle/Exception/SameContactException.php rename to app/bundles/LeadBundle/Deduplicate/Exception/SameContactException.php index b5f4fc6dd2c..88add02ed58 100644 --- a/app/bundles/LeadBundle/Exception/SameContactException.php +++ b/app/bundles/LeadBundle/Deduplicate/Exception/SameContactException.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Exception; +namespace Mautic\LeadBundle\Deduplicate\Exception; class SameContactException extends \Exception { diff --git a/app/bundles/LeadBundle/Helper/MergeValueHelper.php b/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php similarity index 73% rename from app/bundles/LeadBundle/Helper/MergeValueHelper.php rename to app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php index 5038ec39418..0c846a500d1 100644 --- a/app/bundles/LeadBundle/Helper/MergeValueHelper.php +++ b/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php @@ -9,24 +9,30 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\LeadBundle\Helper; +namespace Mautic\LeadBundle\Deduplicate\Helper; use Mautic\LeadBundle\Exception\ValueNotMergeable; class MergeValueHelper { /** - * @param $newerValue - * @param $olderValue + * @param mixed $newerValue + * @param mixed $olderValue + * @param null $currentValue * + * @return mixed * @throws ValueNotMergeable */ - public static function getMergeValue($newerValue, $olderValue) + public static function getMergeValue($newerValue, $olderValue, $currentValue = null) { if ($newerValue === $olderValue) { throw new ValueNotMergeable($newerValue, $olderValue); } + if (null !== $currentValue && $newerValue === $currentValue) { + throw new ValueNotMergeable($newerValue, $olderValue); + } + if (self::isNotEmpty($newerValue)) { return $newerValue; } diff --git a/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php b/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php deleted file mode 100644 index 62fa3965864..00000000000 --- a/app/bundles/LeadBundle/Exception/MissingMergeSubjectException.php +++ /dev/null @@ -1,16 +0,0 @@ -leadModel = $leadModel; - $this->repo = $repo; - $this->logger = $logger; - $this->dispatcher = $dispatcher; - } - - /** - * @param Lead $winner - * - * @return MergeModel - */ - public function setWinner(Lead $winner) - { - $this->winner = $winner; - - return $this; - } - - /** - * @param Lead $loser - * - * @return MergeModel - */ - public function setLoser(Lead $loser) - { - $this->loser = $loser; - - return $this; - } - - /** - * @return Lead - * - * @throws MissingMergeSubjectException - * @throws SameContactException - */ - public function merge() - { - $this->checkIfMergeable(); - - $this->logger->debug('CONTACT: ID# '.$this->loser->getId().' will be merged into ID# '.$this->winner->getId()); - - // Dispatch pre merge event - $event = new LeadMergeEvent($this->winner, $this->loser); - $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); - - // Merge everything - try { - $this->updateMergeRecords() - ->mergeTimestamps() - ->mergeIpAddressHistory() - ->mergeFieldData() - ->mergeOwners() - ->mergePoints() - ->mergeTags(); - } catch (SameContactException $exception) { - // Already handled; this is to just to make IDE happy - } catch (MissingMergeSubjectException $exception) { - // Already handled; this is to just to make IDE happy - } - - // Save the updated contact - $this->leadModel->saveEntity($this->winner, false); - - // Dispatch post merge event - $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); - - // Delete the loser - $this->leadModel->deleteEntity($this->loser); - - return $this->winner; - } - - /** - * Merge timestamps. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - public function mergeTimestamps() - { - $this->checkIfMergeable(); - - // The winner should keep the most recent last active timestamp of the two - if ($this->loser->getLastActive() > $this->winner->getLastActive()) { - $this->winner->setLastActive($this->loser->getLastActive()); - } - - // The winner should keep the oldest date identified timestamp - if ($this->loser->getDateIdentified() < $this->winner->getDateIdentified()) { - $this->winner->setDateIdentified($this->loser->getDateIdentified()); - } - - return $this; - } - - /** - * Merge IP history into the winner. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - public function mergeIpAddressHistory() - { - $this->checkIfMergeable(); - - $ipAddresses = $this->loser->getIpAddresses(); - foreach ($ipAddresses as $ip) { - $this->winner->addIpAddress($ip); - - $this->logger->debug('CONTACT: Associating '.$this->winner->getId().' with IP '.$ip->getIpAddress()); - } - - return $this; - } - - /** - * Merge custom field data into winner. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - public function mergeFieldData() - { - $this->checkIfMergeable(); - - // Use the modified date if applicable or date added if the contact has never been edited - $loserDate = ($this->loser->getDateModified()) ? $this->loser->getDateModified() : $this->loser->getDateAdded(); - $winnerDate = ($this->winner->getDateModified()) ? $this->winner->getDateModified() : $this->winner->getDateAdded(); - - // When it comes to data, keep the newest value regardless of the winner/loser - $newest = ($loserDate > $winnerDate) ? $this->loser : $this->winner; - $oldest = ($newest->getId() === $this->winner->getId()) ? $this->loser : $this->winner; - - $newestFields = $newest->getProfileFields(); - $oldestFields = $oldest->getProfileFields(); - - foreach (array_keys($newestFields) as $field) { - if (in_array($field, ['id', 'points'])) { - // Let mergePoints() take care of this - continue; - } - - try { - $newValue = MergeValueHelper::getMergeValue($newestFields[$field], $oldestFields[$field]); - $this->winner->addUpdatedField($field, $newValue); - - $fromValue = empty($winnerFields[$field]) ? 'empty' : $winnerFields[$field]; - $this->logger->debug("CONTACT: Updated $field from $fromValue to $newValue for {$this->winner->getId()}"); - } catch (ValueNotMergeable $exception) { - $this->logger->info("CONTACT: $field is not mergeable for {$this->winner->getId()} - ".$exception->getMessage()); - } - } - - return $this; - } - - /** - * Merge owners if the winner isn't already assigned an owner. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - public function mergeOwners() - { - $this->checkIfMergeable(); - - $oldOwner = $this->winner->getOwner(); - $newOwner = $this->loser->getOwner(); - - if ($oldOwner === null && $newOwner !== null) { - $this->winner->setOwner($newOwner); - - $this->logger->debug("CONTACT: New owner of {$this->winner->getId()} is {$newOwner->getId()}"); - } - - return $this; - } - - /** - * Sum points from both contacts. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - public function mergePoints() - { - $this->checkIfMergeable(); - - $winnerPoints = (int) $this->winner->getPoints(); - $loserPoints = (int) $this->loser->getPoints(); - $this->winner->adjustPoints($loserPoints); - - $this->logger->debug('CONTACT: Adding '.$loserPoints.' points from contact ID #'.$this->loser->getId().' to contact ID #'.$this->winner->getId().' with '.$winnerPoints.' points'); - - return $this; - } - - /** - * Merge tags from loser into winner. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - public function mergeTags() - { - $this->checkIfMergeable(); - - $loserTags = $this->loser->getTags(); - $addTags = $loserTags->getKeys(); - - $this->leadModel->modifyTags($this->winner, $addTags, null, false); - - return $this; - } - - /** - * Merge past merge records into the winner. - * - * @return $this - * - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - private function updateMergeRecords() - { - $this->checkIfMergeable(); - - // Update merge records for the lead about to be deleted - $this->repo->moveMergeRecord($this->loser->getId(), $this->winner->getId()); - - // Create an entry this contact was merged - $mergeRecord = new MergeRecord(); - $mergeRecord->setContact($this->winner) - ->setDateAdded() - ->setName($this->loser->getPrimaryIdentifier()) - ->setMergedId($this->loser->getId()); - - $this->repo->saveEntity($mergeRecord); - $this->repo->clear(); - - return $this; - } - - /** - * @throws SameContactException - * @throws MissingMergeSubjectException - */ - private function checkIfMergeable() - { - if (!$this->winner || !$this->loser) { - throw new MissingMergeSubjectException(); - } - - if ($this->winner->getId() === $this->loser->getId()) { - throw new SameContactException(); - } - } -} diff --git a/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php b/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php new file mode 100644 index 00000000000..0a33829899a --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php @@ -0,0 +1,166 @@ +fieldModel = $this->getMockBuilder(FieldModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contactMerger = $this->getMockBuilder(ContactMerger::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->leadRepository = $this->getMockBuilder(LeadRepository::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testDuplicatesAreMergedWithMergeOlderIntoNewer() + { + $this->leadRepository->expects($this->once()) + ->method('getIdentifiedContactCount') + ->willReturn(4); + + $lead1 = $this->getLead(1, 'lead1@test.com'); + $lead2 = $this->getLead(2, 'lead2@test.com'); + $lead3 = $this->getLead(3, 'lead3@test.com'); + // Duplicate + $lead4 = $this->getLead(4, 'lead1@test.com'); + + $this->leadRepository->expects($this->exactly(4)) + ->method('getNextIdentifiedContact') + ->withConsecutive([0], [1], [2], [3]) + ->willReturnOnConsecutiveCalls($lead1, $lead2, $lead3, null); + + $this->fieldModel->expects($this->exactly(3)) + ->method('getUniqueIdentifierFields') + ->willReturn(['email' => 'email']); + $this->fieldModel->expects($this->once()) + ->method('getFieldList') + ->willReturn(['email' => 'email']); + + $this->leadRepository->expects($this->exactly(3)) + ->method('getLeadsByUniqueFields') + // $lead4 has a older dateAdded + ->willReturnOnConsecutiveCalls([$lead4, $lead1], [], []); + + // $lead4 is winner as the older contact + $this->contactMerger->expects($this->once()) + ->method('merge') + ->with($lead4, $lead1); + + $this->getDeduper()->dedup(); + } + + public function testDuplicatesAreMergedWithMergeNewerIntoOlder() + { + $this->leadRepository->expects($this->once()) + ->method('getIdentifiedContactCount') + ->willReturn(4); + + $lead1 = $this->getLead(1, 'lead1@test.com'); + $lead2 = $this->getLead(2, 'lead2@test.com'); + $lead3 = $this->getLead(3, 'lead3@test.com'); + // Duplicate + $lead4 = $this->getLead(4, 'lead1@test.com'); + + $this->leadRepository->expects($this->exactly(4)) + ->method('getNextIdentifiedContact') + ->withConsecutive([0], [1], [2], [3]) + ->willReturnOnConsecutiveCalls($lead1, $lead2, $lead3, null); + + $this->fieldModel->expects($this->exactly(3)) + ->method('getUniqueIdentifierFields') + ->willReturn(['email' => 'email']); + $this->fieldModel->expects($this->once()) + ->method('getFieldList') + ->willReturn(['email' => 'email']); + + $this->leadRepository->expects($this->exactly(3)) + ->method('getLeadsByUniqueFields') + // $lead1 has a older dateAdded + ->willReturnOnConsecutiveCalls([$lead1, $lead4], [], []); + + // $lead1 is the winner as the newer contact + $this->contactMerger->expects($this->once()) + ->method('merge') + ->with($lead1, $lead4); + + $this->getDeduper()->dedup(); + } + + /** + * @param $id + * @param $email + * + * @return Lead|\PHPUnit_Framework_MockObject_MockObject + */ + private function getLead($id, $email) + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->any()) + ->method('getId') + ->willReturn($id); + $lead->expects($this->any()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => $id, + 'points' => 10, + 'email' => $email, + ] + ); + $lead->expects($this->any()) + ->method('getDateModified') + ->willReturn(new \DateTime()); + + return $lead; + } + + /** + * @return ContactDeduper + */ + private function getDeduper() + { + return new ContactDeduper( + $this->fieldModel, + $this->contactMerger, + $this->leadRepository + ); + } +} diff --git a/app/bundles/LeadBundle/Tests/Deduplicate/ContactMergerTest.php b/app/bundles/LeadBundle/Tests/Deduplicate/ContactMergerTest.php new file mode 100644 index 00000000000..8a23188dfee --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Deduplicate/ContactMergerTest.php @@ -0,0 +1,505 @@ +leadModel = $this->getMockBuilder(LeadModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mergeRecordRepo = $this->getMockBuilder(MergeRecordRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->dispatcher = $this->getMockBuilder(EventDispatcher::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->logger = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testMergeTimestamps() + { + $oldestDateTime = new \DateTime('-60 minutes'); + $latestDateTime = new \DateTime('-30 minutes'); + + $winner = new Lead(); + $winner->setLastActive($oldestDateTime); + $winner->setDateIdentified($latestDateTime); + + $loser = new Lead(); + $loser->setLastActive($latestDateTime); + $loser->setDateIdentified($oldestDateTime); + + $this->getMerger()->mergeTimestamps($winner, $loser); + + $this->assertEquals($latestDateTime, $winner->getLastActive()); + $this->assertEquals($oldestDateTime, $winner->getDateIdentified()); + } + + public function testMergeIpAddresses() + { + $winner = new Lead(); + $winner->addIpAddress((new IpAddress())->setIpAddress('1.2.3.4')); + $winner->addIpAddress((new IpAddress())->setIpAddress('4.3.2.1')); + + $loser = new Lead(); + $loser->addIpAddress((new IpAddress())->setIpAddress('5.6.7.8')); + $loser->addIpAddress((new IpAddress())->setIpAddress('8.7.6.5')); + + $this->getMerger()->mergeIpAddressHistory($winner, $loser); + + $ipAddresses = $winner->getIpAddresses(); + $this->assertCount(4, $ipAddresses); + } + + public function testMergeFieldDataWithLoserAsNewlyUpdated() + { + $winner = $this->getMockBuilder(Lead::class) + ->getMock(); + $winner->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 1, + 'points' => 10, + 'email' => 'winner@test.com', + ] + ); + + $loser = $this->getMockBuilder(Lead::class) + ->getMock(); + $loser->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 2, + 'points' => 20, + 'email' => 'loser@test.com', + ] + ); + + $merger = $this->getMerger(); + + $winnerDateModified = new \DateTime('-30 minutes'); + $loserDateModified = new \DateTime(); + $winner->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn($winnerDateModified); + $loser->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn($loserDateModified); + $winner->expects($this->once()) + ->method('getFieldValue') + ->with('email') + ->willReturn('winner@test.com'); + + $winner->expects($this->exactly(2)) + ->method('getId') + ->willReturn(1); + + $loser->expects($this->once()) + ->method('getId') + ->willReturn(2); + + // Loser values are newest so should be kept + // id and points should not be set addUpdatedField should only be called once for email + $winner->expects($this->once()) + ->method('addUpdatedField') + ->with('email', 'loser@test.com'); + + $merger->mergeFieldData($winner, $loser); + } + + public function testMergeFieldDataWithWinnerAsNewlyUpdated() + { + $winner = $this->getMockBuilder(Lead::class) + ->getMock(); + $winner->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 1, + 'points' => 10, + 'email' => 'winner@test.com', + ] + ); + + $loser = $this->getMockBuilder(Lead::class) + ->getMock(); + $loser->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 2, + 'points' => 20, + 'email' => 'loser@test.com', + ] + ); + + $merger = $this->getMerger(); + + $winnerDateModified = new \DateTime(); + $loserDateModified = new \DateTime('-30 minutes'); + $winner->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn($winnerDateModified); + $winner->expects($this->once()) + ->method('getFieldValue') + ->with('email') + ->willReturn('winner@test.com'); + + $loser->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn($loserDateModified); + + $winner->expects($this->exactly(3)) + ->method('getId') + ->willReturn(1); + + $loser->expects($this->never()) + ->method('getId'); + + // Winner values are newest so should be kept + // addUpdatedField should never be called as they aren't different values + $winner->expects($this->never()) + ->method('addUpdatedField'); + + $merger->mergeFieldData($winner, $loser); + } + + public function testMergeFieldDataWithLoserAsNewlyCreated() + { + $winner = $this->getMockBuilder(Lead::class) + ->getMock(); + $winner->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 1, + 'points' => 10, + 'email' => 'winner@test.com', + ] + ); + + $loser = $this->getMockBuilder(Lead::class) + ->getMock(); + $loser->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 2, + 'points' => 20, + 'email' => 'loser@test.com', + ] + ); + + $merger = $this->getMerger(); + + $winnerDateModified = new \DateTime('-30 minutes'); + $loserDateModified = new \DateTime(); + $winner->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn($winnerDateModified); + $winner->expects($this->once()) + ->method('getFieldValue') + ->with('email') + ->willReturn('winner@test.com'); + + $loser->expects($this->exactly(1)) + ->method('getDateModified') + ->willReturn(null); + $loser->expects($this->once()) + ->method('getDateAdded') + ->willReturn($loserDateModified); + + $winner->expects($this->exactly(2)) + ->method('getId') + ->willReturn(1); + + $loser->expects($this->once()) + ->method('getId') + ->willReturn(2); + + // Loser values are newest so should be kept + // id and points should not be set addUpdatedField should only be called once for email + $winner->expects($this->once()) + ->method('addUpdatedField') + ->with('email', 'loser@test.com'); + + $merger->mergeFieldData($winner, $loser); + } + + public function testMergeFieldDataWithWinnerAsNewlyCreated() + { + $winner = $this->getMockBuilder(Lead::class) + ->getMock(); + $winner->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 1, + 'points' => 10, + 'email' => 'winner@test.com', + ] + ); + + $loser = $this->getMockBuilder(Lead::class) + ->getMock(); + $loser->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 2, + 'points' => 20, + 'email' => 'loser@test.com', + ] + ); + + $merger = $this->getMerger(); + + $winnerDateModified = new \DateTime(); + $loserDateModified = new \DateTime('-30 minutes'); + $winner->expects($this->once()) + ->method('getDateModified') + ->willReturn(null); + $winner->expects($this->once()) + ->method('getDateAdded') + ->willReturn($winnerDateModified); + $winner->expects($this->once()) + ->method('getFieldValue') + ->with('email') + ->willReturn('winner@test.com'); + + $loser->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn($loserDateModified); + + $winner->expects($this->exactly(3)) + ->method('getId') + ->willReturn(1); + + $loser->expects($this->never()) + ->method('getId'); + + // Winner values are newest so should be kept + // addUpdatedField should never be called as they aren't different values + $winner->expects($this->never()) + ->method('addUpdatedField'); + + $merger->mergeFieldData($winner, $loser); + } + + public function testMergeOwners() + { + $winner = new Lead(); + $loser = new Lead(); + + $winnerOwner = new User(); + $winnerOwner->setUsername('bob'); + $winner->setOwner($winnerOwner); + + $loserOwner = new User(); + $loserOwner->setUsername('susan'); + $loser->setOwner($loserOwner); + + // Should not have been merged due to winner already having one + $this->getMerger()->mergeOwners($winner, $loser); + $this->assertEquals($winnerOwner->getUsername(), $winner->getOwner()->getUsername()); + + $winner->setOwner(null); + $this->getMerger()->mergeOwners($winner, $loser); + + // Should be set to loser owner since winner owner was null + $this->assertEquals($loserOwner->getUsername(), $winner->getOwner()->getUsername()); + } + + public function testMergePoints() + { + $winner = new Lead(); + $loser = new Lead(); + + $winner->setPoints(100); + $loser->setPoints(50); + + $this->getMerger()->mergePoints($winner, $loser); + + $this->assertEquals(150, $winner->getPoints()); + } + + public function testMergeTags() + { + $winner = new Lead(); + $loser = new Lead(); + $loser->addTag(new Tag('loser')); + $loser->addTag(new Tag('loser2')); + + $this->leadModel->expects($this->once()) + ->method('modifyTags') + ->with($winner, ['loser', 'loser2'], null, false); + + $this->getMerger()->mergeTags($winner, $loser); + } + + public function testFullMergeThrowsSameContactException() + { + $winner = $this->getMockBuilder(Lead::class) + ->getMock(); + $winner->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $loser = $this->getMockBuilder(Lead::class) + ->getMock(); + $loser->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->expectException(SameContactException::class); + + $this->getMerger()->merge($winner, $loser); + } + + public function testFullMerge() + { + $winner = $this->getMockBuilder(Lead::class) + ->getMock(); + $winner->expects($this->any()) + ->method('getId') + ->willReturn(1); + $winner->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 1, + 'points' => 10, + 'email' => 'winner@test.com', + ] + ); + $winner->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn(new \DateTime('-30 minutes')); + + $loser = $this->getMockBuilder(Lead::class) + ->getMock(); + $loser->expects($this->any()) + ->method('getId') + ->willReturn(2); + $loser->expects($this->once()) + ->method('getProfileFields') + ->willReturn( + [ + 'id' => 2, + 'points' => 20, + 'email' => 'loser@test.com', + ] + ); + $loser->expects($this->exactly(2)) + ->method('getDateModified') + ->willReturn(new \DateTime()); + + // updateMergeRecords + $this->mergeRecordRepo->expects($this->once()) + ->method('moveMergeRecord') + ->with(2, 1); + + // mergeIpAddresses + $ip = new IpAddress('1.2.3..4'); + $loser->expects($this->once()) + ->method('getIpAddresses') + ->willReturn(new ArrayCollection([$ip])); + $winner->expects($this->once()) + ->method('addIpAddress') + ->with($ip); + + // mergeFieldData + $winner->expects($this->once()) + ->method('getFieldValue') + ->with('email') + ->willReturn('winner@test.com'); + $winner->expects($this->once()) + ->method('addUpdatedField') + ->with('email', 'loser@test.com'); + + // mergeOwners + $winner->expects($this->never()) + ->method('setOwner'); + + // mergePoints + $loser->expects($this->once()) + ->method('getPoints') + ->willReturn(100); + $winner->expects($this->once()) + ->method('adjustPoints') + ->with(100); + + // mergeTags + $loser->expects($this->once()) + ->method('getTags') + ->willReturn(new ArrayCollection()); + $this->leadModel->expects($this->once()) + ->method('modifyTags') + ->with($winner, [], null, false); + + $this->getMerger()->merge($winner, $loser); + } + + /** + * @return ContactMerger + */ + private function getMerger() + { + return new ContactMerger( + $this->leadModel, + $this->mergeRecordRepo, + $this->dispatcher, + $this->logger + ); + } +} From d850c7794993fd2c739f653fa4b58bf14dca29d3 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 19 Apr 2018 08:53:29 -0600 Subject: [PATCH 242/778] Fixed variable --- app/bundles/LeadBundle/Deduplicate/ContactMerger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Deduplicate/ContactMerger.php b/app/bundles/LeadBundle/Deduplicate/ContactMerger.php index 57ae497574d..05677cf6286 100644 --- a/app/bundles/LeadBundle/Deduplicate/ContactMerger.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactMerger.php @@ -186,7 +186,7 @@ public function mergeFieldData(Lead $winner, Lead $loser) $newValue = MergeValueHelper::getMergeValue($newestFields[$field], $oldestFields[$field], $winner->getFieldValue($field)); $winner->addUpdatedField($field, $newValue); - $fromValue = empty($winnerFields[$field]) ? 'empty' : $winnerFields[$field]; + $fromValue = empty($oldestFields[$field]) ? 'empty' : $oldestFields[$field]; $this->logger->debug("CONTACT: Updated $field from $fromValue to $newValue for {$winner->getId()}"); } catch (ValueNotMergeable $exception) { $this->logger->info("CONTACT: $field is not mergeable for {$winner->getId()} - ".$exception->getMessage()); From ddb139c1759c95a0bfdeca99103015c8d89865b0 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 19 Apr 2018 17:49:04 -0600 Subject: [PATCH 243/778] CS fixes --- app/bundles/LeadBundle/Deduplicate/ContactDeduper.php | 3 +-- app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php index cf807a0c572..16ebbd8602a 100644 --- a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php @@ -11,10 +11,9 @@ namespace Mautic\LeadBundle\Deduplicate; -use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; -use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; use Mautic\LeadBundle\Model\FieldModel; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; diff --git a/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php b/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php index 0c846a500d1..0311fc8c8e8 100644 --- a/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php +++ b/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php @@ -21,6 +21,7 @@ class MergeValueHelper * @param null $currentValue * * @return mixed + * * @throws ValueNotMergeable */ public static function getMergeValue($newerValue, $olderValue, $currentValue = null) From ce3646c1c8f75326570402868e626333699e5e60 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 13:20:01 -0500 Subject: [PATCH 244/778] Use DI for command --- ...edupCommand.php => DeduplicateCommand.php} | 35 +++++++++++++++---- app/bundles/LeadBundle/Config/config.php | 10 ++++++ .../LeadBundle/Deduplicate/ContactDeduper.php | 2 +- .../Tests/Deduplicate/ContactDeduperTest.php | 4 +-- 4 files changed, 42 insertions(+), 9 deletions(-) rename app/bundles/LeadBundle/Command/{DedupCommand.php => DeduplicateCommand.php} (66%) diff --git a/app/bundles/LeadBundle/Command/DedupCommand.php b/app/bundles/LeadBundle/Command/DeduplicateCommand.php similarity index 66% rename from app/bundles/LeadBundle/Command/DedupCommand.php rename to app/bundles/LeadBundle/Command/DeduplicateCommand.php index f8e9eccba5a..eb203667ae0 100644 --- a/app/bundles/LeadBundle/Command/DedupCommand.php +++ b/app/bundles/LeadBundle/Command/DeduplicateCommand.php @@ -16,14 +16,39 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Translation\TranslatorInterface; -class DedupCommand extends ModeratedCommand +class DeduplicateCommand extends ModeratedCommand { + /** + * @var ContactDeduper + */ + private $contactDeduper; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * DeduplicateCommand constructor. + * + * @param ContactDeduper $contactDeduper + * @param TranslatorInterface $translator + */ + public function __construct(ContactDeduper $contactDeduper, TranslatorInterface $translator) + { + parent::__construct(); + + $this->contactDeduper = $contactDeduper; + $this->translator = $translator; + } + public function configure() { parent::configure(); - $this->setName('mautic:contacts:dedup') + $this->setName('mautic:contacts:deduplicate') ->setDescription('Merge contacts based on same unique identifiers') ->addOption( '--newer-into-older', @@ -46,14 +71,12 @@ public function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - /** @var ContactDeduper $deduper */ - $deduper = $this->getContainer()->get('mautic.lead.deduper'); $newerIntoOlder = (bool) $input->getOption('newer-into-older'); - $count = $deduper->dedup($newerIntoOlder, $output); + $count = $this->contactDeduper->deduplicate($newerIntoOlder, $output); $output->writeln(''); $output->writeln( - $this->getContainer()->get('translator')->trans( + $this->translator->trans( 'mautic.lead.merge.count', [ '%count%' => $count, diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 9d8bd735590..0a7ee300187 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -926,6 +926,16 @@ ], ], ], + 'command' => [ + 'mautic.lead.command.deduplicate' => [ + 'class' => \Mautic\LeadBundle\Command\DeduplicateCommand::class, + 'arguments' => [ + 'mautic.lead.deduper', + 'translator', + ], + 'tag' => 'console.command', + ], + ], ], 'parameters' => [ 'parallel_import_limit' => 1, diff --git a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php index 16ebbd8602a..703f1f410c2 100644 --- a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php @@ -65,7 +65,7 @@ public function __construct(FieldModel $fieldModel, ContactMerger $merger, LeadR * * @return int */ - public function dedup($mergeNewerIntoOlder = false, OutputInterface $output = null) + public function deduplicate($mergeNewerIntoOlder = false, OutputInterface $output = null) { $this->mergeNewerIntoOlder = $mergeNewerIntoOlder; $lastContactId = 0; diff --git a/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php b/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php index 0a33829899a..0d0c30d88a9 100644 --- a/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php +++ b/app/bundles/LeadBundle/Tests/Deduplicate/ContactDeduperTest.php @@ -83,7 +83,7 @@ public function testDuplicatesAreMergedWithMergeOlderIntoNewer() ->method('merge') ->with($lead4, $lead1); - $this->getDeduper()->dedup(); + $this->getDeduper()->deduplicate(); } public function testDuplicatesAreMergedWithMergeNewerIntoOlder() @@ -120,7 +120,7 @@ public function testDuplicatesAreMergedWithMergeNewerIntoOlder() ->method('merge') ->with($lead1, $lead4); - $this->getDeduper()->dedup(); + $this->getDeduper()->deduplicate(); } /** From b52ae16fa708189113826398ae36ef51dabd9103 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 13:22:28 -0500 Subject: [PATCH 245/778] Renamed class variables --- .../LeadBundle/Deduplicate/ContactDeduper.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php index 703f1f410c2..3956ce07148 100644 --- a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php @@ -28,12 +28,12 @@ class ContactDeduper /** * @var ContactMerger */ - private $merger; + private $contactMerger; /** * @var LeadRepository */ - private $repository; + private $leadRepository; /** * @var array @@ -49,14 +49,14 @@ class ContactDeduper * DedupModel constructor. * * @param FieldModel $fieldModel - * @param ContactMerger $merger - * @param LeadRepository $repository + * @param ContactMerger $contactMerger + * @param LeadRepository $leadRepository */ - public function __construct(FieldModel $fieldModel, ContactMerger $merger, LeadRepository $repository) + public function __construct(FieldModel $fieldModel, ContactMerger $contactMerger, LeadRepository $leadRepository) { - $this->fieldModel = $fieldModel; - $this->merger = $merger; - $this->repository = $repository; + $this->fieldModel = $fieldModel; + $this->contactMerger = $contactMerger; + $this->leadRepository = $leadRepository; } /** @@ -69,7 +69,7 @@ public function deduplicate($mergeNewerIntoOlder = false, OutputInterface $outpu { $this->mergeNewerIntoOlder = $mergeNewerIntoOlder; $lastContactId = 0; - $totalContacts = $this->repository->getIdentifiedContactCount(); + $totalContacts = $this->leadRepository->getIdentifiedContactCount(); $progress = null; if ($output) { @@ -77,7 +77,7 @@ public function deduplicate($mergeNewerIntoOlder = false, OutputInterface $outpu } $dupCount = 0; - while ($contact = $this->repository->getNextIdentifiedContact($lastContactId)) { + while ($contact = $this->leadRepository->getNextIdentifiedContact($lastContactId)) { $lastContactId = $contact->getId(); $fields = $contact->getProfileFields(); $duplicates = $this->checkForDuplicateContacts($fields); @@ -91,7 +91,7 @@ public function deduplicate($mergeNewerIntoOlder = false, OutputInterface $outpu $loser = reset($duplicates); while ($winner = next($duplicates)) { try { - $this->merger->merge($winner, $loser); + $this->contactMerger->merge($winner, $loser); ++$dupCount; @@ -107,7 +107,7 @@ public function deduplicate($mergeNewerIntoOlder = false, OutputInterface $outpu } // Clear all entities in memory for RAM control - $this->repository->clear(); + $this->leadRepository->clear(); gc_collect_cycles(); } @@ -123,7 +123,7 @@ public function checkForDuplicateContacts(array $queryFields) { $duplicates = []; if ($uniqueData = $this->getUniqueData($queryFields)) { - $duplicates = $this->repository->getLeadsByUniqueFields($uniqueData); + $duplicates = $this->leadRepository->getLeadsByUniqueFields($uniqueData); // By default, duplicates are ordered by newest first if (!$this->mergeNewerIntoOlder) { From 7be6d90cef6f1cad735eabe1248b5057b5348889 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 13:25:36 -0500 Subject: [PATCH 246/778] Moved and renamed ValueNotMergeable exception --- .../LeadBundle/Deduplicate/ContactMerger.php | 4 +- .../Exception/ValueNotMergeableException.php | 55 +++++++++++++++++++ .../Deduplicate/Helper/MergeValueHelper.php | 10 ++-- .../Exception/ValueNotMergeable.php | 26 --------- 4 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php delete mode 100644 app/bundles/LeadBundle/Exception/ValueNotMergeable.php diff --git a/app/bundles/LeadBundle/Deduplicate/ContactMerger.php b/app/bundles/LeadBundle/Deduplicate/ContactMerger.php index 05677cf6286..459e967d59a 100644 --- a/app/bundles/LeadBundle/Deduplicate/ContactMerger.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactMerger.php @@ -12,12 +12,12 @@ namespace Mautic\LeadBundle\Deduplicate; use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; +use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException; use Mautic\LeadBundle\Deduplicate\Helper\MergeValueHelper; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\MergeRecord; use Mautic\LeadBundle\Entity\MergeRecordRepository; use Mautic\LeadBundle\Event\LeadMergeEvent; -use Mautic\LeadBundle\Exception\ValueNotMergeable; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Model\LeadModel; use Psr\Log\LoggerInterface; @@ -188,7 +188,7 @@ public function mergeFieldData(Lead $winner, Lead $loser) $fromValue = empty($oldestFields[$field]) ? 'empty' : $oldestFields[$field]; $this->logger->debug("CONTACT: Updated $field from $fromValue to $newValue for {$winner->getId()}"); - } catch (ValueNotMergeable $exception) { + } catch (ValueNotMergeableException $exception) { $this->logger->info("CONTACT: $field is not mergeable for {$winner->getId()} - ".$exception->getMessage()); } } diff --git a/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php b/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php new file mode 100644 index 00000000000..326fc1de15c --- /dev/null +++ b/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php @@ -0,0 +1,55 @@ +newerValue = $newerValue; + $this->olderValue = $olderValue; + + parent::__construct(); + } + + /** + * @return mixed + */ + public function getNewerValue() + { + return $this->newerValue; + } + + /** + * @return mixed + */ + public function getOlderValue() + { + return $this->olderValue; + } +} diff --git a/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php b/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php index 0311fc8c8e8..649cc8a3eab 100644 --- a/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php +++ b/app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php @@ -11,7 +11,7 @@ namespace Mautic\LeadBundle\Deduplicate\Helper; -use Mautic\LeadBundle\Exception\ValueNotMergeable; +use Mautic\LeadBundle\Deduplicate\Exception\ValueNotMergeableException; class MergeValueHelper { @@ -22,16 +22,16 @@ class MergeValueHelper * * @return mixed * - * @throws ValueNotMergeable + * @throws ValueNotMergeableException */ public static function getMergeValue($newerValue, $olderValue, $currentValue = null) { if ($newerValue === $olderValue) { - throw new ValueNotMergeable($newerValue, $olderValue); + throw new ValueNotMergeableException($newerValue, $olderValue); } if (null !== $currentValue && $newerValue === $currentValue) { - throw new ValueNotMergeable($newerValue, $olderValue); + throw new ValueNotMergeableException($newerValue, $olderValue); } if (self::isNotEmpty($newerValue)) { @@ -42,7 +42,7 @@ public static function getMergeValue($newerValue, $olderValue, $currentValue = n return $olderValue; } - throw new ValueNotMergeable($newerValue, $olderValue); + throw new ValueNotMergeableException($newerValue, $olderValue); } /** diff --git a/app/bundles/LeadBundle/Exception/ValueNotMergeable.php b/app/bundles/LeadBundle/Exception/ValueNotMergeable.php deleted file mode 100644 index 2c69040a303..00000000000 --- a/app/bundles/LeadBundle/Exception/ValueNotMergeable.php +++ /dev/null @@ -1,26 +0,0 @@ - Date: Thu, 26 Apr 2018 13:26:06 -0500 Subject: [PATCH 247/778] Fixed docblock --- .../Deduplicate/Exception/ValueNotMergeableException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php b/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php index 326fc1de15c..179e802ef0e 100644 --- a/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php +++ b/app/bundles/LeadBundle/Deduplicate/Exception/ValueNotMergeableException.php @@ -24,7 +24,7 @@ class ValueNotMergeableException extends \Exception private $olderValue; /** - * ValueNotMergeable constructor. + * ValueNotMergeableException constructor. * * @param mixed $newerValue * @param mixed $olderValue From 3a7151a7fe92552c23e832d0b4c67bf275b531c7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 14:54:01 -0500 Subject: [PATCH 248/778] Use a LegacyLeadModel to work around circular dependency for merging leads --- app/bundles/LeadBundle/Config/config.php | 8 + .../LeadBundle/Controller/LeadController.php | 9 +- app/bundles/LeadBundle/Model/LeadModel.php | 157 +++--------------- .../LeadBundle/Model/LegacyLeadModel.php | 70 ++++++++ 4 files changed, 113 insertions(+), 131 deletions(-) create mode 100644 app/bundles/LeadBundle/Model/LegacyLeadModel.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 0a7ee300187..5e3de8816a5 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -740,6 +740,13 @@ 'mautic.lead.repository.lead', ], ], + // Deprecated support for circular dependency + 'mautic.lead.legacy_merger' => [ + 'class' => \Mautic\LeadBundle\Deduplicate\LegacyContactMerger::class, + 'arguments' => [ + 'service_container', + ], + ], ], 'repositories' => [ 'mautic.lead.repository.dnc' => [ @@ -809,6 +816,7 @@ 'mautic.user.provider', 'mautic.tracker.contact', 'mautic.tracker.device', + 'mautic.lead.legacy_merger', ], ], 'mautic.lead.model.field' => [ diff --git a/app/bundles/LeadBundle/Controller/LeadController.php b/app/bundles/LeadBundle/Controller/LeadController.php index 2e3478e839b..ffa5b1512ff 100644 --- a/app/bundles/LeadBundle/Controller/LeadController.php +++ b/app/bundles/LeadBundle/Controller/LeadController.php @@ -15,6 +15,8 @@ use Mautic\CoreBundle\Helper\EmojiHelper; use Mautic\CoreBundle\Model\IteratorExportDataModel; use Mautic\LeadBundle\DataObject\LeadManipulator; +use Mautic\LeadBundle\Deduplicate\ContactMerger; +use Mautic\LeadBundle\Deduplicate\Exception\SameContactException; use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; @@ -873,7 +875,12 @@ public function mergeAction($objectId) } //Both leads are good so now we merge them - $mainLead = $model->mergeLeads($mainLead, $secLead, false); + /** @var ContactMerger $contactMerger */ + $contactMerger = $this->container->get('mautic.lead.merger'); + try { + $mainLead = $contactMerger->merge($mainLead, $secLead); + } catch (SameContactException $exception) { + } } } diff --git a/app/bundles/LeadBundle/Model/LeadModel.php b/app/bundles/LeadBundle/Model/LeadModel.php index 003a9698a16..eb4936bb1c7 100644 --- a/app/bundles/LeadBundle/Model/LeadModel.php +++ b/app/bundles/LeadBundle/Model/LeadModel.php @@ -27,7 +27,6 @@ use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Helper\PathsHelper; use Mautic\CoreBundle\Model\FormModel; -use Mautic\EmailBundle\Entity\Stat; use Mautic\EmailBundle\Entity\StatRepository; use Mautic\EmailBundle\Helper\EmailValidator; use Mautic\LeadBundle\DataObject\LeadManipulator; @@ -41,7 +40,6 @@ use Mautic\LeadBundle\Entity\LeadEventLog; use Mautic\LeadBundle\Entity\LeadField; use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Entity\MergeRecord; use Mautic\LeadBundle\Entity\OperatorListTrait; use Mautic\LeadBundle\Entity\PointsChangeLog; use Mautic\LeadBundle\Entity\StagesChangeLog; @@ -49,7 +47,6 @@ use Mautic\LeadBundle\Entity\UtmTag; use Mautic\LeadBundle\Event\CategoryChangeEvent; use Mautic\LeadBundle\Event\LeadEvent; -use Mautic\LeadBundle\Event\LeadMergeEvent; use Mautic\LeadBundle\Event\LeadTimelineEvent; use Mautic\LeadBundle\Helper\ContactRequestHelper; use Mautic\LeadBundle\Helper\IdentifyCompanyHelper; @@ -176,6 +173,11 @@ class LeadModel extends FormModel */ private $deviceTracker; + /** + * @var LegacyLeadModel + */ + private $legacyLeadModel; + /** * @var bool */ @@ -210,6 +212,7 @@ class LeadModel extends FormModel * @param UserProvider $userProvider * @param ContactTracker $contactTracker * @param DeviceTracker $deviceTracker + * @param LegacyLeadModel $legacyLeadModel */ public function __construct( RequestStack $requestStack, @@ -227,24 +230,26 @@ public function __construct( EmailValidator $emailValidator, UserProvider $userProvider, ContactTracker $contactTracker, - DeviceTracker $deviceTracker + DeviceTracker $deviceTracker, + LegacyLeadModel $legacyLeadModel ) { - $this->request = $requestStack->getCurrentRequest(); - $this->cookieHelper = $cookieHelper; - $this->ipLookupHelper = $ipLookupHelper; - $this->pathsHelper = $pathsHelper; - $this->integrationHelper = $integrationHelper; - $this->leadFieldModel = $leadFieldModel; - $this->leadListModel = $leadListModel; - $this->companyModel = $companyModel; - $this->formFactory = $formFactory; - $this->categoryModel = $categoryModel; - $this->channelListHelper = $channelListHelper; - $this->coreParametersHelper = $coreParametersHelper; - $this->emailValidator = $emailValidator; - $this->userProvider = $userProvider; - $this->contactTracker = $contactTracker; - $this->deviceTracker = $deviceTracker; + $this->request = $requestStack->getCurrentRequest(); + $this->cookieHelper = $cookieHelper; + $this->ipLookupHelper = $ipLookupHelper; + $this->pathsHelper = $pathsHelper; + $this->integrationHelper = $integrationHelper; + $this->leadFieldModel = $leadFieldModel; + $this->leadListModel = $leadListModel; + $this->companyModel = $companyModel; + $this->formFactory = $formFactory; + $this->categoryModel = $categoryModel; + $this->channelListHelper = $channelListHelper; + $this->coreParametersHelper = $coreParametersHelper; + $this->emailValidator = $emailValidator; + $this->userProvider = $userProvider; + $this->contactTracker = $contactTracker; + $this->deviceTracker = $deviceTracker; + $this->legacyLeadModel = $legacyLeadModel; } /** @@ -2427,16 +2432,6 @@ private function processManipulator(Lead $lead) } } - /** - * @param Lead $trackedLead - * @param array $queryFields - * - * @return Lead - */ - private function getContactFromClickthrough(Lead $trackedLead, array &$queryFields) - { - } - /** * @param IpAddress $ip * @param bool $persist @@ -2688,7 +2683,7 @@ public function setSystemCurrentLead(Lead $lead = null) /** * Merge two leads; if a conflict of data occurs, the newest lead will get precedence. * - * @deprecated 2.13.0; to be removed in 3.0. Use \Mautic\LeadBundle\Model\MergeModel instead + * @deprecated 2.13.0; to be removed in 3.0. Use \Mautic\LeadBundle\Deduplicate\ContactMerger instead * * @param Lead $lead * @param Lead $lead2 @@ -2698,104 +2693,6 @@ public function setSystemCurrentLead(Lead $lead = null) */ public function mergeLeads(Lead $lead, Lead $lead2, $autoMode = true) { - $this->logger->debug('LEAD: Merging leads'); - - $leadId = $lead->getId(); - $lead2Id = $lead2->getId(); - - //if they are the same lead, then just return one - if ($leadId === $lead2Id) { - $this->logger->debug('LEAD: Leads are the same'); - - return $lead; - } - - if ($autoMode) { - //which lead is the oldest? - $mergeWith = ($lead->getDateAdded() < $lead2->getDateAdded()) ? $lead : $lead2; - $mergeFrom = ($mergeWith->getId() === $leadId) ? $lead2 : $lead; - } else { - $mergeWith = $lead2; - $mergeFrom = $lead; - } - $this->logger->debug('LEAD: Lead ID# '.$mergeFrom->getId().' will be merged into ID# '.$mergeWith->getId()); - - //dispatch pre merge event - $event = new LeadMergeEvent($mergeWith, $mergeFrom); - if ($this->dispatcher->hasListeners(LeadEvents::LEAD_PRE_MERGE)) { - $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_MERGE, $event); - } - - //merge IP addresses - $ipAddresses = $mergeFrom->getIpAddresses(); - foreach ($ipAddresses as $ip) { - $mergeWith->addIpAddress($ip); - - $this->logger->debug('LEAD: Associating with IP '.$ip->getIpAddress()); - } - - //merge fields - $mergeFromFields = $mergeFrom->getFields(); - foreach ($mergeFromFields as $group => $groupFields) { - foreach ($groupFields as $alias => $details) { - if ('points' === $alias) { - // We have to ignore this as it's a special field and it will reset the points for the contact - continue; - } - - //overwrite old lead's data with new lead's if new lead's is not empty - if (!empty($details['value'])) { - $mergeWith->addUpdatedField($alias, $details['value']); - - $this->logger->debug('LEAD: Updated '.$alias.' = '.$details['value']); - } - } - } - - //merge owner - $oldOwner = $mergeWith->getOwner(); - $newOwner = $mergeFrom->getOwner(); - - if ($oldOwner === null && $newOwner !== null) { - $mergeWith->setOwner($newOwner); - - $this->logger->debug('LEAD: New owner is '.$newOwner->getId()); - } - - // Sum points - $mergeFromPoints = $mergeFrom->getPoints(); - $mergeWithPoints = $mergeWith->getPoints(); - $mergeWith->adjustPoints($mergeFromPoints); - $this->logger->debug('LEAD: Adding '.$mergeFromPoints.' points from lead ID #'.$mergeFrom->getId().' to lead ID #'.$mergeWith->getId().' with '.$mergeWithPoints.' points'); - - //merge tags - $mergeFromTags = $mergeFrom->getTags(); - $addTags = $mergeFromTags->getKeys(); - $this->modifyTags($mergeWith, $addTags, null, false); - - //save the updated lead - $this->saveEntity($mergeWith, false); - - // Update merge records for the lead about to be deleted - $this->getMergeRecordRepository()->moveMergeRecord($mergeFrom->getId(), $mergeWith->getId()); - - // Create an entry this contact was merged - $mergeRecord = new MergeRecord(); - $mergeRecord->setContact($mergeWith) - ->setDateAdded() - ->setName($mergeFrom->getPrimaryIdentifier()) - ->setMergedId($mergeFrom->getId()); - $this->getMergeRecordRepository()->saveEntity($mergeRecord); - - //post merge events - if ($this->dispatcher->hasListeners(LeadEvents::LEAD_POST_MERGE)) { - $this->dispatcher->dispatch(LeadEvents::LEAD_POST_MERGE, $event); - } - - //delete the old - $this->deleteEntity($mergeFrom); - - //return the merged lead - return $mergeWith; + return $this->legacyLeadModel->mergeLeads($lead, $lead2, $autoMode); } } diff --git a/app/bundles/LeadBundle/Model/LegacyLeadModel.php b/app/bundles/LeadBundle/Model/LegacyLeadModel.php new file mode 100644 index 00000000000..f3f92b6caea --- /dev/null +++ b/app/bundles/LeadBundle/Model/LegacyLeadModel.php @@ -0,0 +1,70 @@ +container = $container; + } + + /** + * @param Lead $lead + * @param Lead $lead2 + * @param bool $autoMode + * + * @return Lead + */ + public function mergeLeads(Lead $lead, Lead $lead2, $autoMode = true) + { + $leadId = $lead->getId(); + + if ($autoMode) { + //which lead is the oldest? + $winner = ($lead->getDateAdded() < $lead2->getDateAdded()) ? $lead : $lead2; + $loser = ($winner->getId() === $leadId) ? $lead2 : $lead; + } else { + $winner = $lead2; + $loser = $lead; + } + + try { + /** @var ContactMerger $contactMerger */ + $contactMerger = $this->container->get('mautic.lead.merger'); + + return $contactMerger->merge($winner, $loser); + } catch (SameContactException $exception) { + return $lead; + } + } +} From 0d1072e7ee1108462d49fce1098f6259ad43e936 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 15:16:56 -0500 Subject: [PATCH 249/778] Fixed DI --- app/bundles/LeadBundle/Config/config.php | 17 +++++++++-------- .../LeadBundle/Model/LegacyLeadModel.php | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 5e3de8816a5..f67f437b84e 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -740,13 +740,6 @@ 'mautic.lead.repository.lead', ], ], - // Deprecated support for circular dependency - 'mautic.lead.legacy_merger' => [ - 'class' => \Mautic\LeadBundle\Deduplicate\LegacyContactMerger::class, - 'arguments' => [ - 'service_container', - ], - ], ], 'repositories' => [ 'mautic.lead.repository.dnc' => [ @@ -816,7 +809,15 @@ 'mautic.user.provider', 'mautic.tracker.contact', 'mautic.tracker.device', - 'mautic.lead.legacy_merger', + 'mautic.lead.model.legacy_lead', + ], + ], + + // Deprecated support for circular dependency + 'mautic.lead.model.legacy_lead' => [ + 'class' => \Mautic\LeadBundle\Model\LegacyLeadModel::class, + 'arguments' => [ + 'service_container', ], ], 'mautic.lead.model.field' => [ diff --git a/app/bundles/LeadBundle/Model/LegacyLeadModel.php b/app/bundles/LeadBundle/Model/LegacyLeadModel.php index f3f92b6caea..dfe5efd1a25 100644 --- a/app/bundles/LeadBundle/Model/LegacyLeadModel.php +++ b/app/bundles/LeadBundle/Model/LegacyLeadModel.php @@ -19,7 +19,7 @@ /** * Class LegacyLeadModel. * - * @deprecated Used temporarily to get around circular depdenency for LeadModel + * @deprecated 2.14.0 to be removed in 3.0; Used temporarily to get around circular depdenency for LeadModel */ class LegacyLeadModel { From e8d5fc3d2c0de2ab08e8373f207bcd0a0a0296f1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 15:28:08 -0500 Subject: [PATCH 250/778] Prevent `.1` from appending to theme filepath --- app/bundles/CoreBundle/Helper/ThemeHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/CoreBundle/Helper/ThemeHelper.php b/app/bundles/CoreBundle/Helper/ThemeHelper.php index d6e7b8d8fee..ce6d1d365fb 100644 --- a/app/bundles/CoreBundle/Helper/ThemeHelper.php +++ b/app/bundles/CoreBundle/Helper/ThemeHelper.php @@ -137,7 +137,7 @@ public function createThemeHelper($themeName) */ private function getDirectoryName($newName) { - return InputHelper::filename($newName, true); + return InputHelper::filename($newName); } /** From 7de532f08fd278c9a1ed26b91a16c909a4179c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=20=E2=98=95?= Date: Tue, 1 May 2018 08:26:46 -0400 Subject: [PATCH 251/778] [Enhancement] Allow filtering contacts by UTM data for segments. (#5886) * Filter a segment by UTM data. * PHPCS fixes. * Reverse CS fixes for LeadListRepository --- .../LeadBundle/Entity/LeadListRepository.php | 27 ++++++++++ app/bundles/LeadBundle/Model/ListModel.php | 52 ++++++++++++++++--- .../Translations/en_US/messages.ini | 5 ++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 8cdfbae108c..06e349927d2 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -618,6 +618,7 @@ protected function generateSegmentExpression(array $filters, array &$parameters, } else { continue; } + // no break case is_bool($v): $paramType = 'boolean'; break; @@ -1617,6 +1618,32 @@ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNo $groupExpr->add(sprintf('%s (%s)', $operand, $subQb->getSQL())); + break; + case 'utm_campaign': + case 'utm_content': + case 'utm_medium': + case 'utm_source': + case 'utm_term': + // Special handling of lead lists and utmtags + $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + + $table = 'lead_utmtags'; + $column = $details['field']; + + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $details['filter'], + $parameters, + $leadId + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); break; default: if (!$column) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 5647a3a9f4d..e728f7a5849 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -180,7 +180,7 @@ public function createForm($entity, $formFactory, $action = null, $options = []) */ public function getEntity($id = null) { - if ($id === null) { + if (null === $id) { return new LeadList(); } @@ -668,6 +668,46 @@ public function getChoiceFields() 'operators' => $this->getOperatorsForFieldType('multiselect'), 'object' => 'lead', ], + 'utm_campaign' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmcampaign'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_content' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmcontent'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_medium' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmmedium'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_source' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmsource'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_term' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmterm'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], ]; // Add custom choices @@ -702,7 +742,7 @@ public function getChoiceFields() $properties = $field->getProperties(); $properties['type'] = $type; if (in_array($type, ['lookup', 'multiselect', 'boolean'])) { - if ($type == 'boolean') { + if ('boolean' == $type) { //create a lookup list with ID $properties['list'] = [ 0 => $properties['no'], @@ -1010,7 +1050,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa */ public function addLead($lead, $lists, $manuallyAdded = false, $batchProcess = false, $searchListLead = 1, $dateManipulated = null) { - if ($dateManipulated == null) { + if (null == $dateManipulated) { $dateManipulated = new \DateTime(); } @@ -1087,7 +1127,7 @@ public function addLead($lead, $lists, $manuallyAdded = false, $batchProcess = f ); } - if ($listLead != null) { + if (null != $listLead) { if ($manuallyAdded && $listLead->wasManuallyRemoved()) { $listLead->setManuallyRemoved(false); $listLead->setManuallyAdded($manuallyAdded); @@ -1213,7 +1253,7 @@ public function removeLead($lead, $lists, $manuallyRemoved = false, $batchProces 'list' => $listId, ]); - if ($listLead == null) { + if (null == $listLead) { // Lead is not part of this list continue; } @@ -1278,7 +1318,7 @@ public function getLeadsByList($lists, $idOnly = false, $args = []) protected function batchSleep() { $leadSleepTime = $this->coreParametersHelper->getParameter('batch_lead_sleep_time', false); - if ($leadSleepTime === false) { + if (false === $leadSleepTime) { $leadSleepTime = $this->coreParametersHelper->getParameter('batch_sleep_time', 1); } diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index 4fa31050e67..0b3eb2ed9c0 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -653,6 +653,11 @@ mautic.lead.lead.events.changecompanyscore="Add to company's score" mautic.lead.lead.events.changecompanyscore_descr="This action will add the specified value to the company's existing score" mautic.lead.timeline.displaying_events_for_contact="for contact: %contact% (%id%)" mautic.lead.list.filter.categories="Subscribed Categories" +mautic.lead.list.filter.utmcampaign="UTM Campaign" +mautic.lead.list.filter.utmcontent="UTM Content" +mautic.lead.list.filter.utmmedium="UTM Medium" +mautic.lead.list.filter.utmsource="UTM Source" +mautic.lead.list.filter.utmterm="UTM Term" mautic.lead.audit.created="The contact was created." mautic.lead.audit.deleted="The contact was deleted." mautic.lead.audit.updated="The contact was updated." From 86f48ac2bd8f82ed3157f793b3e4a8953f13b461 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 1 May 2018 10:44:00 -0500 Subject: [PATCH 252/778] Fixed tests for Travis --- .../DashboardBundle/Tests/Controller/DashboardControllerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php index 02f4d2e0b0c..dae5441513d 100644 --- a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php +++ b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php @@ -35,6 +35,7 @@ class DashboardControllerTest extends \PHPUnit_Framework_TestCase private $sessionMock; private $flashBagMock; private $containerMock; + private $controller; protected function setUp() { From e6eababe0e32b5f7ca003b465b28dc196975fb76 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 1 May 2018 11:03:29 -0500 Subject: [PATCH 253/778] Fixed tests for Travis --- .../Tests/Controller/DashboardControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php index dae5441513d..54f9b458fef 100644 --- a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php +++ b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php @@ -137,7 +137,7 @@ public function testSaveWithPostAjaxWillSave() ->with('session') ->willReturn($this->sessionMock); - $this->routerMock->expects($this->any(0)) + $this->routerMock->expects($this->any()) ->method('generate') ->willReturn('https://some.url'); From eabcb59ad4e6277d200f9d609024c90607433cf7 Mon Sep 17 00:00:00 2001 From: scottshipman Date: Wed, 2 May 2018 15:50:39 -0400 Subject: [PATCH 254/778] [ENG-276] All plugins to modify report query prior to execution --- .../ReportBundle/Event/ReportQueryEvent.php | 87 +++++++++++++++++++ .../ReportBundle/Model/ReportModel.php | 17 ++-- app/bundles/ReportBundle/ReportEvents.php | 9 ++ 3 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 app/bundles/ReportBundle/Event/ReportQueryEvent.php diff --git a/app/bundles/ReportBundle/Event/ReportQueryEvent.php b/app/bundles/ReportBundle/Event/ReportQueryEvent.php new file mode 100644 index 00000000000..5eadebb31d0 --- /dev/null +++ b/app/bundles/ReportBundle/Event/ReportQueryEvent.php @@ -0,0 +1,87 @@ +context = $report->getSource(); + $this->report = $report; + $this->query = $query; + $this->options = $options; + $this->totalResults = (int) $totalResults; + } + + /** + * @return array + */ + public function getQuery() + { + return $this->query; + } + + /** + * @param QueryBuilder $query + * + * @return ReportDataEvent + */ + public function setQuery($query) + { + $this->query = $query; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @return int + */ + public function getTotalResults() + { + return $this->totalResults; + } +} diff --git a/app/bundles/ReportBundle/Model/ReportModel.php b/app/bundles/ReportBundle/Model/ReportModel.php index 877e794edca..7ed1bde6b4c 100644 --- a/app/bundles/ReportBundle/Model/ReportModel.php +++ b/app/bundles/ReportBundle/Model/ReportModel.php @@ -26,6 +26,7 @@ use Mautic\ReportBundle\Event\ReportDataEvent; use Mautic\ReportBundle\Event\ReportEvent; use Mautic\ReportBundle\Event\ReportGraphEvent; +use Mautic\ReportBundle\Event\ReportQueryEvent; use Mautic\ReportBundle\Generator\ReportGenerator; use Mautic\ReportBundle\Helper\ReportHelper; use Mautic\ReportBundle\ReportEvents; @@ -172,7 +173,7 @@ public function createForm($entity, $formFactory, $action = null, $options = []) */ public function getEntity($id = null) { - if ($id === null) { + if (null === $id) { return new Report(); } @@ -239,7 +240,7 @@ public function buildAvailableReports($context) $data[$context]['graphs'] = &$data['all']['graphs'][$context]; } else { //build them - $eventContext = ($context == 'all') ? '' : $context; + $eventContext = ('all' == $context) ? '' : $context; $event = new ReportBuilderEvent($this->translator, $this->channelListHelper, $eventContext, $this->fieldModel->getPublishedFieldArrays(), $this->reportHelper); $this->dispatcher->dispatch(ReportEvents::REPORT_ON_BUILD, $event); @@ -247,7 +248,7 @@ public function buildAvailableReports($context) $tables = $event->getTables(); $graphs = $event->getGraphs(); - if ($context == 'all') { + if ('all' == $context) { $data[$context]['tables'] = $tables; $data[$context]['graphs'] = $graphs; } else { @@ -310,7 +311,7 @@ public function getColumnList($context, $isGroupBy = false) $return->definitions = []; foreach ($columns as $column => $data) { - if ($isGroupBy && ($column == 'unsubscribed' || $column == 'unsubscribed_ratio' || $column == 'unique_ratio')) { + if ($isGroupBy && ('unsubscribed' == $column || 'unsubscribed_ratio' == $column || 'unique_ratio' == $column)) { continue; } if (isset($data['label'])) { @@ -599,7 +600,7 @@ public function getReportData(Report $entity, FormFactoryInterface $formFactory $limit = $options['limit']; $reportPage = $options['page']; } - $start = ($reportPage === 1) ? 0 : (($reportPage - 1) * $limit); + $start = (1 === $reportPage) ? 0 : (($reportPage - 1) * $limit); if ($start < 0) { $start = 0; } @@ -612,6 +613,12 @@ public function getReportData(Report $entity, FormFactoryInterface $formFactory } $query->add('orderBy', $order); + + // Allow plugin to manipulate the query + $event = new ReportQueryEvent($entity, $query, $totalResults, $dataOptions); + $this->dispatcher->dispatch(ReportEvents::REPORT_QUERY_PRE_EXECUTE, $event); + $query = $event->getQuery(); + $queryTime = microtime(true); $data = $query->execute()->fetchAll(); $queryTime = round((microtime(true) - $queryTime) * 1000); diff --git a/app/bundles/ReportBundle/ReportEvents.php b/app/bundles/ReportBundle/ReportEvents.php index bd2d7049bdf..60ebb39f5b4 100644 --- a/app/bundles/ReportBundle/ReportEvents.php +++ b/app/bundles/ReportBundle/ReportEvents.php @@ -73,6 +73,15 @@ final class ReportEvents */ const REPORT_ON_GENERATE = 'mautic.report_on_generate'; + /** + * The mautic.report_query_pre_execute event is dispatched to allow a plugin to alter the query before execution. + * + * The event listener receives a Mautic\ReportBundle\Event\ReportQueryEvent instance. + * + * @var string + */ + const REPORT_QUERY_PRE_EXECUTE = 'mautic.report_query_pre_execute'; + /** * The mautic.report_on_display event is dispatched when displaying a report. * From 0195bd47195dddb66030266f78571d9c255849c4 Mon Sep 17 00:00:00 2001 From: scottshipman Date: Wed, 2 May 2018 16:04:12 -0400 Subject: [PATCH 255/778] [ENG-276] update const definition --- app/bundles/ReportBundle/Event/ReportQueryEvent.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/ReportBundle/Event/ReportQueryEvent.php b/app/bundles/ReportBundle/Event/ReportQueryEvent.php index 5eadebb31d0..f49572092a5 100644 --- a/app/bundles/ReportBundle/Event/ReportQueryEvent.php +++ b/app/bundles/ReportBundle/Event/ReportQueryEvent.php @@ -20,9 +20,9 @@ class ReportQueryEvent extends AbstractReportEvent { /** - * @var array + * @var QueryBuilder */ - private $query = []; + private $query; /** * @var array From 102322990dfc1e24eb2e55c0d5275ca083ba74b7 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 3 May 2018 10:26:34 +0200 Subject: [PATCH 256/778] Validate time units for data API endpoint --- .../CoreBundle/Helper/DateTimeHelper.php | 18 ++++++++++++++++++ .../Tests/unit/Helper/DateTimeHelperTest.php | 17 +++++++++++++++++ .../Controller/Api/WidgetApiController.php | 8 ++++++++ 3 files changed, 43 insertions(+) diff --git a/app/bundles/CoreBundle/Helper/DateTimeHelper.php b/app/bundles/CoreBundle/Helper/DateTimeHelper.php index b556bdf1078..a6d04970c13 100644 --- a/app/bundles/CoreBundle/Helper/DateTimeHelper.php +++ b/app/bundles/CoreBundle/Helper/DateTimeHelper.php @@ -388,4 +388,22 @@ public function guessTimezoneFromOffset($offset = 0) return $timezone; } + + /** + * @param string $unit + * + * @throws \InvalidArgumentException + */ + public static function validateMysqlDateTimeUnit($unit) + { + $possibleUnits = ['s', 'i', 'H', 'd', 'W', 'm', 'Y']; + $timeUnitInArray = array_filter($possibleUnits, function ($possibleUnit) use ($unit) { + return $unit === $possibleUnit; + }); + + if (!$timeUnitInArray) { + $possibleUnitsString = implode(', ', $possibleUnits); + throw new \InvalidArgumentException("Unit '$unit' is not supported. Use one of these: $possibleUnitsString"); + } + } } diff --git a/app/bundles/CoreBundle/Tests/unit/Helper/DateTimeHelperTest.php b/app/bundles/CoreBundle/Tests/unit/Helper/DateTimeHelperTest.php index e557455a4a7..1ed34c1ad92 100644 --- a/app/bundles/CoreBundle/Tests/unit/Helper/DateTimeHelperTest.php +++ b/app/bundles/CoreBundle/Tests/unit/Helper/DateTimeHelperTest.php @@ -55,4 +55,21 @@ public function testBuildIntervalWithRightUnits() $interval = $helper->buildInterval(4, 'S'); $this->assertEquals(new \DateInterval('PT4S'), $interval); } + + public function testvalidateMysqlDateTimeUnitWillThrowExceptionOnBadUnit() + { + $this->expectException(\InvalidArgumentException::class); + DateTimeHelper::validateMysqlDateTimeUnit('D'); + } + + public function testvalidateMysqlDateTimeUnitWillNotThrowExceptionOnExpectedUnit() + { + DateTimeHelper::validateMysqlDateTimeUnit('s'); + DateTimeHelper::validateMysqlDateTimeUnit('i'); + DateTimeHelper::validateMysqlDateTimeUnit('H'); + DateTimeHelper::validateMysqlDateTimeUnit('d'); + DateTimeHelper::validateMysqlDateTimeUnit('W'); + DateTimeHelper::validateMysqlDateTimeUnit('m'); + DateTimeHelper::validateMysqlDateTimeUnit('Y'); + } } diff --git a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php index e36ca5aa363..e46463353e9 100644 --- a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php +++ b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php @@ -13,6 +13,7 @@ use FOS\RestBundle\Util\Codes; use Mautic\ApiBundle\Controller\CommonApiController; +use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\InputHelper; use Mautic\DashboardBundle\DashboardEvents; use Mautic\DashboardBundle\Entity\Widget; @@ -65,8 +66,15 @@ public function getDataAction($type) $from = InputHelper::clean($this->request->get('dateFrom', null)); $to = InputHelper::clean($this->request->get('dateTo', null)); $dataFormat = InputHelper::clean($this->request->get('dataFormat', null)); + $unit = InputHelper::clean($this->request->get('timeUnit', 'Y')); $response = ['success' => 0]; + try { + DateTimeHelper::validateMysqlDateTimeUnit($unit); + } catch (\InvalidArgumentException $e) { + return $this->returnError($e->getMessage(), Codes::HTTP_BAD_REQUEST); + } + if ($timezone) { $fromDate = new \DateTime($from, new \DateTimeZone($timezone)); $toDate = new \DateTime($to, new \DateTimeZone($timezone)); From 75aefac89f6e4538d2e2ac2d896fc0be1d2767fa Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 3 May 2018 15:33:54 +0200 Subject: [PATCH 257/778] Use === null for valid 0 values --- app/bundles/DashboardBundle/Model/DashboardModel.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/bundles/DashboardBundle/Model/DashboardModel.php b/app/bundles/DashboardBundle/Model/DashboardModel.php index 8bd954060c8..850f47a236a 100644 --- a/app/bundles/DashboardBundle/Model/DashboardModel.php +++ b/app/bundles/DashboardBundle/Model/DashboardModel.php @@ -234,11 +234,11 @@ public function populateWidgetEntity(array $data) * @param Widget $widget * @param array $filter */ - public function populateWidgetContent(Widget &$widget, $filter = []) + public function populateWidgetContent(Widget $widget, $filter = []) { $cacheDir = $this->coreParametersHelper->getParameter('cached_data_dir', $this->pathsHelper->getSystemPath('cache', true)); - if ($widget->getCacheTimeout() == null || $widget->getCacheTimeout() == -1) { + if ($widget->getCacheTimeout() === null || $widget->getCacheTimeout() === -1) { $widget->setCacheTimeout($this->coreParametersHelper->getParameter('cached_data_timeout')); } @@ -262,7 +262,6 @@ public function populateWidgetContent(Widget &$widget, $filter = []) $event = new WidgetDetailEvent($this->translator); $event->setWidget($widget); - $event->setCacheDir($cacheDir, $this->userHelper->getUser()->getId()); $event->setSecurity($this->security); $this->dispatcher->dispatch(DashboardEvents::DASHBOARD_ON_MODULE_DETAIL_GENERATE, $event); From 8556a5890d2265e3e7ad8851f07110e03b6b686a Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 4 May 2018 08:36:47 +0200 Subject: [PATCH 258/778] fix incorrect logic for related segments --- .../Query/Filter/SegmentReferenceFilterQueryBuilder.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index 6701e82aae7..caa94734a82 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -14,6 +14,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\ContactSegmentFilterFactory; use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -84,6 +85,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $segmentIds = [intval($segmentIds)]; } + $orLogic = []; + foreach ($segmentIds as $segmentId) { $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); @@ -120,13 +123,17 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); if (!$exclusion && count($segmentIds) > 1) { - $queryBuilder->addLogic($expression, 'or'); + $orLogic[] = $expression; } else { $queryBuilder->addLogic($expression, $filter->getGlue()); } } } + if (count($orLogic)) { + $queryBuilder->andWhere(new CompositeExpression(CompositeExpression::TYPE_OR, $orLogic)); + } + return $queryBuilder; } } From da84fc8915c7276d452e7cbfe4104d573b3eaa15 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 4 May 2018 14:14:11 +0200 Subject: [PATCH 259/778] Replace array filter with in array function (strict mode) --- app/bundles/CoreBundle/Helper/DateTimeHelper.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/bundles/CoreBundle/Helper/DateTimeHelper.php b/app/bundles/CoreBundle/Helper/DateTimeHelper.php index a6d04970c13..11ce4c3fa4b 100644 --- a/app/bundles/CoreBundle/Helper/DateTimeHelper.php +++ b/app/bundles/CoreBundle/Helper/DateTimeHelper.php @@ -397,11 +397,8 @@ public function guessTimezoneFromOffset($offset = 0) public static function validateMysqlDateTimeUnit($unit) { $possibleUnits = ['s', 'i', 'H', 'd', 'W', 'm', 'Y']; - $timeUnitInArray = array_filter($possibleUnits, function ($possibleUnit) use ($unit) { - return $unit === $possibleUnit; - }); - if (!$timeUnitInArray) { + if (!in_array($unit, $possibleUnits, true)) { $possibleUnitsString = implode(', ', $possibleUnits); throw new \InvalidArgumentException("Unit '$unit' is not supported. Use one of these: $possibleUnitsString"); } From be1c6c9324c6d8ae79ab7c849615d32179309b52 Mon Sep 17 00:00:00 2001 From: scottshipman Date: Mon, 7 May 2018 11:10:21 -0400 Subject: [PATCH 260/778] add queryBuilder setter to ReportGraphEvent so the query can be altered prior to execution --- app/bundles/ReportBundle/Event/ReportGraphEvent.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/bundles/ReportBundle/Event/ReportGraphEvent.php b/app/bundles/ReportBundle/Event/ReportGraphEvent.php index 43ca83badea..549336da574 100644 --- a/app/bundles/ReportBundle/Event/ReportGraphEvent.php +++ b/app/bundles/ReportBundle/Event/ReportGraphEvent.php @@ -124,4 +124,12 @@ public function getQueryBuilder() { return $this->queryBuilder; } + + /** + * @param QueryBuilder $queryBuilder + */ + public function setQueryBuilder(QueryBuilder $queryBuilder) + { + $this->queryBuilder = $queryBuilder; + } } From add9c99aee9b4ad3283daa27d00e181703f8f118 Mon Sep 17 00:00:00 2001 From: scottshipman Date: Mon, 7 May 2018 13:22:00 -0400 Subject: [PATCH 261/778] chanfe doc block copywrite date --- app/bundles/ReportBundle/Event/ReportGraphEvent.php | 2 +- app/bundles/ReportBundle/Event/ReportQueryEvent.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/ReportBundle/Event/ReportGraphEvent.php b/app/bundles/ReportBundle/Event/ReportGraphEvent.php index 549336da574..1c62651b1b5 100644 --- a/app/bundles/ReportBundle/Event/ReportGraphEvent.php +++ b/app/bundles/ReportBundle/Event/ReportGraphEvent.php @@ -57,7 +57,7 @@ public function getGraphs() * Set the graph array. * * @param string $graph - * @param array $data prepared for this chart + * @param array $data prepared for this chart */ public function setGraph($graph, $data) { diff --git a/app/bundles/ReportBundle/Event/ReportQueryEvent.php b/app/bundles/ReportBundle/Event/ReportQueryEvent.php index f49572092a5..58847a32f40 100644 --- a/app/bundles/ReportBundle/Event/ReportQueryEvent.php +++ b/app/bundles/ReportBundle/Event/ReportQueryEvent.php @@ -1,7 +1,7 @@ Date: Tue, 8 May 2018 15:35:16 +0200 Subject: [PATCH 262/778] Add companyFields to checkbox and radio form field --- app/bundles/FormBundle/Views/Field/checkboxgrp.html.php | 1 + app/bundles/FormBundle/Views/Field/radiogrp.html.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/bundles/FormBundle/Views/Field/checkboxgrp.html.php b/app/bundles/FormBundle/Views/Field/checkboxgrp.html.php index 13453f6933c..5a07c753635 100644 --- a/app/bundles/FormBundle/Views/Field/checkboxgrp.html.php +++ b/app/bundles/FormBundle/Views/Field/checkboxgrp.html.php @@ -18,5 +18,6 @@ 'type' => 'checkbox', 'formName' => (isset($formName)) ? $formName : '', 'contactFields' => (isset($contactFields)) ? $contactFields : [], + 'companyFields' => (isset($companyFields)) ? $companyFields : [], ] ); diff --git a/app/bundles/FormBundle/Views/Field/radiogrp.html.php b/app/bundles/FormBundle/Views/Field/radiogrp.html.php index 949e1152afb..62310406c17 100644 --- a/app/bundles/FormBundle/Views/Field/radiogrp.html.php +++ b/app/bundles/FormBundle/Views/Field/radiogrp.html.php @@ -18,5 +18,6 @@ 'formName' => (isset($formName)) ? $formName : '', 'type' => 'radio', 'contactFields' => (isset($contactFields)) ? $contactFields : [], + 'companyFields' => (isset($companyFields)) ? $companyFields : [], ] ); From 38a1b9027ec93a748ebf115cabd93716ed7358ab Mon Sep 17 00:00:00 2001 From: scottshipman Date: Tue, 8 May 2018 14:26:45 -0400 Subject: [PATCH 263/778] add a space --- app/bundles/ReportBundle/Event/ReportGraphEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/ReportBundle/Event/ReportGraphEvent.php b/app/bundles/ReportBundle/Event/ReportGraphEvent.php index 1c62651b1b5..549336da574 100644 --- a/app/bundles/ReportBundle/Event/ReportGraphEvent.php +++ b/app/bundles/ReportBundle/Event/ReportGraphEvent.php @@ -57,7 +57,7 @@ public function getGraphs() * Set the graph array. * * @param string $graph - * @param array $data prepared for this chart + * @param array $data prepared for this chart */ public function setGraph($graph, $data) { From ef02332dfdcc074ab5df9cd2c1ca0f9089e77036 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 19:37:02 -0500 Subject: [PATCH 264/778] Fixed a couple docblock issues --- app/bundles/LeadBundle/Command/DeduplicateCommand.php | 2 +- app/bundles/LeadBundle/Deduplicate/ContactMerger.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Command/DeduplicateCommand.php b/app/bundles/LeadBundle/Command/DeduplicateCommand.php index eb203667ae0..9ca667f6c5d 100644 --- a/app/bundles/LeadBundle/Command/DeduplicateCommand.php +++ b/app/bundles/LeadBundle/Command/DeduplicateCommand.php @@ -1,7 +1,7 @@ Date: Wed, 28 Feb 2018 10:32:25 +0100 Subject: [PATCH 265/778] Fixed double return : unreachable code after return statement --- plugins/MauticFocusBundle/Views/Builder/generate.js.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/MauticFocusBundle/Views/Builder/generate.js.php b/plugins/MauticFocusBundle/Views/Builder/generate.js.php index c67f53c7019..5edfa3857ad 100644 --- a/plugins/MauticFocusBundle/Views/Builder/generate.js.php +++ b/plugins/MauticFocusBundle/Views/Builder/generate.js.php @@ -577,9 +577,10 @@ Focus.iframeResizerEnabled = true; return true; - + return false; + }, // Disable iframe resizer @@ -741,4 +742,4 @@ // Initialize MauticFocus().initialize(); -})(window); \ No newline at end of file +})(window); From 601f1933f6f12e2f40fd035584b9a139439b3324 Mon Sep 17 00:00:00 2001 From: mtahiue Date: Tue, 8 May 2018 18:20:22 +0200 Subject: [PATCH 266/778] Fixed typo --- plugins/MauticFocusBundle/Views/Builder/generate.js.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/MauticFocusBundle/Views/Builder/generate.js.php b/plugins/MauticFocusBundle/Views/Builder/generate.js.php index 5edfa3857ad..3b0c79f7366 100644 --- a/plugins/MauticFocusBundle/Views/Builder/generate.js.php +++ b/plugins/MauticFocusBundle/Views/Builder/generate.js.php @@ -577,7 +577,7 @@ Focus.iframeResizerEnabled = true; return true; - + return false; From 5482befdf659649427110a8aee2bcf01a391c9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=20=E2=98=95?= Date: Wed, 9 May 2018 05:32:50 -0400 Subject: [PATCH 267/778] Add UTM data to contact field conditions within campaigns. (#5869) --- .../LeadBundle/Entity/LeadFieldRepository.php | 32 +++++++++++++------ .../Type/CampaignEventLeadFieldValueType.php | 1 + .../LeadBundle/Form/Type/LeadFieldsType.php | 8 +++++ .../Translations/en_US/messages.ini | 5 +++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadFieldRepository.php b/app/bundles/LeadBundle/Entity/LeadFieldRepository.php index 1d6d4c16c2c..11116277790 100644 --- a/app/bundles/LeadBundle/Entity/LeadFieldRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadFieldRepository.php @@ -160,29 +160,36 @@ public function compareValue($lead, $field, $value, $operatorExpr) return false; } } else { - // Standard field + // Standard field / UTM field + $utmField = in_array($field, ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term']); + if ($utmField) { + $q->join('l', MAUTIC_TABLE_PREFIX.'lead_utmtags', 'u', 'l.id = u.lead_id'); + $property = 'u.'.$field; + } else { + $property = 'l.'.$field; + } if ($operatorExpr === 'empty' || $operatorExpr === 'notEmpty') { $q->where( $q->expr()->andX( $q->expr()->eq('l.id', ':lead'), ($operatorExpr === 'empty') ? $q->expr()->orX( - $q->expr()->isNull('l.'.$field), - $q->expr()->eq('l.'.$field, $q->expr()->literal('')) + $q->expr()->isNull($property), + $q->expr()->eq($property, $q->expr()->literal('')) ) : $q->expr()->andX( - $q->expr()->isNotNull('l.'.$field), - $q->expr()->neq('l.'.$field, $q->expr()->literal('')) + $q->expr()->isNotNull($property), + $q->expr()->neq($property, $q->expr()->literal('')) ) ) ) ->setParameter('lead', (int) $lead); } elseif ($operatorExpr === 'regexp' || $operatorExpr === 'notRegexp') { if ($operatorExpr === 'regexp') { - $where = 'l.'.$field.' REGEXP :value'; + $where = $property.' REGEXP :value'; } else { - $where = 'l.'.$field.' NOT REGEXP :value'; + $where = $property.' NOT REGEXP :value'; } $q->where( @@ -224,8 +231,8 @@ public function compareValue($lead, $field, $value, $operatorExpr) // include null $expr->add( $q->expr()->orX( - $q->expr()->$operatorExpr('l.'.$field, ':value'), - $q->expr()->isNull('l.'.$field) + $q->expr()->$operatorExpr($property, ':value'), + $q->expr()->isNull($property) ) ); } else { @@ -245,7 +252,7 @@ public function compareValue($lead, $field, $value, $operatorExpr) } $expr->add( - $q->expr()->$operatorExpr('l.'.$field, ':value') + $q->expr()->$operatorExpr($property, ':value') ); } @@ -253,6 +260,11 @@ public function compareValue($lead, $field, $value, $operatorExpr) ->setParameter('lead', (int) $lead) ->setParameter('value', $value); } + if ($utmField) { + // Match only against the latest UTM properties. + $q->orderBy('u.date_added', 'DESC'); + $q->setMaxResults(1); + } $result = $q->execute()->fetch(); return !empty($result['id']); diff --git a/app/bundles/LeadBundle/Form/Type/CampaignEventLeadFieldValueType.php b/app/bundles/LeadBundle/Form/Type/CampaignEventLeadFieldValueType.php index afda9f792d5..499962ea932 100755 --- a/app/bundles/LeadBundle/Form/Type/CampaignEventLeadFieldValueType.php +++ b/app/bundles/LeadBundle/Form/Type/CampaignEventLeadFieldValueType.php @@ -68,6 +68,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label_attr' => ['class' => 'control-label'], 'multiple' => false, 'with_tags' => true, + 'with_utm' => true, 'empty_value' => 'mautic.core.select', 'attr' => [ 'class' => 'form-control', diff --git a/app/bundles/LeadBundle/Form/Type/LeadFieldsType.php b/app/bundles/LeadBundle/Form/Type/LeadFieldsType.php index 9d0aa15de09..2ea01511257 100644 --- a/app/bundles/LeadBundle/Form/Type/LeadFieldsType.php +++ b/app/bundles/LeadBundle/Form/Type/LeadFieldsType.php @@ -44,12 +44,20 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) if ($options['with_tags']) { $fieldList['Core']['tags'] = 'mautic.lead.field.tags'; } + if ($options['with_utm']) { + $fieldList['UTM']['utm_campaign'] = 'mautic.lead.field.utmcampaign'; + $fieldList['UTM']['utm_content'] = 'mautic.lead.field.utmcontent'; + $fieldList['UTM']['utm_medium'] = 'mautic.lead.field.utmmedium'; + $fieldList['UTM']['utm_source'] = 'mautic.lead.field.umtsource'; + $fieldList['UTM']['utm_term'] = 'mautic.lead.field.utmterm'; + } return $fieldList; }, 'global_only' => false, 'required' => false, 'with_tags' => false, + 'with_utm' => false, ]); } diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index 0b3eb2ed9c0..199f0ab274e 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -55,6 +55,11 @@ mautic.lead.event.update="Contact updated" mautic.lead.event.utmtagsadded="UTM tags recorded" mautic.lead.field.tags="Tags" mautic.lead.field.address="Address" +mautic.lead.field.utmcampaign="Campaign" +mautic.lead.field.utmcontent="Content" +mautic.lead.field.utmmedium="Medium" +mautic.lead.field.umtsource="Source" +mautic.lead.field.utmterm="Term" mautic.lead.event.api="API" mautic.lead.field.form.choose="Select a field" mautic.lead.field.form.confirmbatchdelete="Delete the selected custom fields? WARNING: this will also delete any values of these custom fields that are associated with contacts." From 73e6527e7ee1d9c9f23fd22ac90f16298fd92f5e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 12:12:42 +0200 Subject: [PATCH 268/778] Remove debug command (check query performance) --- .../CheckQueryPerformanceSupplierCommand.php | 136 ------------------ .../LeadBundle/Entity/LeadListRepository.php | 13 +- 2 files changed, 4 insertions(+), 145 deletions(-) delete mode 100644 app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php diff --git a/app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php b/app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php deleted file mode 100644 index 4a71e35f7d3..00000000000 --- a/app/bundles/LeadBundle/Command/CheckQueryPerformanceSupplierCommand.php +++ /dev/null @@ -1,136 +0,0 @@ -setName('mautic:segments:check-performance') - ->setDescription('Blah') - ->addOption('--segment-id', '-i', InputOption::VALUE_OPTIONAL, 'Set the ID of segment to process') - ->addOption('--skip-old', null, InputOption::VALUE_NONE, 'Skip old query builder'); - - parent::configure(); - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - define('blah_2', true); - $container = $this->getContainer(); - $this->logger = $container->get('monolog.logger.mautic'); - - /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ - $listModel = $container->get('mautic.lead.model.list'); - - $id = $input->getOption('segment-id'); - $verbose = $input->getOption('verbose'); - $this->skipOld = $input->getOption('skip-old'); - - $failed = $ok = 0; - - if ($id && substr($id, strlen($id) - 1, 1) != '+') { - $list = $listModel->getEntity($id); - - if (!$list) { - $output->writeln('Segment with id "'.$id.'" not found'); - - return 1; - } - $response = $this->runSegment($output, $verbose, $list, $listModel); - if ($response) { - ++$ok; - } else { - ++$failed; - } - } else { - $lists = $listModel->getEntities( - [ - 'iterator_mode' => true, - 'orderBy' => 'l.id', - ] - ); - - while (($l = $lists->next()) !== false) { - // Get first item; using reset as the key will be the ID and not 0 - $l = reset($l); - - if (substr($id, strlen($id) - 1, 1) == '+' and $l->getId() < intval(trim($id, '+'))) { - continue; - } - $response = $this->runSegment($output, $verbose, $l, $listModel); - if (!$response) { - ++$failed; - } else { - ++$ok; - } - } - - unset($l); - - unset($lists); - } - - $total = $ok + $failed; - //$output->writeln(''); - //$output->writeln(sprintf('Total success rate: %d%%, %d succeeded: and %s%s failed... ', round(($ok / $total) * 100), $ok, ($failed ? $failed : ''), (!$failed ? $failed : ''))); - - return 0; - } - - private function format_period($inputSeconds) - { - $now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', '')); - - return $now->format('H:i:s.u'); - } - - private function runSegment($output, $verbose, $l, ListModel $listModel) - { - //$output->write('Running segment '.$l->getId().'...'); - - if (!$this->skipOld) { - $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); - - $timer1 = microtime(true); - $processed = $listModel->getVersionOld($l); - $timer1 = microtime(true) - $timer1; - } else { - $processed = ['count'=>-1, 'maxId'=>-1]; - $timer1 = 0; - } - - $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); - - $timer2 = microtime(true); - $processed2 = $listModel->getVersionNew($l); - $timer2 = microtime(true) - $timer2; - - $processed2 = array_shift($processed2); - - return 0; - } -} diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index ad6e57c0e87..3e016b798ad 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -529,15 +529,10 @@ public function getLeadsByList($lists, $args = [], Logger $logger) } $logger->debug(sprintf('Old version SQL: %s', $sqlT)); - if (defined('blah_2')) { - echo $id.";1;\"{$sqlT}\"\n"; - $results = []; - } else { - $timer = microtime(true); - $results = $q->execute()->fetchAll(); - $timer = microtime(true) - $timer; - $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); - } + $timer = microtime(true); + $results = $q->execute()->fetchAll(); + $timer = microtime(true) - $timer; + $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); foreach ($results as $r) { if ($countOnly) { From 4229abf22c7d601d96ae829ba6bde78ffed2f610 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 12:25:55 +0200 Subject: [PATCH 269/778] UTM tags - add a report for UTM tags (#5974) * UTM tags - add report for UTM tags * UTM report - add company data * Test for ReportUtmTagSubscriber * Test fix for PHP 5.6 - order of items in an array * Test fix for PHP 5.6 - order of items in an array * Typo - campaing > campaign * Typo - correct year for copyright --- app/bundles/LeadBundle/Config/config.php | 7 + .../EventListener/ReportUtmTagSubscriber.php | 132 ++++++++++ .../ReportUtmTagSubscriberTest.php | 230 ++++++++++++++++++ .../Translations/en_US/messages.ini | 6 + 4 files changed, 375 insertions(+) create mode 100644 app/bundles/LeadBundle/EventListener/ReportUtmTagSubscriber.php create mode 100644 app/bundles/LeadBundle/Tests/EventListener/ReportUtmTagSubscriberTest.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1d092ca201b..ec182b48b2f 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -395,6 +395,13 @@ 'mautic.lead.reportbundle.fields_builder', ], ], + 'mautic.lead.reportbundle.report_utm_tag_subscriber' => [ + 'class' => \Mautic\LeadBundle\EventListener\ReportUtmTagSubscriber::class, + 'arguments' => [ + 'mautic.lead.reportbundle.fields_builder', + 'mautic.lead.model.company_report_data', + ], + ], 'mautic.lead.calendarbundle.subscriber' => [ 'class' => 'Mautic\LeadBundle\EventListener\CalendarSubscriber', ], diff --git a/app/bundles/LeadBundle/EventListener/ReportUtmTagSubscriber.php b/app/bundles/LeadBundle/EventListener/ReportUtmTagSubscriber.php new file mode 100644 index 00000000000..86d25ea58ac --- /dev/null +++ b/app/bundles/LeadBundle/EventListener/ReportUtmTagSubscriber.php @@ -0,0 +1,132 @@ +fieldsBuilder = $fieldsBuilder; + $this->companyReportData = $companyReportData; + } + + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + ReportEvents::REPORT_ON_BUILD => ['onReportBuilder', 0], + ReportEvents::REPORT_ON_GENERATE => ['onReportGenerate', 0], + ]; + } + + /** + * Add available tables and columns to the report builder lookup. + * + * @param ReportBuilderEvent $event + */ + public function onReportBuilder(ReportBuilderEvent $event) + { + if (!$event->checkContext([self::UTM_TAG])) { + return; + } + + $columns = $this->fieldsBuilder->getLeadFieldsColumns('l.'); + $companyColumns = $this->companyReportData->getCompanyData(); + + $utmTagColumns = [ + 'utm.utm_campaign' => [ + 'label' => 'mautic.lead.report.utm.campaign', + 'type' => 'text', + ], + 'utm.utm_content' => [ + 'label' => 'mautic.lead.report.utm.content', + 'type' => 'text', + ], + 'utm.utm_medium' => [ + 'label' => 'mautic.lead.report.utm.medium', + 'type' => 'text', + ], + 'utm.utm_source' => [ + 'label' => 'mautic.lead.report.utm.source', + 'type' => 'text', + ], + 'utm.utm_term' => [ + 'label' => 'mautic.lead.report.utm.term', + 'type' => 'text', + ], + ]; + + $data = [ + 'display_name' => 'mautic.lead.report.utm.utm_tag', + 'columns' => array_merge($columns, $companyColumns, $utmTagColumns), + ]; + $event->addTable(self::UTM_TAG, $data, ReportSubscriber::GROUP_CONTACTS); + } + + /** + * Initialize the QueryBuilder object to generate reports from. + * + * @param ReportGeneratorEvent $event + */ + public function onReportGenerate(ReportGeneratorEvent $event) + { + if (!$event->checkContext([self::UTM_TAG])) { + return; + } + + $qb = $event->getQueryBuilder(); + $qb->from(MAUTIC_TABLE_PREFIX.'lead_utmtags', 'utm') + ->leftJoin('utm', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = utm.lead_id'); + + if ($event->hasColumn(['u.first_name', 'u.last_name']) || $event->hasFilter(['u.first_name', 'u.last_name'])) { + $qb->leftJoin('l', MAUTIC_TABLE_PREFIX.'users', 'u', 'u.id = l.owner_id'); + } + + if ($event->hasColumn('i.ip_address') || $event->hasFilter('i.ip_address')) { + $event->addLeadIpAddressLeftJoin($qb); + } + + if ($this->companyReportData->eventHasCompanyColumns($event)) { + $qb->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', 'companies_lead', 'l.id = companies_lead.lead_id'); + $qb->leftJoin('companies_lead', MAUTIC_TABLE_PREFIX.'companies', 'comp', 'companies_lead.company_id = comp.id'); + } + + $event->setQueryBuilder($qb); + } +} diff --git a/app/bundles/LeadBundle/Tests/EventListener/ReportUtmTagSubscriberTest.php b/app/bundles/LeadBundle/Tests/EventListener/ReportUtmTagSubscriberTest.php new file mode 100644 index 00000000000..a2735db85bc --- /dev/null +++ b/app/bundles/LeadBundle/Tests/EventListener/ReportUtmTagSubscriberTest.php @@ -0,0 +1,230 @@ +createMock(FieldsBuilder::class); + $companyReportDataMock = $this->createMock(CompanyReportData::class); + $reportBuilderEventMock = $this->createMock(ReportBuilderEvent::class); + + $reportBuilderEventMock->expects($this->once()) + ->method('checkContext') + ->with(['lead.utmTag']) + ->willReturn(false); + + $reportBuilderEventMock->expects($this->never()) + ->method('addTable'); + + $reportUtmTagSubscriber = new ReportUtmTagSubscriber($fieldsBuilderMock, $companyReportDataMock); + $reportUtmTagSubscriber->onReportBuilder($reportBuilderEventMock); + } + + public function testNotRelevantContextGenerate() + { + $fieldsBuilderMock = $this->createMock(FieldsBuilder::class); + $companyReportDataMock = $this->createMock(CompanyReportData::class); + $reportGeneratorEventMock = $this->createMock(ReportGeneratorEvent::class); + + $reportGeneratorEventMock->expects($this->once()) + ->method('checkContext') + ->with(['lead.utmTag']) + ->willReturn(false); + + $reportGeneratorEventMock->expects($this->never()) + ->method('getQueryBuilder'); + + $reportUtmTagSubscriber = new ReportUtmTagSubscriber($fieldsBuilderMock, $companyReportDataMock); + $reportUtmTagSubscriber->onReportGenerate($reportGeneratorEventMock); + } + + public function testReportBuilder() + { + $translatorMock = $this->createMock(TranslatorInterface::class); + $channelListHelperMock = $this->createMock(ChannelListHelper::class); + $reportHelperMock = $this->createMock(ReportHelper::class); + $fieldsBuilderMock = $this->createMock(FieldsBuilder::class); + $companyReportDataMock = $this->createMock(CompanyReportData::class); + + $leadColumns = [ + 'lead.name' => [ + 'label' => 'lead name', + 'type' => 'bool', + ], + ]; + $companyColumns = [ + 'comp.name' => [ + 'label' => 'company name', + 'type' => 'bool', + ], + ]; + + $fieldsBuilderMock->expects($this->once()) + ->method('getLeadFieldsColumns') + ->with('l.') + ->willReturn($leadColumns); + + $companyReportDataMock->expects($this->once()) + ->method('getCompanyData') + ->with() + ->willReturn($companyColumns); + + $reportBuilderEvent = new ReportBuilderEvent($translatorMock, $channelListHelperMock, 'lead.utmTag', [], $reportHelperMock); + + $segmentReportSubscriber = new ReportUtmTagSubscriber($fieldsBuilderMock, $companyReportDataMock); + $segmentReportSubscriber->onReportBuilder($reportBuilderEvent); + + $expected = [ + 'lead.utmTag' => [ + 'display_name' => 'mautic.lead.report.utm.utm_tag', + 'columns' => [ + 'lead.name' => [ + 'label' => null, + 'type' => 'bool', + 'alias' => 'name', + ], + 'comp.name' => [ + 'label' => null, + 'type' => 'bool', + 'alias' => 'name', + ], + 'utm.utm_campaign' => [ + 'label' => null, + 'type' => 'text', + 'alias' => 'utm_campaign', + ], + 'utm.utm_content' => [ + 'label' => null, + 'type' => 'text', + 'alias' => 'utm_content', + ], + 'utm.utm_medium' => [ + 'label' => null, + 'type' => 'text', + 'alias' => 'utm_medium', + ], + 'utm.utm_source' => [ + 'label' => null, + 'type' => 'text', + 'alias' => 'utm_source', + ], + 'utm.utm_term' => [ + 'label' => null, + 'type' => 'text', + 'alias' => 'utm_term', + ], + ], + 'group' => 'contacts', + ], + ]; + + $this->assertEquals($expected, $reportBuilderEvent->getTables()); //Different order of keys on PHP 5.6. + } + + public function testReportGenerateNoJoinedTables() + { + if (!defined('MAUTIC_TABLE_PREFIX')) { + define('MAUTIC_TABLE_PREFIX', ''); + } + + $reportGeneratorEventMock = $this->getReportGeneratorEventMock(); + $reportUtmTagSubscriber = $this->getReportUtmTagSubscriber(); + $queryBuilderMock = $this->getQueryBuilderMock(); + + $reportGeneratorEventMock->expects($this->once()) + ->method('getQueryBuilder') + ->willReturn($queryBuilderMock); + + $reportUtmTagSubscriber->onReportGenerate($reportGeneratorEventMock); + } + + public function testReportGenerateWithUsers() + { + if (!defined('MAUTIC_TABLE_PREFIX')) { + define('MAUTIC_TABLE_PREFIX', ''); + } + + $reportGeneratorEventMock = $this->getReportGeneratorEventMock(); + $reportUtmTagSubscriber = $this->getReportUtmTagSubscriber(); + $queryBuilderMock = $this->getQueryBuilderMock(); + + $reportGeneratorEventMock->expects($this->at(1)) + ->method('getQueryBuilder') + ->willReturn($queryBuilderMock); + + $reportGeneratorEventMock->expects($this->at(2)) + ->method('hasColumn') + ->with(['u.first_name', 'u.last_name']) + ->willReturn(true); + + $reportUtmTagSubscriber->onReportGenerate($reportGeneratorEventMock); + } + + /** + * @return ReportUtmTagSubscriber + */ + private function getReportUtmTagSubscriber() + { + $fieldsBuilderMock = $this->createMock(FieldsBuilder::class); + $companyReportDataMock = $this->createMock(CompanyReportData::class); + $reportUtmTagSubscriber = new ReportUtmTagSubscriber($fieldsBuilderMock, $companyReportDataMock); + + return $reportUtmTagSubscriber; + } + + /** + * @return ReportGeneratorEvent|\PHPUnit_Framework_MockObject_MockObject + */ + private function getReportGeneratorEventMock() + { + $reportGeneratorEventMock = $this->createMock(ReportGeneratorEvent::class); + + $reportGeneratorEventMock->expects($this->at(0)) + ->method('checkContext') + ->with(['lead.utmTag']) + ->willReturn(true); + + return $reportGeneratorEventMock; + } + + /** + * @return QueryBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private function getQueryBuilderMock() + { + $queryBuilderMock = $this->createMock(QueryBuilder::class); + + $queryBuilderMock->expects($this->at(0)) + ->method('from') + ->with(MAUTIC_TABLE_PREFIX.'lead_utmtags', 'utm') + ->willReturn($queryBuilderMock); + + $queryBuilderMock->expects($this->at(1)) + ->method('leftJoin') + ->with('utm', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = utm.lead_id') + ->willReturn($queryBuilderMock); + + return $queryBuilderMock; + } +} diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index 199f0ab274e..3d029d3e3c1 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -500,6 +500,12 @@ mautic.lead.report.points.type="Point event type" mautic.lead.report.segment.membership="Segment Membership" mautic.lead.report.segment.manually_added="Manually added" mautic.lead.report.segment.manually_removed="Manually removed" +mautic.lead.report.utm.utm_tag="UTM codes" +mautic.lead.report.utm.campaign="UTM campaign" +mautic.lead.report.utm.content="UTM content" +mautic.lead.report.utm.medium="UTM medium" +mautic.lead.report.utm.source="UTM source" +mautic.lead.report.utm.term="UTM term" mautic.lead.integrations.header="No integration relationships found" mautic.lead.socialprofiles.header="No Social Profiles Found" mautic.lead.auditlog.header="No audit log entries found" From ab8a78571181b3e63b43fedae17015164aa7f8c6 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 13:49:37 +0200 Subject: [PATCH 270/778] Functions documentation + fixed return types in Doc blocks --- .../Segment/Decorator/BaseDecorator.php | 4 +- .../Decorator/CustomMappedDecorator.php | 2 +- .../Decorator/FilterDecoratorInterface.php | 64 +++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 6c970429272..82bac336833 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -96,7 +96,7 @@ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrat /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate - * @param $argument + * @param array|string $argument * * @return array|string */ @@ -117,7 +117,7 @@ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilt /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * - * @return array|bool|float|mixed|null|string + * @return array|bool|float|null|string */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { diff --git a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php index 486c7b37f29..6bb3724c79f 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/CustomMappedDecorator.php @@ -90,7 +90,7 @@ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrat /** * @param ContactSegmentFilterCrate $contactSegmentFilterCrate * - * @return bool + * @return string|bool if no func needed */ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { diff --git a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php index ba08565dd62..8d81849f132 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php +++ b/app/bundles/LeadBundle/Segment/Decorator/FilterDecoratorInterface.php @@ -13,26 +13,80 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilterCrate; -/** - * Interface FilterDecoratorInterface. - * - * @TODO @petr document these functions please with phpdoc and meaningful description - */ interface FilterDecoratorInterface { + /** + * Returns filter's field (usually a column name in DB). + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|string + */ public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate); + /** + * Returns DB table. + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate); + /** + * Returns a string operator (like, eq, neq, ...). + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate); + /** + * Returns an argument for QueryBuilder (usually ':arg' in case that $argument is equal to 'arg' string. + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * @param array|string $argument + * + * @return array|string + */ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument); + /** + * Returns formatted value for QueryBuilder ('%value%' for 'like', '%value' for 'Ends with', SQL-formatted date etc.). + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return array|bool|float|null|string + */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate); + /** + * Returns QueryBuilder's service name from the container. + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate); + /** + * Returns a name of aggregation function for SQL (SUM, COUNT etc.) + * Returns false if no aggregation function is needed. + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string|bool if no func needed + */ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate); + /** + * Returns a special where condition which is needed to be added to QueryBuilder (like email_stats.is_read = 1 for 'Read emails') + * Returns null if no special condition is needed. + * + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression|null|string + */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate); } From a98b95643fa7f4655b5fdea823b4b0a370da805b Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 13:59:03 +0200 Subject: [PATCH 271/778] Typo - remove unnecessary comments + rename function to correct format --- .../Segment/ContactSegmentService.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 400a04dbb7f..3e43e501e8f 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -38,6 +38,11 @@ class ContactSegmentService */ private $preparedQB; + /** + * @param ContactSegmentFilterFactory $contactSegmentFilterFactory + * @param ContactSegmentQueryBuilder $queryBuilder + * @param Logger $logger + */ public function __construct( ContactSegmentFilterFactory $contactSegmentFilterFactory, ContactSegmentQueryBuilder $queryBuilder, @@ -120,11 +125,11 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters $qb = $this->getNewSegmentContactsQuery($segment, $batchLimiters); if (isset($batchLimiters['minId'])) { - $qb->andWhere($qb->expr()->gte('l.id', $qb->expr()->literal(intval($batchLimiters['minId'])))); + $qb->andWhere($qb->expr()->gte('l.id', $qb->expr()->literal((int) $batchLimiters['minId']))); } if (isset($batchLimiters['maxId'])) { - $qb->andWhere($qb->expr()->lte('l.id', $qb->expr()->literal(intval($batchLimiters['maxId'])))); + $qb->andWhere($qb->expr()->lte('l.id', $qb->expr()->literal((int) $batchLimiters['maxId']))); } $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); @@ -142,9 +147,6 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters * @return array * * @throws \Exception - * - * @nottotodo This is almost copy of getNewLeadListLeadsCount method. Only difference is that it calls getTotalSegmentContactsQuery - * @answer Yes it is, it's just a facade */ public function getTotalLeadListLeadsCount(LeadList $segment) { @@ -165,7 +167,6 @@ public function getTotalLeadListLeadsCount(LeadList $segment) $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); - //dump($qb->getDebugOutput()); $result = $this->timedFetch($qb, $segment->getId()); @@ -283,7 +284,7 @@ public function getOrphanedLeadListLeads(LeadList $segment) * * @return string */ - private function format_period($inputSeconds) + private function formatPeriod($inputSeconds) { $now = \DateTime::createFromFormat('U.u', number_format($inputSeconds, 6, '.', '')); @@ -307,7 +308,7 @@ private function timedFetch(QueryBuilder $qb, $segmentId) $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.$this->format_period($end).', Result count: '.count($result), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.$this->formatPeriod($end).', Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), @@ -334,7 +335,7 @@ private function timedFetchAll(QueryBuilder $qb, $segmentId) $end = microtime(true) - $start; - $this->logger->debug('Segment QB: Query took: '.$this->format_period($end).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); + $this->logger->debug('Segment QB: Query took: '.$this->formatPeriod($end).'ms. Result count: '.count($result), ['segmentId' => $segmentId]); } catch (\Exception $e) { $this->logger->error('Segment QB: Query Exception: '.$e->getMessage(), [ 'query' => $qb->getSQL(), 'parameters' => $qb->getParameters(), From f12f627587a02923830a2f7fd051ef145b1a37f8 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 14:11:38 +0200 Subject: [PATCH 272/778] Fixed year in copyright for new classes --- .../Segment/ContactSegmentFilterCrate.php | 2 +- .../Segment/ContactSegmentFilterFactory.php | 2 +- .../Segment/ContactSegmentService.php | 2 +- .../Decorator/Date/DateOptionAbstract.php | 43 ++++++++++++++++++- .../Decorator/Date/DateOptionFactory.php | 2 +- .../Decorator/Date/DateOptionParameters.php | 2 +- .../Decorator/Date/Day/DateDayAbstract.php | 2 +- .../Decorator/Date/Day/DateDayToday.php | 2 +- .../Decorator/Date/Day/DateDayTomorrow.php | 2 +- .../Decorator/Date/Day/DateDayYesterday.php | 2 +- .../Date/Month/DateMonthAbstract.php | 2 +- .../Decorator/Date/Month/DateMonthLast.php | 2 +- .../Decorator/Date/Month/DateMonthNext.php | 2 +- .../Decorator/Date/Month/DateMonthThis.php | 2 +- .../Decorator/Date/Other/DateAnniversary.php | 2 +- .../Decorator/Date/Other/DateDefault.php | 2 +- .../Date/Other/DateRelativeInterval.php | 2 +- .../Decorator/Date/Week/DateWeekAbstract.php | 2 +- .../Decorator/Date/Week/DateWeekLast.php | 2 +- .../Decorator/Date/Week/DateWeekNext.php | 2 +- .../Decorator/Date/Week/DateWeekThis.php | 2 +- .../Decorator/Date/Year/DateYearAbstract.php | 2 +- .../Decorator/Date/Year/DateYearLast.php | 2 +- .../Decorator/Date/Year/DateYearNext.php | 2 +- .../Decorator/Date/Year/DateYearThis.php | 2 +- .../Segment/Decorator/DateDecorator.php | 5 ++- .../Segment/Decorator/DecoratorFactory.php | 2 +- .../DoNotContact/DoNotContactParts.php | 3 +- .../Query/ContactSegmentQueryBuilder.php | 2 +- .../Segment/RandomParameterName.php | 2 +- .../LeadBundle/Segment/RelativeDate.php | 2 +- .../ContactSegmentFilterDictionary.php | 2 +- 32 files changed, 77 insertions(+), 32 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 1efa1efb97a..b75aa2722da 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -1,7 +1,7 @@ dateDecorator->getField($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getTable($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { if ($this->dateOptionParameters->isBetweenRequired()) { @@ -92,11 +107,22 @@ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate return $this->dateDecorator->getOperator($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * @param array|string $argument + * + * @return array|string + */ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return array|bool|float|null|string + */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { $dateTimeHelper = $this->dateDecorator->getDefaultDate(); @@ -118,16 +144,31 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte return $dateTimeHelper->toUtcString($dateFormat); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return bool|string + */ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression|null|string + */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getWhere($contactSegmentFilterCrate); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index b7b63abfe2d..b07fdcf64e3 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -1,7 +1,7 @@ Date: Wed, 9 May 2018 14:17:39 +0200 Subject: [PATCH 273/778] Date classes - Doc blocks --- .../Decorator/Date/Other/DateAnniversary.php | 44 +++++++++++++++++++ .../Decorator/Date/Other/DateDefault.php | 41 +++++++++++++++++ .../Date/Other/DateRelativeInterval.php | 41 +++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index 7b4c2c310d6..dee74f4dd2e 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -22,31 +22,60 @@ class DateAnniversary implements FilterDecoratorInterface */ private $dateDecorator; + /** + * @param DateDecorator $dateDecorator + */ public function __construct(DateDecorator $dateDecorator) { $this->dateDecorator = $dateDecorator; } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|string + */ public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getField($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getTable($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return 'like'; } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * @param array|string $argument + * + * @return array|string + */ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return array|bool|float|null|string + */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { $dateTimeHelper = $this->dateDecorator->getDefaultDate(); @@ -56,16 +85,31 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte return '%'.date('-m-d'); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return bool|string + */ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression|null|string + */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getWhere($contactSegmentFilterCrate); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php index a28eb4d6e51..4df27ea362d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateDefault.php @@ -37,41 +37,82 @@ public function __construct(DateDecorator $dateDecorator, $originalValue) $this->originalValue = $originalValue; } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|string + */ public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getField($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getTable($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getOperator($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * @param array|string $argument + * + * @return array|string + */ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return array|bool|float|null|string + */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->originalValue; } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return bool|string + */ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression|null|string + */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getWhere($contactSegmentFilterCrate); diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php index ef25599d1d6..823c787f85d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateRelativeInterval.php @@ -37,16 +37,31 @@ public function __construct(DateDecorator $dateDecorator, $originalValue) $this->originalValue = $originalValue; } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return null|string + */ public function getField(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getField($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getTable(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getTable($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate) { if ($contactSegmentFilterCrate->getOperator() === '=') { @@ -59,11 +74,22 @@ public function getOperator(ContactSegmentFilterCrate $contactSegmentFilterCrate return $this->dateDecorator->getOperator($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * @param array|string $argument + * + * @return array|string + */ public function getParameterHolder(ContactSegmentFilterCrate $contactSegmentFilterCrate, $argument) { return $this->dateDecorator->getParameterHolder($contactSegmentFilterCrate, $argument); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return array|bool|float|null|string + */ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilterCrate) { $date = $this->dateDecorator->getDefaultDate(); @@ -78,16 +104,31 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte return $date->toUtcString($format); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return string + */ public function getQueryType(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getQueryType($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return bool|string + */ public function getAggregateFunc(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getAggregateFunc($contactSegmentFilterCrate); } + /** + * @param ContactSegmentFilterCrate $contactSegmentFilterCrate + * + * @return \Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression|null|string + */ public function getWhere(ContactSegmentFilterCrate $contactSegmentFilterCrate) { return $this->dateDecorator->getWhere($contactSegmentFilterCrate); From 6479caf411e61104fabdac6891a1a89dbb851b02 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 14:19:06 +0200 Subject: [PATCH 274/778] Remove unreachable return statement --- .../LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php index dee74f4dd2e..6cb6459d673 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/Other/DateAnniversary.php @@ -81,8 +81,6 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte $dateTimeHelper = $this->dateDecorator->getDefaultDate(); return $dateTimeHelper->toUtcString('%-m-d'); - - return '%'.date('-m-d'); } /** From 0f0cb2bc4aa2a39c7e87dd78668992dcc7da85bf Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Mon, 5 Mar 2018 10:24:56 -0500 Subject: [PATCH 275/778] fixing font --- app/bundles/CoreBundle/Assets/css/libraries/builder.css | 6 ++++++ app/bundles/CoreBundle/Assets/css/libraries/builder.less | 7 +++++++ media/css/libraries.css | 3 ++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/bundles/CoreBundle/Assets/css/libraries/builder.css b/app/bundles/CoreBundle/Assets/css/libraries/builder.css index feaa7c3a2ec..0c8ffe194a2 100644 --- a/app/bundles/CoreBundle/Assets/css/libraries/builder.css +++ b/app/bundles/CoreBundle/Assets/css/libraries/builder.css @@ -5482,3 +5482,9 @@ div[data-slot].ui-sortable-helper { [data-slot="dynamicContent"] { z-index: 50; } +.chosen-container .chosen-results li { + font-family: "Open Sans", Helvetica, Arial, sans-serif; +} +.chosen-container .chosen-results li:before { + font: normal normal normal 14px/1 FontAwesome; +} diff --git a/app/bundles/CoreBundle/Assets/css/libraries/builder.less b/app/bundles/CoreBundle/Assets/css/libraries/builder.less index 95401b6fe87..bb0371469a1 100644 --- a/app/bundles/CoreBundle/Assets/css/libraries/builder.less +++ b/app/bundles/CoreBundle/Assets/css/libraries/builder.less @@ -253,4 +253,11 @@ div[data-slot].ui-sortable-helper { [data-slot="dynamicContent"] { z-index:50; +} + +.chosen-container .chosen-results li { + font-family: "Open Sans",Helvetica,Arial,sans-serif; + &:before { + font: normal normal normal 14px/1 FontAwesome; + } } \ No newline at end of file diff --git a/media/css/libraries.css b/media/css/libraries.css index ef224ad9c69..b81521dbc04 100644 --- a/media/css/libraries.css +++ b/media/css/libraries.css @@ -275,7 +275,8 @@ solid transparent;white-space:nowrap;line-height:1.3856;-webkit-user-select:none solid #4e5e9e}.slot-placeholder{border:2px dotted #4e5e9e}[data-slot="text"].fr-box{padding:initial}[data-slot="text"].fr-box .fr-toolbar{border-top:2px solid #4e5e9e;position:absolute;left:-15px;bottom:initial !important;min-width:385px}[data-slot="text"].fr-box .fr-toolbar.fr-top{top:-78px !important;bottom:initial !important}[data-slot="text"].fr-box .fr-toolbar.fr-bottom{bottom:-78px !important;top:initial !important}[data-slot="text"].fr-box .fr-wrapper{border-radius:0 0 0px 0px;-moz-border-radius:0 0 0px 0px;-webkit-border-radius:0 0 0px 0px;-webkit-box-shadow:none !important;-moz-box-shadow:none !important;box-shadow:none !important;background:transparent !important}[data-slot="text"].fr-box .fr-wrapper .fr-element{text-align:inherit !important;padding:0 !important;overflow-x:initial !important;color:inherit !important;min-height:inherit !important}.slot-type-handle.btn,.section-type-handle.btn{float:left;width:111px;margin:2px;height:75px;padding-left:5px;padding-right:5px;text-align:center;word-wrap:break-word}.slot-type-handle.ui-draggable-dragging,.section-type-handle.ui-draggable-dragging{color:#5d6c7c;background-color:#f5f5f5;border-color:#d3d3d3;padding:10px -16px;font-size:16px;line-height:1.25;border-radius:4px;margin:2px;text-align:center}.theme-list .panel-body{height:350px}.theme-list .select-theme-link{margin-top:5px}.theme-list .select-theme-selected{margin-top:5px}[data-slot="dynamicContent"]{z-index:50} +16px;font-size:16px;line-height:1.25;border-radius:4px;margin:2px;text-align:center}.theme-list .panel-body{height:350px}.theme-list .select-theme-link{margin-top:5px}.theme-list .select-theme-selected{margin-top:5px}[data-slot="dynamicContent"]{z-index:50}.chosen-container .chosen-results +li{font-family:"Open Sans",Helvetica,Arial,sans-serif}.chosen-container .chosen-results li:before{font:normal normal normal 14px/1 FontAwesome} /*! normalize.css v3.0.1 | MIT License | git.io/normalize */ html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button, From f39f49fcd365d699f2999c31796a8ccb132bbfe5 Mon Sep 17 00:00:00 2001 From: heathdutton Date: Wed, 9 May 2018 09:35:44 -0400 Subject: [PATCH 276/778] Add empty entity check. --- app/bundles/FormBundle/Controller/AjaxController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/FormBundle/Controller/AjaxController.php b/app/bundles/FormBundle/Controller/AjaxController.php index b029766dcee..728655eb955 100644 --- a/app/bundles/FormBundle/Controller/AjaxController.php +++ b/app/bundles/FormBundle/Controller/AjaxController.php @@ -69,7 +69,7 @@ protected function updateFormFieldsAction(Request $request) $dataArray = ['success' => 0]; $model = $this->getModel('form'); $entity = $model->getEntity($formId); - $formFields = $entity->getFields(); + $formFields = empty($entity) ? [] : $entity->getFields(); $fields = []; foreach ($formFields as $field) { From ce4d197d9f9732e0bd98231ef1c275f2090248aa Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 08:54:54 -0500 Subject: [PATCH 277/778] Updated copyright date --- app/bundles/LeadBundle/Deduplicate/ContactDeduper.php | 2 +- app/bundles/LeadBundle/Deduplicate/ContactMerger.php | 2 +- .../LeadBundle/Deduplicate/Exception/SameContactException.php | 2 +- .../Deduplicate/Exception/ValueNotMergeableException.php | 2 +- app/bundles/LeadBundle/Deduplicate/Helper/MergeValueHelper.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php index 3956ce07148..eca6b8fa259 100644 --- a/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php +++ b/app/bundles/LeadBundle/Deduplicate/ContactDeduper.php @@ -1,7 +1,7 @@ Date: Wed, 9 May 2018 15:56:04 +0200 Subject: [PATCH 278/778] Remove todo comment --- .../LeadBundle/Services/ContactSegmentFilterDictionary.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index eb4f467b790..51ad12b0794 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -18,11 +18,6 @@ use Mautic\LeadBundle\Segment\Query\Filter\SegmentReferenceFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder; -/** - * Class ContactSegmentFilterDictionary. - * - * @todo @petr Já jsem to myslím předělával už. Chtěl jsem z toho pak udělat i objekt, aby se člověk nemusel ptát na klíče v poli, ale pak jsme na to nesahali, protože to nebylo komplet - */ class ContactSegmentFilterDictionary extends \ArrayIterator { private $translations; From 4b3ce08c3359f9d5b1787c318ebf3f0e3f6a46ba Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 9 May 2018 17:05:46 +0200 Subject: [PATCH 279/778] Remove jmeter debug class --- parse-jmeter-output.php | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 parse-jmeter-output.php diff --git a/parse-jmeter-output.php b/parse-jmeter-output.php deleted file mode 100644 index 14af3676e92..00000000000 --- a/parse-jmeter-output.php +++ /dev/null @@ -1,40 +0,0 @@ -children() as $node) { - $response = $node->responseData->__toString(); - - $matches = null; - - $check = preg_match("/\[Select Statement\] select ([0-9]+) as id, ([0-9]+) as version from \((.*)\)/", $node->samplerData->__toString(), $matches); - - if (!$check) { - throw new \Exception('Invalid data'); - } - $attributes = $node->attributes(); - - if (isset($rows[$matches[1]][$matches[2]])) { - $imSum = ($rows[$matches[1]][$matches[2]] + $attributes['t']->__toString()) / (count($attributes['t']->__toString()) + 1); - $rows[$matches[1]][$matches[2]] = $imSum; - } - $rows[$matches[1]][$matches[2]] = $attributes['t']->__toString(); -} - -foreach ($rows as $segmentId=>$times) { - if (isset($times[1]) && isset($times[2])) { - printf("%d;%s;%s\n", $segmentId, $times[1], $times[2]); - } -} From c33db49a27b324e5c36ad40da7077d144ccb4edc Mon Sep 17 00:00:00 2001 From: heathdutton Date: Wed, 9 May 2018 12:02:29 -0400 Subject: [PATCH 280/778] Prevent notice when field is not yet provided. --- .../FormBundle/Form/Type/CampaignEventFormFieldValueType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php b/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php index c42761002af..ab1cd7ffff8 100644 --- a/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php +++ b/app/bundles/FormBundle/Form/Type/CampaignEventFormFieldValueType.php @@ -133,7 +133,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ); // Display selectbox for a field with choices, textbox for others - if (empty($options[$data['field']])) { + if (empty($data['field']) || empty($options[$data['field']])) { $form->add( 'value', 'text', From 9fe38f71a6093e2be909169ca1f451e0b060dc12 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 10 May 2018 13:54:49 +0200 Subject: [PATCH 281/778] fix incorrect sql for removing contacts from segments --- .../Segment/ContactSegmentService.php | 20 ++++++++++--------- .../LeadBundle/Segment/Query/QueryBuilder.php | 6 ++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 13495069729..40ca9b4bdfe 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -227,15 +227,17 @@ private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); - $queryBuilder->rightJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp', 'l.id = orp.lead_id and orp.leadlist_id = '.$segment->getId()); - $queryBuilder->andWhere($queryBuilder->expr()->andX( - $queryBuilder->expr()->isNull('l.id'), - $queryBuilder->expr()->eq('orp.leadlist_id', $segment->getId()) - )); - - $queryBuilder->select($queryBuilder->guessPrimaryLeadContactIdColumn().' as id'); - - return $queryBuilder; + $qbO = new QueryBuilder($queryBuilder->getConnection()); + $qbO->select('orp.lead_id as id, orp.leadlist_id') + ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp'); + $qbO->leftJoin('orp', '('.$queryBuilder->getSQL().')', 'members', 'members.id=orp.lead_id'); + $qbO->setParameters($queryBuilder->getParameters()); + $qbO->andWhere($qbO->expr()->eq('orp.leadlist_id', ':orpsegid')); + $qbO->andWhere($qbO->expr()->isNull('members.id')); + $qbO->andWhere($qbO->expr()->eq('orp.manually_added', $qbO->expr()->literal(0))); + $qbO->setParameter(':orpsegid', $segment->getId()); + + return $qbO; } /** diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index bed24441bc3..be287104aec 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1528,10 +1528,16 @@ public function getTableJoins($tableName) public function guessPrimaryLeadContactIdColumn() { $parts = $this->getQueryParts(); + $leadTable = $parts['from'][0]['alias']; if (!isset($parts['join'][$leadTable])) { return $leadTable.'.id'; } + + if ($leadTable == 'orp') { + return 'orp.lead_id'; + } + $joins = $parts['join'][$leadTable]; foreach ($joins as $join) { From 51842cc18acb08529e6cb5954f9c10222739fc44 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 10 May 2018 15:41:38 +0200 Subject: [PATCH 282/778] implement segment events for plugins, still need to write tests --- app/bundles/LeadBundle/Config/config.php | 1 + .../Event/LeadListFilteringEvent.php | 2 +- .../Segment/ContactSegmentFilterCrate.php | 26 ++++++++++++++----- .../Segment/ContactSegmentService.php | 3 ++- .../Query/ContactSegmentQueryBuilder.php | 24 ++++++++++++++++- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 6f06b3c10da..7ac2c58a3a5 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -853,6 +853,7 @@ 'arguments' => [ 'doctrine.orm.entity_manager', 'mautic.lead.model.random_parameter_name', + 'event_dispatcher', ], ], 'mautic.lead.model.lead_segment_service' => [ diff --git a/app/bundles/LeadBundle/Event/LeadListFilteringEvent.php b/app/bundles/LeadBundle/Event/LeadListFilteringEvent.php index 6875de7eb56..9f159092053 100644 --- a/app/bundles/LeadBundle/Event/LeadListFilteringEvent.php +++ b/app/bundles/LeadBundle/Event/LeadListFilteringEvent.php @@ -65,7 +65,7 @@ class LeadListFilteringEvent extends CommonEvent * @param QueryBuilder $queryBuilder * @param EntityManager $entityManager */ - public function __construct($details, $leadId, $alias, $func, QueryBuilder $queryBuilder, EntityManager $entityManager) + public function __construct($details, $leadId, $alias, $func, $queryBuilder, EntityManager $entityManager) { $this->details = $details; $this->leadId = $leadId; diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index b75aa2722da..69c3b5c2b26 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -46,6 +46,11 @@ class ContactSegmentFilterCrate */ private $operator; + /** + * @var array + */ + private $sourceArray; + /** * ContactSegmentFilterCrate constructor. * @@ -53,12 +58,13 @@ class ContactSegmentFilterCrate */ public function __construct(array $filter) { - $this->glue = isset($filter['glue']) ? $filter['glue'] : null; - $this->field = isset($filter['field']) ? $filter['field'] : null; - $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; - $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->operator = isset($filter['operator']) ? $filter['operator'] : null; - $this->filter = isset($filter['filter']) ? $filter['filter'] : null; + $this->glue = isset($filter['glue']) ? $filter['glue'] : null; + $this->field = isset($filter['field']) ? $filter['field'] : null; + $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; + $this->type = isset($filter['type']) ? $filter['type'] : null; + $this->operator = isset($filter['operator']) ? $filter['operator'] : null; + $this->filter = isset($filter['filter']) ? $filter['filter'] : null; + $this->sourceArray = $filter; } /** @@ -165,4 +171,12 @@ private function getType() { return $this->type; } + + /** + * @return array + */ + public function getArray() + { + return $this->sourceArray; + } } diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 40ca9b4bdfe..fd92f1ab706 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -72,7 +72,8 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); - //$queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubsribedQuery($queryBuilder, $segment->getId()); + // I really the following line should be enabled; but it doesn't match with the old results + //$queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId()); return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index d0b73258197..2e1ea1342a3 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -12,10 +12,13 @@ namespace Mautic\LeadBundle\Segment\Query; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Event\LeadListFilteringEvent; +use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\ContactSegmentFilters; use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\RandomParameterName; +use Symfony\Component\EventDispatcher\EventDispatcher; /** * Class ContactSegmentQueryBuilder is responsible for building queries for segments. @@ -28,16 +31,23 @@ class ContactSegmentQueryBuilder /** @var RandomParameterName */ private $randomParameterName; + /** + * @var EventDispatcher + */ + private $dispatcher; + /** * ContactSegmentQueryBuilder constructor. * * @param EntityManager $entityManager * @param RandomParameterName $randomParameterName + * @param EventDispatcher $dispatcher */ - public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName) + public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName, EventDispatcher $dispatcher) { $this->entityManager = $entityManager; $this->randomParameterName = $randomParameterName; + $this->dispatcher = $dispatcher; } /** @@ -68,7 +78,19 @@ public function assembleContactsSegmentQueryBuilder(ContactSegmentFilters $conta } $references = $references + $segmentIdArray; } + // This has to run for every filter + $filterCrate = $filter->contactSegmentFilterCrate->getArray(); + $queryBuilder = $filter->applyQuery($queryBuilder); + + if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { + $alias = $this->generateRandomParameterName(); + $event = new LeadListFilteringEvent($filterCrate, null, $alias, $filterCrate['operator'], $queryBuilder, $this->entityManager); + $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); + if ($event->isFilteringDone()) { + $queryBuilder->andWhere($event->getSubQuery()); + } + } } $queryBuilder->applyStackLogic(); From 6daf3dcc9e0ab4f8f28d4eb360e3125093fd94b7 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 10 May 2018 15:51:21 +0200 Subject: [PATCH 283/778] fix dispatcher to to interface --- .../Segment/Query/ContactSegmentQueryBuilder.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 2e1ea1342a3..d56c4cff408 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -18,7 +18,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilters; use Mautic\LeadBundle\Segment\Exception\SegmentQueryException; use Mautic\LeadBundle\Segment\RandomParameterName; -use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Class ContactSegmentQueryBuilder is responsible for building queries for segments. @@ -32,18 +32,18 @@ class ContactSegmentQueryBuilder private $randomParameterName; /** - * @var EventDispatcher + * @var EventDispatcherInterface */ private $dispatcher; /** * ContactSegmentQueryBuilder constructor. * - * @param EntityManager $entityManager - * @param RandomParameterName $randomParameterName - * @param EventDispatcher $dispatcher + * @param EntityManager $entityManager + * @param RandomParameterName $randomParameterName + * @param EventDispatcherInterface $dispatcher */ - public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName, EventDispatcher $dispatcher) + public function __construct(EntityManager $entityManager, RandomParameterName $randomParameterName, EventDispatcherInterface $dispatcher) { $this->entityManager = $entityManager; $this->randomParameterName = $randomParameterName; From 0f3de2d76c5cf1cbe13e95258421a933e671186a Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 10 May 2018 17:53:17 +0200 Subject: [PATCH 284/778] Fix for Date filter - date field is / is not empty --- .../Segment/Decorator/Date/DateOptionFactory.php | 5 +++++ .../Segment/Decorator/Date/DateOptionFactoryTest.php | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php index b07fdcf64e3..2cb964a1a3d 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php +++ b/app/bundles/LeadBundle/Segment/Decorator/Date/DateOptionFactory.php @@ -63,6 +63,11 @@ public function getDateOption(ContactSegmentFilterCrate $leadSegmentFilterCrate) $dateOptionParameters = new DateOptionParameters($leadSegmentFilterCrate, $relativeDateStrings); $timeframe = $dateOptionParameters->getTimeframe(); + + if (!$timeframe) { + return new DateDefault($this->dateDecorator, $originalValue); + } + switch ($timeframe) { case 'birthday': case 'anniversary': diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php index 105c04bd04c..a9c93502825 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/Date/DateOptionFactoryTest.php @@ -243,6 +243,18 @@ public function testDateDefault() $this->assertInstanceOf(DateDefault::class, $filterDecorator); } + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\Date\DateOptionFactory::getDateOption + */ + public function testNullValue() + { + $filterName = null; + + $filterDecorator = $this->getFilterDecorator($filterName); + + $this->assertInstanceOf(DateDefault::class, $filterDecorator); + } + /** * @param string $filterName * From 5dc7b7a7089fcd9f0c935c16ef301548c279d4fd Mon Sep 17 00:00:00 2001 From: John Linhart Date: Fri, 11 May 2018 16:42:24 +0200 Subject: [PATCH 285/778] Store tokens in redirect URLs and replace the tokens on redirect (#6043) * Store tokens in redirect URLs and replace the tokens on redirect * Return 404 on redirect to empty URL * Return prepareUrlForTracking back for HTML emails and adjust it with the tests to allow tokens in URLs * New method in UrlHelper to find all URLs in a text + tests * New method in TokenHelper to get value from a token when list of tokens provided + test * Implement new smarter methods in helpers into the TrackableModel + test * Doc block added --- app/bundles/CoreBundle/Helper/UrlHelper.php | 101 +++++++++++++++++ .../Tests/unit/Helper/UrlHelperTest.php | 105 ++++++++++++++++++ app/bundles/LeadBundle/Helper/TokenHelper.php | 17 +++ .../Tests/Helper/TokenHelperTest.php | 48 ++++++++ .../Controller/PublicController.php | 18 ++- .../PageBundle/Event/UntrackableUrlsEvent.php | 3 - .../PageBundle/Model/TrackableModel.php | 77 +++---------- .../Tests/Model/TrackableModelTest.php | 101 +++++++++++++---- 8 files changed, 380 insertions(+), 90 deletions(-) create mode 100644 app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php diff --git a/app/bundles/CoreBundle/Helper/UrlHelper.php b/app/bundles/CoreBundle/Helper/UrlHelper.php index 34157f01743..bde80bdb02d 100644 --- a/app/bundles/CoreBundle/Helper/UrlHelper.php +++ b/app/bundles/CoreBundle/Helper/UrlHelper.php @@ -146,4 +146,105 @@ public static function rel2abs($rel) /* absolute URL is ready! */ return $scheme.'://'.$abs; } + + /** + * Takes a plaintext, finds all URLs in it and return the array of those URLs. + * With exception of URLs used as a token default values. + * + * @param string $text + * + * @return array + */ + public static function getUrlsFromPlaintext($text) + { + $regex = '#[-a-zA-Z0-9@:%_\+.~\#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~\#?&//=]*)?#si'; + + if (!preg_match_all($regex, $text, $matches)) { + return []; + } + + $urls = $matches[0]; + + foreach ($urls as $key => $url) { + // We don't want to match URLs in token default values + // like {contactfield=website|http://ignore.this.url} + $isDefautlTokenValue = stripos($text, "|$url}") !== false; + if ($isDefautlTokenValue) { + unset($urls[$key]); + } + } + + return $urls; + } + + /** + * Sanitize parts of the URL to make sure the URL query values are HTTP encoded. + * + * @param string $url + * + * @return string + */ + public static function sanitizeAbsoluteUrl($url) + { + if (!$url) { + return $url; + } + + $url = self::sanitizeUrlScheme($url); + $url = self::sanitizeUrlQuery($url); + + return $url; + } + + /** + * Make sure the URL has a scheme. Defaults to HTTP if not provided. + * + * @param string $url + * + * @return string + */ + private static function sanitizeUrlScheme($url) + { + $isRelative = strpos($url, '//') === 0; + + if ($isRelative) { + return $url; + } + + $containSlashes = strpos($url, '://') !== false; + + if (!$containSlashes) { + $url = sprintf('://%s', $url); + } + + $scheme = parse_url($url, PHP_URL_SCHEME); + + // Set default scheme to http if missing + if (empty($scheme)) { + $url = sprintf('http%s', $url); + } + + return $url; + } + + /** + * @param string $url + * + * @return string + */ + private static function sanitizeUrlQuery($url) + { + $query = parse_url($url, PHP_URL_QUERY); + + if (!empty($query)) { + parse_str($query, $parsedQuery); + + if ($parsedQuery) { + $encodedQuery = http_build_query($parsedQuery); + $url = str_replace($query, $encodedQuery, $url); + } + } + + return $url; + } } diff --git a/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php b/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php new file mode 100644 index 00000000000..7c17f6502bb --- /dev/null +++ b/app/bundles/CoreBundle/Tests/unit/Helper/UrlHelperTest.php @@ -0,0 +1,105 @@ +assertEquals( + 'http://username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('http://username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlSetHttpIfSchemeIsMissing() + { + $this->assertEquals( + 'http://username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlSetHttpIfSchemeIsRelative() + { + $this->assertEquals( + '//username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('//username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlDoNotSetHttpIfSchemeIsRelative() + { + $this->assertEquals( + '//username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('//username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlWithHttps() + { + $this->assertEquals( + 'https://username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('https://username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlWithHttp() + { + $this->assertEquals( + 'http://username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('http://username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlWithFtp() + { + $this->assertEquals( + 'ftp://username:password@hostname:9090/path?arg=value#anchor', + UrlHelper::sanitizeAbsoluteUrl('ftp://username:password@hostname:9090/path?arg=value#anchor') + ); + } + + public function testSanitizeAbsoluteUrlSanitizeQuery() + { + $this->assertEquals( + 'http://username:password@hostname:9090/path?ar_g1=value&arg2=some+email%40address.com#anchor', + UrlHelper::sanitizeAbsoluteUrl('http://username:password@hostname:9090/path?ar g1=value&arg2=some+email@address.com#anchor') + ); + } + + public function testGetUrlsFromPlaintextWithHttp() + { + $this->assertEquals( + ['http://mautic.org'], + UrlHelper::getUrlsFromPlaintext('Hello there, http://mautic.org!') + ); + } + + public function testGetUrlsFromPlaintextSkipDefaultTokenValues() + { + $this->assertEquals( + ['https://find.this'], + UrlHelper::getUrlsFromPlaintext('Find this url: https://find.this, but ignore this one: {contactfield=website|http://skip.this}! ') + ); + } + + public function testGetUrlsFromPlaintextWith2Urls() + { + $this->assertEquals( + ['http://mautic.org', 'http://mucktick.org'], + UrlHelper::getUrlsFromPlaintext('Hello there, http://mautic.org is the correct URL. Not http://mucktick.org.') + ); + } +} diff --git a/app/bundles/LeadBundle/Helper/TokenHelper.php b/app/bundles/LeadBundle/Helper/TokenHelper.php index 400c2eead3c..ad8885c9dc0 100644 --- a/app/bundles/LeadBundle/Helper/TokenHelper.php +++ b/app/bundles/LeadBundle/Helper/TokenHelper.php @@ -62,6 +62,23 @@ public static function findLeadTokens($content, $lead, $replace = false) return $replace ? $content : $tokenList; } + /** + * Returns correct token value from provided list of tokens and the concrete token. + * + * @param array $tokens like ['{contactfield=website}' => 'https://mautic.org'] + * @param string $token like '{contactfield=website|https://default.url}' + * + * @return string empty string if no match + */ + public static function getValueFromTokens(array $tokens, $token) + { + $token = str_replace(['{', '}'], '', $token); + $alias = self::getFieldAlias($token); + $default = self::getTokenDefaultValue($token); + + return empty($tokens["{{$alias}}"]) ? $default : $tokens["{{$alias}}"]; + } + /** * @param array $lead * @param $alias diff --git a/app/bundles/LeadBundle/Tests/Helper/TokenHelperTest.php b/app/bundles/LeadBundle/Tests/Helper/TokenHelperTest.php index 01b578b2525..23545592feb 100644 --- a/app/bundles/LeadBundle/Tests/Helper/TokenHelperTest.php +++ b/app/bundles/LeadBundle/Tests/Helper/TokenHelperTest.php @@ -106,4 +106,52 @@ public function testValueIsUrlEncoded() $tokenList = TokenHelper::findLeadTokens($token, $lead); $this->assertEquals([$token => 'Somewhere%26Else'], $tokenList); } + + public function testGetValueFromTokensWhenSomeValue() + { + $token = '{contactfield=website}'; + $tokens = [ + '{contactfield=website}' => 'https://mautic.org', + ]; + $this->assertEquals( + 'https://mautic.org', + TokenHelper::getValueFromTokens($tokens, $token) + ); + } + + public function testGetValueFromTokensWhenSomeValueWithDefaultValue() + { + $token = '{contactfield=website|ftp://default.url}'; + $tokens = [ + '{contactfield=website}' => 'https://mautic.org', + ]; + $this->assertEquals( + 'https://mautic.org', + TokenHelper::getValueFromTokens($tokens, $token) + ); + } + + public function testGetValueFromTokensWhenNoValueWithDefaultValue() + { + $token = '{contactfield=website|ftp://default.url}'; + $tokens = [ + '{contactfield=website}' => '', + ]; + $this->assertEquals( + 'ftp://default.url', + TokenHelper::getValueFromTokens($tokens, $token) + ); + } + + public function testGetValueFromTokensWhenNoValueWithoutDefaultValue() + { + $token = '{contactfield=website}'; + $tokens = [ + '{contactfield=website}' => '', + ]; + $this->assertEquals( + '', + TokenHelper::getValueFromTokens($tokens, $token) + ); + } } diff --git a/app/bundles/PageBundle/Controller/PublicController.php b/app/bundles/PageBundle/Controller/PublicController.php index 613a0f9c88e..0477474b1b0 100644 --- a/app/bundles/PageBundle/Controller/PublicController.php +++ b/app/bundles/PageBundle/Controller/PublicController.php @@ -13,6 +13,7 @@ use Mautic\CoreBundle\Controller\FormController as CommonFormController; use Mautic\CoreBundle\Helper\TrackingPixelHelper; +use Mautic\CoreBundle\Helper\UrlHelper; use Mautic\LeadBundle\Helper\TokenHelper; use Mautic\LeadBundle\Model\LeadModel; use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface; @@ -422,22 +423,24 @@ public function redirectAction($redirectId) $redirectModel = $this->getModel('page.redirect'); $redirect = $redirectModel->getRedirectById($redirectId); - if (empty($redirect) || !$redirect->isPublished(false)) { - throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404')); - } /** @var \Mautic\PageBundle\Model\PageModel $pageModel */ $pageModel = $this->getModel('page'); $pageModel->hitPage($redirect, $this->request); $url = $redirect->getUrl(); + if (empty($redirect) || !$redirect->isPublished(false)) { + throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404', ['%url%' => $url])); + } + // Ensure the URL does not have encoded ampersands $url = str_replace('&', '&', $url); // Get query string $query = $this->request->query->all(); - // Unset the clickthrough + // Unset the clickthrough from the URL query + $ct = $query['ct']; unset($query['ct']); // Tak on anything left to the URL @@ -449,9 +452,14 @@ public function redirectAction($redirectId) // Search replace lead fields in the URL /** @var \Mautic\LeadBundle\Model\LeadModel $leadModel */ $leadModel = $this->getModel('lead'); - $lead = $leadModel->getCurrentLead(); + $lead = $leadModel->getContactFromRequest([$ct]); $leadArray = ($lead) ? $lead->getProfileFields() : []; $url = TokenHelper::findLeadTokens($url, $leadArray, true); + $url = UrlHelper::sanitizeAbsoluteUrl($url); + + if (false === filter_var($url, FILTER_VALIDATE_URL)) { + throw $this->createNotFoundException($this->translator->trans('mautic.core.url.error.404', ['%url%' => $url])); + } return $this->redirect($url); } diff --git a/app/bundles/PageBundle/Event/UntrackableUrlsEvent.php b/app/bundles/PageBundle/Event/UntrackableUrlsEvent.php index 9f6f3239ac5..7f1118dd618 100644 --- a/app/bundles/PageBundle/Event/UntrackableUrlsEvent.php +++ b/app/bundles/PageBundle/Event/UntrackableUrlsEvent.php @@ -25,9 +25,6 @@ class UntrackableUrlsEvent extends Event '{webview_url}', '{unsubscribe_url}', '{trackable=(.*?)}', - // Ignore lead fields with URLs for tracking since each is unique - '^{leadfield=(.*?)}', - '^{contactfield=(.*?)}', ]; /** diff --git a/app/bundles/PageBundle/Model/TrackableModel.php b/app/bundles/PageBundle/Model/TrackableModel.php index 6d37b29efe3..bef5bd0a59d 100644 --- a/app/bundles/PageBundle/Model/TrackableModel.php +++ b/app/bundles/PageBundle/Model/TrackableModel.php @@ -11,7 +11,9 @@ namespace Mautic\PageBundle\Model; +use Mautic\CoreBundle\Helper\UrlHelper; use Mautic\CoreBundle\Model\AbstractCommonModel; +use Mautic\LeadBundle\Helper\TokenHelper; use Mautic\PageBundle\Entity\Redirect; use Mautic\PageBundle\Entity\Trackable; use Mautic\PageBundle\Event\UntrackableUrlsEvent; @@ -428,12 +430,12 @@ protected function extractTrackablesFromHtml($html) protected function extractTrackablesFromText($text) { // Remove any HTML tags (such as img) that could contain href or src attributes prior to parsing for links - $text = strip_tags($text); - - // Plaintext links + $text = strip_tags($text); + $allUrls = UrlHelper::getUrlsFromPlaintext($text); $trackableUrls = []; - if (preg_match_all('/((https?|ftps?):\/\/)([a-zA-Z0-9-\.{}]*[a-zA-Z0-9=}]*)(\??)([^\s\]"]+)?/i', $text, $matches)) { - foreach ($matches[0] as $url) { + + if ($allUrls) { + foreach ($allUrls as $url) { if ($preparedUrl = $this->prepareUrlForTracking($url)) { list($urlKey, $urlValue) = $preparedUrl; $trackableUrls[$urlKey] = $urlValue; @@ -441,7 +443,7 @@ protected function extractTrackablesFromText($text) } } - // Any tokens could potentially be a URL so extract and send through prepareUrlForTracking() which will determine + // Any tokens could potentially be a URL so extract and send through prepareUrlForTracking() which will determine // if it's a valid URL or not if (preg_match_all('/{.*?}/i', $text, $matches)) { foreach ($matches[0] as $url) { @@ -508,52 +510,10 @@ protected function prepareUrlForTracking($url) return false; } - // Extract any tokens that are part of the query - $tokenizedParams = $this->extractTokensFromQuery($urlParts); - - // Check if URL is trackable - $tokenizedHost = (!isset($urlParts['host']) && isset($urlParts['path'])) ? $urlParts['path'] : $urlParts['host']; - if (preg_match('/^(\{\S+?\})/', $tokenizedHost, $match)) { - $token = $match[1]; - - // Tokenized hosts shouldn't use a scheme since the token value should contain it - if ($scheme = (!empty($urlParts['scheme'])) ? $urlParts['scheme'] : false) { - // Token has a schema so let's get rid of it before replacing tokens - $this->contentReplacements['first_pass'][$scheme.'://'.$tokenizedHost] = $tokenizedHost; - unset($urlParts['scheme']); - } - - // Validate that the token is something that can be trackable - if (!$this->validateTokenIsTrackable($token, $tokenizedHost)) { - return false; - } - - $trackableUrl = (!empty($urlParts['query'])) ? $this->contentTokens[$token].'?'.$urlParts['query'] : $this->contentTokens[$token]; - $trackableKey = $trackableUrl; + $trackableUrl = $this->httpBuildUrl($urlParts); - // Replace the URL token with the actual URL - $this->contentReplacements['first_pass'][$url] = $trackableUrl; - } else { - // Regular URL without a tokenized host - $trackableUrl = $this->httpBuildUrl($urlParts); - - if ($this->isInDoNotTrack($trackableUrl)) { - return false; - } - } - - // Append tokenized params to the end of the URL as these will not be part of the stored redirect URL - // They'll be passed through as regular parameters outside the trackable token - // For example, {trackable=123}?foo={bar} - if ($tokenizedParams) { - // The URL to be tokenized is without the tokenized parameters - $trackableKey = $trackableUrl.($this->usingClickthrough || (strpos($trackableUrl, '?') !== false) ? '&' : '?'). - $this->httpBuildQuery($tokenizedParams); - - // Replace the original URL with the updated URL before replacing with tokens - if ($trackableKey !== $url) { - $this->contentReplacements['first_pass'][$url] = $trackableKey; - } + if ($this->isInDoNotTrack($trackableUrl)) { + return false; } return [$trackableKey, $trackableUrl]; @@ -588,20 +548,19 @@ protected function isInDoNotTrack($url) */ protected function validateTokenIsTrackable($token, $tokenizedHost = null) { - // Token as URL - if ($tokenizedHost && !preg_match('/^(\{\S+?\})$/', $tokenizedHost)) { - // Currently this does not apply to something like "{leadfield=firstname}.com" since that could result in URL per lead - + // Validate if this token is listed as not to be tracked + if ($this->isInDoNotTrack($token)) { return false; } - // Validate if this token is listed as not to be tracked - if ($this->isInDoNotTrack($token)) { + $tokenValue = TokenHelper::getValueFromTokens($this->contentTokens, $token); + + // Validate that the token is available + if (!$tokenValue) { return false; } - // Validate that the token is available and is a URL - if (!isset($this->contentTokens[$token]) || !$this->isValidUrl($this->contentTokens[$token])) { + if (!$this->isValidUrl($tokenValue)) { return false; } diff --git a/app/bundles/PageBundle/Tests/Model/TrackableModelTest.php b/app/bundles/PageBundle/Tests/Model/TrackableModelTest.php index 712722c2339..a6b072ee394 100644 --- a/app/bundles/PageBundle/Tests/Model/TrackableModelTest.php +++ b/app/bundles/PageBundle/Tests/Model/TrackableModelTest.php @@ -13,6 +13,7 @@ use Mautic\PageBundle\Entity\Redirect; use Mautic\PageBundle\Entity\Trackable; +use Mautic\PageBundle\Model\RedirectModel; use Mautic\PageBundle\Model\TrackableModel; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; @@ -78,11 +79,11 @@ public function testHtmlIsDetectedInContent() */ public function testPlainTextIsDetectedInContent() { - $mockRedirectModel = $this->getMockBuilder('Mautic\PageBundle\Model\RedirectModel') + $mockRedirectModel = $this->getMockBuilder(RedirectModel::class) ->disableOriginalConstructor() ->getMock(); - $mockModel = $this->getMockBuilder('Mautic\PageBundle\Model\TrackableModel') + $mockModel = $this->getMockBuilder(TrackableModel::class) ->setConstructorArgs([$mockRedirectModel]) ->setMethods(['getDoNotTrackList', 'getEntitiesFromUrls', 'createTrackingTokens', 'extractTrackablesFromText']) ->getMock(); @@ -196,7 +197,7 @@ public function testStandardLinkWithoutQuery() public function testStandardLinkWithTokenizedQuery() { $url = 'https://foo-bar.com?foo={contactfield=bar}&bar=foo'; - $model = $this->getModel($url, 'https://foo-bar.com?bar=foo'); + $model = $this->getModel($url, 'https://foo-bar.com?foo={contactfield=bar}&bar=foo'); list($content, $trackables) = $model->parseContentForTrackables( $this->generateContent($url, 'html'), @@ -207,7 +208,7 @@ public function testStandardLinkWithTokenizedQuery() 1 ); - $tokenFound = preg_match('/\{trackable=(.*?)\}&foo=\{contactfield=bar\}/', $content, $match); + $tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match); // Assert that a trackable token exists $this->assertTrue((bool) $tokenFound, $content); @@ -217,27 +218,33 @@ public function testStandardLinkWithTokenizedQuery() } /** - * @testdox Test that a token used in place of a URL is not parsed + * @testdox Test that a token used in place of a URL is parsed properly * * @covers \Mautic\PageBundle\Model\TrackableModel::validateTokenIsTrackable * @covers \Mautic\PageBundle\Model\TrackableModel::parseContentForTrackables * @covers \Mautic\PageBundle\Model\TrackableModel::prepareUrlForTracking */ - public function testTokenizedHostIsIgnored() + public function testTokenizedDomain() { - $url = 'http://{contactfield=foo}.com'; - $model = $this->getModel($url, 'http://{contactfield=foo}.com'); + $url = 'http://{contactfield=foo}.org'; + $model = $this->getModel($url, 'http://{contactfield=foo}.org'); list($content, $trackables) = $model->parseContentForTrackables( $this->generateContent($url, 'html'), [ - '{contactfield=foo}' => '', + '{contactfield=foo}' => 'mautic', ], 'email', 1 ); - $this->assertEmpty($trackables, $content); + $tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match); + + // Assert that a trackable token exists + $this->assertTrue((bool) $tokenFound, $content); + + // Assert the Trackable exists + $this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables); } /** @@ -245,7 +252,7 @@ public function testTokenizedHostIsIgnored() * @covers \Mautic\PageBundle\Model\TrackableModel::parseContentForTrackables * @covers \Mautic\PageBundle\Model\TrackableModel::prepareUrlForTracking */ - public function testTokenizedHostWithSchemeIsIgnored() + public function testTokenizedHostWithScheme() { $url = '{contactfield=foo}'; $model = $this->getModel($url, '{contactfield=foo}'); @@ -253,23 +260,29 @@ public function testTokenizedHostWithSchemeIsIgnored() list($content, $trackables) = $model->parseContentForTrackables( $this->generateContent($url, 'html'), [ - '{contactfield=foo}' => '', + '{contactfield=foo}' => 'https://mautic.org', ], 'email', 1 ); - $this->assertEmpty($trackables, $content); + $tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match); + + // Assert that a trackable token exists + $this->assertTrue((bool) $tokenFound, $content); + + // Assert the Trackable exists + $this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables); } /** - * @testdox Test that a token used in place of a URL is not parsed + * @testdox Test that a token used in place of a URL is parsed * * @covers \Mautic\PageBundle\Model\TrackableModel::validateTokenIsTrackable * @covers \Mautic\PageBundle\Model\TrackableModel::parseContentForTrackables * @covers \Mautic\PageBundle\Model\TrackableModel::prepareUrlForTracking */ - public function testTokenizedHostWithQueryIsIgnored() + public function testTokenizedHostWithQuery() { $url = 'http://{contactfield=foo}.com?foo=bar'; $model = $this->getModel($url, 'http://{contactfield=foo}.com?foo=bar'); @@ -283,7 +296,13 @@ public function testTokenizedHostWithQueryIsIgnored() 1 ); - $this->assertEmpty($trackables, $content); + $tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match); + + // Assert that a trackable token exists + $this->assertTrue((bool) $tokenFound, $content); + + // Assert the Trackable exists + $this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables); } /** @@ -291,7 +310,7 @@ public function testTokenizedHostWithQueryIsIgnored() * @covers \Mautic\PageBundle\Model\TrackableModel::parseContentForTrackables * @covers \Mautic\PageBundle\Model\TrackableModel::prepareUrlForTracking */ - public function testTokenizedHostWithTokenizedQueryIsIgnored() + public function testTokenizedHostWithTokenizedQuery() { $url = 'http://{contactfield=foo}.com?foo={contactfield=bar}'; $model = $this->getModel($url, 'http://{contactfield=foo}.com?foo={contactfield=bar}'); @@ -306,7 +325,13 @@ public function testTokenizedHostWithTokenizedQueryIsIgnored() 1 ); - $this->assertCount(0, $trackables, $content); + $tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match); + + // Assert that a trackable token exists + $this->assertTrue((bool) $tokenFound, $content); + + // Assert the Trackable exists + $this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables); } /** @@ -358,6 +383,39 @@ public function testUnsupportedTokensAreNotConverted() $this->assertEmpty($trackables, $content); } + /** + * @covers \Mautic\PageBundle\Model\TrackableModel::validateTokenIsTrackable + * @covers \Mautic\PageBundle\Model\TrackableModel::parseContentForTrackables + * @covers \Mautic\PageBundle\Model\TrackableModel::prepareUrlForTracking + */ + public function testTokenWithDefaultValueInPlaintextWillCountAsOne() + { + $url = '{contactfield=website|https://mautic.org}'; + $model = $this->getModel($url); + $inputContent = $this->generateContent($url, 'text'); + + list($content, $trackables) = $model->parseContentForTrackables( + $inputContent, + [ + '{contactfield=website}' => 'https://mautic.org/about-us', + ], + 'email', + 1 + ); + + $tokenFound = preg_match('/\{trackable=(.*?)\}/', $content, $match); + + // Assert that a trackable token exists + $this->assertTrue((bool) $tokenFound, $content); + + // Assert the Trackable exists + $trackableKey = '{trackable='.$match[1].'}'; + $this->assertArrayHasKey('{trackable='.$match[1].'}', $trackables); + + $this->assertEquals(1, count($trackables)); + $this->assertEquals('{contactfield=website|https://mautic.org}', $trackables[$trackableKey]->getRedirect()->getUrl()); + } + /** * @testdox Test that a URL injected into the do not track list is not converted * @@ -481,17 +539,14 @@ protected function getModel($urls, $tokenUrls = null, $doNotTrack = []) '{webview_url}', '{unsubscribe_url}', '{trackable=(.*?)}', - // Ignore lead fields as URL hosts for tracking since each is unique - '[^=]{leadfield=(.*?)}', - '[^=]{contactfield=(.*?)}', ] ); - $mockRedirectModel = $this->getMockBuilder('Mautic\PageBundle\Model\RedirectModel') + $mockRedirectModel = $this->getMockBuilder(RedirectModel::class) ->disableOriginalConstructor() ->getMock(); - $mockModel = $this->getMockBuilder('Mautic\PageBundle\Model\TrackableModel') + $mockModel = $this->getMockBuilder(TrackableModel::class) ->setConstructorArgs([$mockRedirectModel]) ->setMethods(['getDoNotTrackList', 'getEntitiesFromUrls']) ->getMock(); From ce7ba4ef731ab602404c8ef735d7d663a048db3e Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Mon, 22 Jan 2018 14:52:00 -0500 Subject: [PATCH 286/778] preview functionality for email builder --- app/bundles/CoreBundle/Assets/css/app.css | 3 +++ .../Controller/BuilderControllerTrait.php | 1 - .../Translations/en_US/messages.ini | 1 + .../CoreBundle/Views/Helper/builder.html.php | 23 ++++++++++++++++++- .../Controller/EmailController.php | 5 ++++ .../EmailBundle/Views/Email/form.html.php | 4 +++- media/css/app.css | 3 ++- 7 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/bundles/CoreBundle/Assets/css/app.css b/app/bundles/CoreBundle/Assets/css/app.css index fe66b73e6a2..21f6039398e 100644 --- a/app/bundles/CoreBundle/Assets/css/app.css +++ b/app/bundles/CoreBundle/Assets/css/app.css @@ -4314,6 +4314,9 @@ lesshat-selector { -lh-property: 0 ; width: 70%; height: 100%; } +.builder-panel #preview .panel-body { + padding: 7px 0; +} .code-mode .builder-panel { width: 50%; position: fixed; diff --git a/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php b/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php index 90207d785c5..cd4245442ab 100644 --- a/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php +++ b/app/bundles/CoreBundle/Controller/BuilderControllerTrait.php @@ -26,7 +26,6 @@ protected function getAssetsForBuilder() /** @var \Symfony\Bundle\FrameworkBundle\Templating\Helper\RouterHelper $routerHelper */ $routerHelper = $this->get('templating.helper.router'); $translator = $this->get('templating.helper.translator'); - $assetsHelper ->setContext(AssetsHelper::CONTEXT_BUILDER) ->addScriptDeclaration("var mauticBasePath = '".$this->request->getBasePath()."';") diff --git a/app/bundles/CoreBundle/Translations/en_US/messages.ini b/app/bundles/CoreBundle/Translations/en_US/messages.ini index cedc886c36e..1170698db2b 100644 --- a/app/bundles/CoreBundle/Translations/en_US/messages.ini +++ b/app/bundles/CoreBundle/Translations/en_US/messages.ini @@ -306,6 +306,7 @@ mautic.core.permissions.viewother="View Others" mautic.core.permissions.viewown="View Own" mautic.core.popupblocked="It seems the browser is blocking popups. Please enable popups for this site and try again." mautic.core.position="Position" +mautic.core.preview="Preview" mautic.core.recent.activity="Recent Activity" mautic.core.redo="Redo" mautic.core.referer="Referer" diff --git a/app/bundles/CoreBundle/Views/Helper/builder.html.php b/app/bundles/CoreBundle/Views/Helper/builder.html.php index eb22ca1484f..1ed0d73ee1c 100644 --- a/app/bundles/CoreBundle/Views/Helper/builder.html.php +++ b/app/bundles/CoreBundle/Views/Helper/builder.html.php @@ -18,7 +18,9 @@
- render('MauticCoreBundle:Helper:builder_buttons.html.php', ['onclick' => "Mautic.closeBuilder('$type');"]); ?> + render('MauticCoreBundle:Helper:builder_buttons.html.php', [ + 'onclick' => "Mautic.closeBuilder('$type');", + ]); ?>
+
+
+

trans('mautic.email.urlvariant'); ?>

+
+
+
+
+ + + + +
+
+
+

trans('mautic.core.slot.types'); ?>

diff --git a/app/bundles/EmailBundle/Controller/EmailController.php b/app/bundles/EmailBundle/Controller/EmailController.php index 93c1aab9ea9..5f96bb99905 100644 --- a/app/bundles/EmailBundle/Controller/EmailController.php +++ b/app/bundles/EmailBundle/Controller/EmailController.php @@ -849,6 +849,11 @@ public function editAction($objectId, $ignorePost = false, $forceTypeSelection = 'builderAssets' => trim(preg_replace('/\s+/', ' ', $this->getAssetsForBuilder())), // strip new lines 'sectionForm' => $sectionForm->createView(), 'permissions' => $permissions, + 'previewUrl' => $this->generateUrl( + 'mautic_email_preview', + ['objectId' => $entity->getId()], + true + ), ], 'contentTemplate' => 'MauticEmailBundle:Email:form.html.php', 'passthroughVars' => [ diff --git a/app/bundles/EmailBundle/Views/Email/form.html.php b/app/bundles/EmailBundle/Views/Email/form.html.php index fac9059e74b..be8d8a28eb9 100644 --- a/app/bundles/EmailBundle/Views/Email/form.html.php +++ b/app/bundles/EmailBundle/Views/Email/form.html.php @@ -276,7 +276,9 @@ 'slots' => $slots, 'sections' => $sections, 'objectId' => $email->getSessionId(), -]); ?> + 'previewUrl' => $previewUrl, +]); +?> getEmailType(); diff --git a/media/css/app.css b/media/css/app.css index 4ea074b7ca4..fa89ba43e6d 100644 --- a/media/css/app.css +++ b/media/css/app.css @@ -446,7 +446,8 @@ solid #ebedf0 !important}.bdr-l{border-left:1px solid #ebedf0 !important}.bdr-r{ .builder-slot .builder-panel-top{margin-bottom:10px}.builder .template-dnd-help, .builder-slot .template-dnd-help, .builder .custom-dnd-help, -.builder-slot .custom-dnd-help{display:table-cell;vertical-align:middle;width:100%}.builder-active{background-color:#fff;position:absolute;top:0;bottom:0;right:0;left:0;width:100%;height:100%;z-index:1030}.builder-panel{position:fixed;top:0;bottom:0;right:0;width:30%;height:100%;padding:15px;background-color:#d5d4d4;overflow-y:auto}.builder-content{position:fixed;left:0;top:0;width:70%;height:100%}.code-mode .builder-panel{width:50%;position:fixed}.code-mode .builder-content{width:50%}.builder-panel .panel +.builder-slot .custom-dnd-help{display:table-cell;vertical-align:middle;width:100%}.builder-active{background-color:#fff;position:absolute;top:0;bottom:0;right:0;left:0;width:100%;height:100%;z-index:1030}.builder-panel{position:fixed;top:0;bottom:0;right:0;width:30%;height:100%;padding:15px;background-color:#d5d4d4;overflow-y:auto}.builder-content{position:fixed;left:0;top:0;width:70%;height:100%}.builder-panel #preview .panel-body{padding:7px +0}.code-mode .builder-panel{width:50%;position:fixed}.code-mode .builder-content{width:50%}.builder-panel .panel a.btn{white-space:normal}.builder-active-slot{background-color:#fff;z-index:1030}.builder-panel-slot{width:50%;padding:15px;background-color:#d5d4d4;overflow-y:auto}.builder-content-slot{left:50%;width:50%}.code-mode .builder-panel-slot{width:50%}.code-mode .builder-content-slot{width:50%}.builder-panel-slot .panel a.btn{white-space:normal}.ui-draggable-iframeFix{z-index:9999 !important}.CodeMirror{border:1px solid #eee;height:auto}.CodeMirror-hints{position:absolute;z-index:9999 !important;overflow:hidden;list-style:none;margin:0;padding:2px;-webkit-box-shadow:2px 3px 5px rgba(0, 0, 0, 0.2);-moz-box-shadow:2px 3px 5px rgba(0, 0, 0, 0.2);box-shadow:2px 3px 5px rgba(0, 0, 0, 0.2);border-radius:3px;border:1px From 95b98eea857235d3e8957b08d746f7a972108fc0 Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Mon, 22 Jan 2018 15:12:07 -0500 Subject: [PATCH 287/778] extending functionality to page builder --- app/bundles/PageBundle/Controller/PageController.php | 3 ++- app/bundles/PageBundle/Views/Page/form.html.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/PageBundle/Controller/PageController.php b/app/bundles/PageBundle/Controller/PageController.php index ac4e7aae1c3..072c5a9deaa 100644 --- a/app/bundles/PageBundle/Controller/PageController.php +++ b/app/bundles/PageBundle/Controller/PageController.php @@ -621,6 +621,7 @@ public function editAction($objectId, $ignorePost = false) 'sections' => $this->buildSlotForms($sections), 'builderAssets' => trim(preg_replace('/\s+/', ' ', $this->getAssetsForBuilder())), // strip new lines 'sectionForm' => $sectionForm->createView(), + 'previewUrl' => $this->generateUrl('mautic_page_preview', ['id' => $objectId], true), 'permissions' => $security->isGranted( [ 'page:preference_center:editown', @@ -628,7 +629,7 @@ public function editAction($objectId, $ignorePost = false) ], 'RETURN_ARRAY' ), - 'security' => $security, + 'security' => $security, ], 'contentTemplate' => 'MauticPageBundle:Page:form.html.php', 'passthroughVars' => [ diff --git a/app/bundles/PageBundle/Views/Page/form.html.php b/app/bundles/PageBundle/Views/Page/form.html.php index d67a2f13de1..16155129477 100644 --- a/app/bundles/PageBundle/Views/Page/form.html.php +++ b/app/bundles/PageBundle/Views/Page/form.html.php @@ -128,4 +128,5 @@ 'slots' => $slots, 'sections' => $sections, 'objectId' => $activePage->getSessionId(), + 'previewUrl' => $previewUrl, ]); ?> From f04ed5cdb33bd34c91232415b46d1733a7e5b5d3 Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Wed, 28 Mar 2018 10:06:46 -0400 Subject: [PATCH 288/778] defining inset previewUrl variable in form.php --- app/bundles/EmailBundle/Views/Email/form.html.php | 4 ++++ app/bundles/PageBundle/Views/Page/form.html.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/bundles/EmailBundle/Views/Email/form.html.php b/app/bundles/EmailBundle/Views/Email/form.html.php index be8d8a28eb9..bb506d879a0 100644 --- a/app/bundles/EmailBundle/Views/Email/form.html.php +++ b/app/bundles/EmailBundle/Views/Email/form.html.php @@ -62,6 +62,10 @@ $isCodeMode = ($email->getTemplate() === 'mautic_code_mode'); +if (!isset($previewUrl)) { + $previewUrl = ''; +} + ?> start($form, ['attr' => $attr]); ?> diff --git a/app/bundles/PageBundle/Views/Page/form.html.php b/app/bundles/PageBundle/Views/Page/form.html.php index 16155129477..d6c4f50af49 100644 --- a/app/bundles/PageBundle/Views/Page/form.html.php +++ b/app/bundles/PageBundle/Views/Page/form.html.php @@ -37,6 +37,10 @@ $isCodeMode = ($activePage->getTemplate() === 'mautic_code_mode'); +if (!isset($previewUrl)) { + $previewUrl = ''; +} + ?> start($form, ['attr' => $attr]); ?> From d64a30272938a69df355816ec099af5f4a0f61be Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Wed, 11 Apr 2018 09:42:49 -0400 Subject: [PATCH 289/778] removing preview section if preview doesn't exist --- .../CoreBundle/Views/Helper/builder.html.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app/bundles/CoreBundle/Views/Helper/builder.html.php b/app/bundles/CoreBundle/Views/Helper/builder.html.php index 1ed0d73ee1c..7da58a68979 100644 --- a/app/bundles/CoreBundle/Views/Helper/builder.html.php +++ b/app/bundles/CoreBundle/Views/Helper/builder.html.php @@ -38,25 +38,27 @@ trans('mautic.core.code.mode.token.dropdown.hint'); ?>
-
-
-

trans('mautic.email.urlvariant'); ?>

-
-
-
-
- - - - + +
+
+

trans('mautic.email.urlvariant'); ?>

+
+
+
+
+ + + + +
-
+

trans('mautic.core.slot.types'); ?>

From faf798264b20dba4711b3d4b8b5cafa22f1b8c0e Mon Sep 17 00:00:00 2001 From: Gabe Alvarez-Millard Date: Wed, 11 Apr 2018 09:46:23 -0400 Subject: [PATCH 290/778] adding preview section to code mode --- .../CoreBundle/Views/Helper/builder.html.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/bundles/CoreBundle/Views/Helper/builder.html.php b/app/bundles/CoreBundle/Views/Helper/builder.html.php index 7da58a68979..e2eb7151f9d 100644 --- a/app/bundles/CoreBundle/Views/Helper/builder.html.php +++ b/app/bundles/CoreBundle/Views/Helper/builder.html.php @@ -33,32 +33,32 @@
+ +
+
+

trans('mautic.email.urlvariant'); ?>

+
+
+
+
+ + + + +
+
+
+
+
trans('mautic.core.code.mode.token.dropdown.hint'); ?>
- -
-
-

trans('mautic.email.urlvariant'); ?>

-
-
-
-
- - - - -
-
-
-
-

trans('mautic.core.slot.types'); ?>

From e05af4dba40033325bc0121ca1988dff9cf87ddf Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 20:14:31 +0200 Subject: [PATCH 291/778] Fixing issues found with PHPSTAN l=0 --- .../DashboardBundle/Controller/Api/WidgetApiController.php | 6 +++--- app/bundles/DashboardBundle/Event/WidgetDetailEvent.php | 2 +- app/bundles/DashboardBundle/Form/Type/WidgetType.php | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php index e36ca5aa363..c4f845a1f23 100644 --- a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php +++ b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php @@ -80,12 +80,12 @@ public function getDataAction($type) 'dateFormat' => InputHelper::clean($this->request->get('dateFormat', null)), 'dateFrom' => $fromDate, 'dateTo' => $toDate, - 'limit' => InputHelper::int($this->request->get('limit', null)), + 'limit' => (int) $this->request->get('limit', null), 'filter' => $this->request->get('filter', []), ]; - $cacheTimeout = InputHelper::int($this->request->get('cacheTimeout', null)); - $widgetHeight = InputHelper::int($this->request->get('height', 300)); + $cacheTimeout = (int) $this->request->get('cacheTimeout', null); + $widgetHeight = (int) $this->request->get('height', 300); $widget = new Widget(); $widget->setParams($params); diff --git a/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php b/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php index 0c0509ec9f7..d5bf9ecfa72 100644 --- a/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php +++ b/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php @@ -254,7 +254,7 @@ public function isCached() /** * Get the Translator object. * - * @return Translator $translator + * @return TranslatorInterface */ public function getTranslator() { diff --git a/app/bundles/DashboardBundle/Form/Type/WidgetType.php b/app/bundles/DashboardBundle/Form/Type/WidgetType.php index e418ede9494..29ac1086e2b 100644 --- a/app/bundles/DashboardBundle/Form/Type/WidgetType.php +++ b/app/bundles/DashboardBundle/Form/Type/WidgetType.php @@ -94,10 +94,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ]); - $ff = $builder->getFormFactory(); - // function to add a form for specific widget type dynamically - $func = function (FormEvent $e) use ($ff, $dispatcher) { + $func = function (FormEvent $e) use ($dispatcher) { $data = $e->getData(); $form = $e->getForm(); $event = new WidgetFormEvent(); From e65cfdce3cf05d291da7bd06287eff459a440485 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 20:16:57 +0200 Subject: [PATCH 292/778] Adding DashboardBundle to PHPSTAN Travis check --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5417a572512..11ff8e7f1b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ script: - bin/phpunit -d memory_limit=2048M --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist --fail-on-warning # Run PHPSTAN analysis for PHP 7+ - - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/DashboardBundle app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi # Check if the code standards weren't broken. # Run it only on PHP 7.1 which should be the fastest. No need to run it for all PHP versions From 92d7dfff89c778819d04072c97650039cce67fe8 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 21:08:04 +0200 Subject: [PATCH 293/778] Removing getParameters which was not called anywhere and if it would have been then it's calling getBase64EncodedFields which does not exist --- .../ConfigBundle/Event/ConfigBuilderEvent.php | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php b/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php index 8a917373083..2eaa5266f53 100644 --- a/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php +++ b/app/bundles/ConfigBundle/Event/ConfigBuilderEvent.php @@ -115,37 +115,6 @@ public function getFormThemes() return $this->formThemes; } - /** - * Helper method can load $parameters array from a config file. - * - * @param string $path (relative from the root dir) - * - * @return array - */ - public function getParameters($path = null) - { - $paramsFile = $this->pathsHelper->getSystemPath('app').$path; - - if (file_exists($paramsFile)) { - // Import the bundle configuration, $parameters is defined in this file - include $paramsFile; - } - - if (!isset($parameters)) { - $parameters = []; - } - - $fields = $this->getBase64EncodedFields(); - $checkThese = array_intersect(array_keys($parameters), $fields); - foreach ($checkThese as $checkMe) { - if (!empty($parameters[$checkMe])) { - $parameters[$checkMe] = base64_decode($parameters[$checkMe]); - } - } - - return $parameters; - } - /** * @param $bundle * From 7d137ee8ae7a70ef53e93801e77f474b92c0cd9e Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 19 Apr 2018 21:08:53 +0200 Subject: [PATCH 294/778] Fixing PHPSTAN l 0 for ConfigBundle --- app/bundles/ConfigBundle/Controller/SysinfoController.php | 1 + app/bundles/ConfigBundle/Form/Type/ConfigType.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/ConfigBundle/Controller/SysinfoController.php b/app/bundles/ConfigBundle/Controller/SysinfoController.php index 58147b599c5..e049bb6433e 100644 --- a/app/bundles/ConfigBundle/Controller/SysinfoController.php +++ b/app/bundles/ConfigBundle/Controller/SysinfoController.php @@ -12,6 +12,7 @@ namespace Mautic\ConfigBundle\Controller; use Mautic\CoreBundle\Controller\FormController; +use Symfony\Component\HttpFoundation\JsonResponse; /** * Class SysinfoController. diff --git a/app/bundles/ConfigBundle/Form/Type/ConfigType.php b/app/bundles/ConfigBundle/Form/Type/ConfigType.php index 1f78e4dfcf8..19078299dec 100644 --- a/app/bundles/ConfigBundle/Form/Type/ConfigType.php +++ b/app/bundles/ConfigBundle/Form/Type/ConfigType.php @@ -62,7 +62,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $builder->addEventListener( FormEvents::PRE_SET_DATA, - function (FormEvent $event) use ($options) { + function (FormEvent $event) { $form = $event->getForm(); foreach ($form as $config => $configForm) { From 4edd236994d858ad56a0835ba197a2a7f2cb20b9 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Fri, 20 Apr 2018 20:22:01 +0200 Subject: [PATCH 295/778] Removing duplicated attr param from an array --- app/bundles/DashboardBundle/Form/Type/WidgetType.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/bundles/DashboardBundle/Form/Type/WidgetType.php b/app/bundles/DashboardBundle/Form/Type/WidgetType.php index 29ac1086e2b..c94c33f1c4f 100644 --- a/app/bundles/DashboardBundle/Form/Type/WidgetType.php +++ b/app/bundles/DashboardBundle/Form/Type/WidgetType.php @@ -57,7 +57,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label' => 'mautic.dashboard.widget.form.type', 'choices' => $event->getTypes(), 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], 'empty_value' => 'mautic.core.select', 'attr' => [ 'class' => 'form-control', From 6056d414bf34dbd468473e0a0fc1cc40dee64a73 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Fri, 20 Apr 2018 20:29:56 +0200 Subject: [PATCH 296/778] Adding ConfigBundle to the PHPSTAN Travis check --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 11ff8e7f1b8..264a0de5488 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ script: - bin/phpunit -d memory_limit=2048M --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist --fail-on-warning # Run PHPSTAN analysis for PHP 7+ - - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/DashboardBundle app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi + - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.6" ]]; then ~/.composer/vendor/phpstan/phpstan-shim/phpstan.phar analyse app/bundles/DashboardBundle app/bundles/ConfigBundle app/bundles/CampaignBundle app/bundles/WebhookBundle app/bundles/LeadBundle; fi # Check if the code standards weren't broken. # Run it only on PHP 7.1 which should be the fastest. No need to run it for all PHP versions From 3607a4294913cffd0330508f0e19542d4897678f Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 24 Apr 2018 18:35:45 +0200 Subject: [PATCH 297/778] CompanyController had duplicated code - removed and was adding fields that started on 'field_' only for some reason. --- .../Controller/CompanyController.php | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/bundles/LeadBundle/Controller/CompanyController.php b/app/bundles/LeadBundle/Controller/CompanyController.php index 6e4bb5f0ff0..29096c7b01f 100644 --- a/app/bundles/LeadBundle/Controller/CompanyController.php +++ b/app/bundles/LeadBundle/Controller/CompanyController.php @@ -351,24 +351,12 @@ public function editAction($objectId, $ignorePost = false) $data = $this->request->request->get('company'); //pull the data from the form in order to apply the form's formatting foreach ($form as $f) { - $name = $f->getName(); - if (strpos($name, 'field_') === 0) { - $data[$name] = $f->getData(); - } - } - $model->setFieldValues($entity, $data, true); - //form is valid so process the data - $data = $this->request->request->get('company'); - - //pull the data from the form in order to apply the form's formatting - foreach ($form as $f) { - $name = $f->getName(); - if (strpos($name, 'field_') === 0) { - $data[$name] = $f->getData(); - } + $data[$f->getName()] = $f->getData(); } $model->setFieldValues($entity, $data, true); + + //form is valid so process the data $model->saveEntity($entity, $form->get('buttons')->get('save')->isClicked()); $this->addFlash( From 5745b3e6cd43a6d70707a5fb94a64ca1338de792 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 24 Apr 2018 17:48:27 -0500 Subject: [PATCH 298/778] Dev version bump --- app/AppKernel.php | 4 ++-- app/version.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/AppKernel.php b/app/AppKernel.php index 67ab9ae1981..3e396c3e28a 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -41,7 +41,7 @@ class AppKernel extends Kernel * * @const integer */ - const PATCH_VERSION = 1; + const PATCH_VERSION = 2; /** * Extra version identifier. @@ -51,7 +51,7 @@ class AppKernel extends Kernel * * @const string */ - const EXTRA_VERSION = ''; + const EXTRA_VERSION = '-dev'; /** * @var array diff --git a/app/version.txt b/app/version.txt index 94f15e9cc30..1e462c61d86 100644 --- a/app/version.txt +++ b/app/version.txt @@ -1 +1 @@ -2.13.1 +2.13.2-dev From eb2b03064c5427062e83a95870d238e262c11497 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Wed, 25 Apr 2018 09:39:26 +0200 Subject: [PATCH 299/778] Make pre-comit hook executable --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 47a38b95556..8b9d5fd7b84 100644 --- a/composer.json +++ b/composer.json @@ -138,12 +138,14 @@ "post-install-cmd": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", - "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"" + "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" ], "post-update-cmd": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", - "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"" + "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" ], "test": "bin/phpunit --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist" }, From 51fecccce49eb06d1a9b671c008d3c6902dec7f7 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Wed, 25 Apr 2018 12:22:18 +0200 Subject: [PATCH 300/778] Path and permission fixed for the pre-commit hook permission fix --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8b9d5fd7b84..6519cf78b5b 100644 --- a/composer.json +++ b/composer.json @@ -139,13 +139,13 @@ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", - "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./.git/hooks/pre-commit',0755);}\"" ], "post-update-cmd": [ "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", "php -r \"if(file_exists('./.git')&&file_exists('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''))){copy('./build/hooks/pre-commit'.(PHP_OS=='WINNT'?'.win':''),'./.git/hooks/pre-commit');}\"", - "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./build/hooks/pre-commit','0755');}\"" + "php -r \"if(file_exists('./.git/hooks/pre-commit')&&(PHP_OS!='WINNT')){chmod('./.git/hooks/pre-commit',0755);}\"" ], "test": "bin/phpunit --bootstrap vendor/autoload.php --configuration app/phpunit.xml.dist" }, From 867e312d0587039137c8a55acc21c3af5cef1db4 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:49:17 +0200 Subject: [PATCH 301/778] Support syncList form in Focus --- .../FormBundle/Views/Field/field_helper.php | 1 - .../FormBundle/Views/Field/select.html.php | 1 - plugins/MauticFocusBundle/Config/config.php | 1 + .../MauticFocusBundle/Model/FocusModel.php | 20 ++++++++++++++----- .../Views/Builder/form.html.php | 13 ++++++------ 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 0c5c3c04110..983718ed92b 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -114,7 +114,6 @@ } $appendAttribute($containerAttr, 'class', $defaultContainerClass); - // Setup list parsing if (isset($list) || isset($properties['syncList']) || isset($properties['list']) || isset($properties['optionlist'])) { $parseList = []; diff --git a/app/bundles/FormBundle/Views/Field/select.html.php b/app/bundles/FormBundle/Views/Field/select.html.php index d4594da0218..f94a004f908 100644 --- a/app/bundles/FormBundle/Views/Field/select.html.php +++ b/app/bundles/FormBundle/Views/Field/select.html.php @@ -13,7 +13,6 @@ $containerType = 'select'; include __DIR__.'/field_helper.php'; - if (!empty($properties['multiple'])) { $inputAttr .= ' multiple="multiple"'; } diff --git a/plugins/MauticFocusBundle/Config/config.php b/plugins/MauticFocusBundle/Config/config.php index be561bfdb20..e69b9dca1ba 100644 --- a/plugins/MauticFocusBundle/Config/config.php +++ b/plugins/MauticFocusBundle/Config/config.php @@ -143,6 +143,7 @@ 'mautic.helper.templating', 'event_dispatcher', 'mautic.lead.model.lead', + 'mautic.lead.model.field', ], ], ], diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 17b823f20b0..5d4f28a9e10 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -17,6 +17,7 @@ use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\TemplatingHelper; use Mautic\CoreBundle\Model\FormModel; +use Mautic\LeadBundle\Model\FieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use MauticPlugin\MauticFocusBundle\Entity\Focus; @@ -56,6 +57,11 @@ class FocusModel extends FormModel */ protected $leadModel; + /** + * @var FieldModel + */ + protected $leadFieldModel; + /** * FocusModel constructor. * @@ -64,14 +70,16 @@ class FocusModel extends FormModel * @param TemplatingHelper $templating * @param EventDispatcherInterface $dispatcher * @param LeadModel $leadModel + * @param FieldModel $leadFieldModel */ - public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel) + public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel, FieldModel $leadFieldModel) { $this->formModel = $formModel; $this->trackableModel = $trackableModel; $this->templating = $templating; $this->dispatcher = $dispatcher; $this->leadModel = $leadModel; + $this->leadFieldModel = $leadFieldModel; } /** @@ -266,10 +274,12 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, + 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), + 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), ] ) : ''; diff --git a/plugins/MauticFocusBundle/Views/Builder/form.html.php b/plugins/MauticFocusBundle/Views/Builder/form.html.php index 327ad27bb6b..806aae27abf 100644 --- a/plugins/MauticFocusBundle/Views/Builder/form.html.php +++ b/plugins/MauticFocusBundle/Views/Builder/form.html.php @@ -25,7 +25,7 @@ render('MauticFormBundle:Builder:script.html.php', ['form' => $form, 'formName' => $formName]); ?> - EXTRA; echo $view->render('MauticFormBundle:Builder:form.html.php', [ - 'form' => $form, - 'formExtra' => $formExtra, - 'action' => ($preview) ? '#' : null, - 'suffix' => '_focus', + 'form' => $form, + 'formExtra' => $formExtra, + 'action' => ($preview) ? '#' : null, + 'suffix' => '_focus', + 'contactFields' => $contactFields, + 'companyFields' => $companyFields, ] ); ?> From 1f3206dc901ab2bd2f560d6687008acde8d66d9e Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:54:51 +0200 Subject: [PATCH 302/778] R --- .../FormBundle/Views/Field/field_helper.php | 2 +- commands.php | 80 +++++ progress.json | 1 + tester.php | 324 ++++++++++++++++++ 4 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 commands.php create mode 100644 progress.json create mode 100644 tester.php diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 983718ed92b..1e186bc5f49 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -80,7 +80,7 @@ if ($field['inputAttributes']) { $inputAttr .= ' '.htmlspecialchars_decode($field['inputAttributes']); - } + } $appendAttribute($inputAttr, 'class', $defaultInputClass); } diff --git a/commands.php b/commands.php new file mode 100644 index 00000000000..0fe70b623b7 --- /dev/null +++ b/commands.php @@ -0,0 +1,80 @@ +'; + echo '

Specify what task to run. You can run these:'; + echo '

    '; + foreach ($allowedTasks as $task) { + $href = $link . '&task=' . urlencode($task); + echo '
  • ' . $task . '
  • '; + } + echo '

Read more'; + echo '
Please, backup your database before executing the doctrine commands!

'; + die; +} + +$task = urldecode($_GET['task']); +if (!in_array($task, $allowedTasks)) { + echo 'Task ' . $task . ' is not allowed.'; + die; +} +$fullCommand = explode(' ', $task); +$command = $fullCommand[0]; +$argsCount = count($fullCommand) - 1; +$args = array('console', $command); +if ($argsCount) { + for ($i = 1; $i <= $argsCount; $i++) { + $args[] = $fullCommand[$i]; + } +} +echo ''; +echo '

Executing ' . implode(' ', $args) . '

'; + +require_once __DIR__.'/app/autoload.php'; +// require_once __DIR__.'/app/bootstrap.php.cache'; +require_once __DIR__.'/app/AppKernel.php'; +require __DIR__.'/vendor/autoload.php'; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\BufferedOutput; + +defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); +try { + $input = new ArgvInput($args); + $output = new BufferedOutput(); + $kernel = new AppKernel('prod', false); + $app = new Application($kernel); + $app->setAutoExit(false); + $result = $app->run($input, $output); + echo "
\n".$output->fetch().'
'; +} catch (\Exception $exception) { + echo $exception->getMessage(); +} \ No newline at end of file diff --git a/progress.json b/progress.json new file mode 100644 index 00000000000..b2422db8246 --- /dev/null +++ b/progress.json @@ -0,0 +1 @@ +{"progress":100} \ No newline at end of file diff --git a/tester.php b/tester.php new file mode 100644 index 00000000000..b1864131306 --- /dev/null +++ b/tester.php @@ -0,0 +1,324 @@ +testerfilename = basename(__FILE__); + $allowedTasks = array( + 'apply', + 'remove' + ); + + + + $task = isset($_REQUEST['task']) ? $_REQUEST['task'] : ''; + $patch = isset($_REQUEST['patch']) ? $_REQUEST['patch'] : ''; + $cmd = isset($_REQUEST['cmd']) ? $_REQUEST['cmd'] : ''; + + // Controller + if (in_array($task, $allowedTasks)) { + @set_time_limit(9999); + if(empty($cmd)) { + $cmd = []; + } + $count = 2 + count($cmd); + $counter = 1; + file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); + $this->$task($patch); + foreach ($cmd as $c => $value) { + $counter++; + file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); + $this->executeCommand($c); + } + file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>100])); + } + else + { + $this->start(); + file_put_contents(__DIR__ . '/progress.json', null); + } + } + + function apply($patch = null) + { + if ($patch) { + $patchUrl = $this->url.$patch.'.patch'; + echo $patchUrl; + $result = exec('curl ' . $patchUrl . ' | git apply'); + print_R($result); + } else { + echo 'Apply with no Patch ID'; + } + } + + function remove($patch = null) + { + if ($patch) { + $patchUrl = $this->url.$patch.'.patch'; + $result = exec('curl ' . $patchUrl . ' | git apply -R'); + + return $result; + } else { + echo 'Could not remove Patch'; + } + } + + function executeCommand($cmd){ + + // cache clear by remove dir + if($cmd == "cache:clear"){ + return exec('rm -r app/cache/prod/'); + } + + $fullCommand = explode(' ', $cmd); + $command = $fullCommand[0]; + $argsCount = count($fullCommand) - 1; + $args = array('console', $command); + if ($argsCount) { + for ($i = 1; $i <= $argsCount; $i++) { + $args[] = $fullCommand[$i]; + } + } + defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); + try { + $input = new ArgvInput($args); + $output = new BufferedOutput(); + $kernel = new AppKernel('prod', false); + $app = new Application($kernel); + $app->setAutoExit(false); + $result = $app->run($input, $output); + echo "
\n".$output->fetch().'
'; + } catch (\Exception $exception) { + echo $exception->getMessage(); + } + } + + + // View + function start() + { ?> + + + + + Mautic Patch Tester + + + + + + +
+ + + localfile))) + { + $dir_class = "alert alert-danger"; + $msg = "

This path is not writable. Please change permissions before continuing.

"; + // $continue = "disabled"; + } + else + { + $dir_class = "alert alert-secondary"; + $msg = ""; + $continue = ""; + } + ?> + + + +
+

Start Testing!

+

This app will allow you to immediately begin testing pull requests against your Mautic installation. You will need to make sure this file is in the root of your Mautic test instance.

+ +
Current Path
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+

Apply Pull Request

+
+
+
+ + + e.g. Simply enter 3456 for PR #3456 +
+
+
Run after apply pull request
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+

Remove Pull Request

+
+
+
+ + + e.g. Simply enter 3456 for PR #3456 +
+
+
Run after remove pull request
+
+ +
+
+
+
+ +
+ + +
+
+
+ +
+
+
+
*This app does not yet take into account any pull requests that require database changes.
+ + +
+ + + + + + + + + Date: Mon, 9 Apr 2018 15:56:05 +0200 Subject: [PATCH 303/778] Revert --- app/bundles/FormBundle/Views/Field/field_helper.php | 2 +- app/bundles/FormBundle/Views/Field/select.html.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 1e186bc5f49..983718ed92b 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -80,7 +80,7 @@ if ($field['inputAttributes']) { $inputAttr .= ' '.htmlspecialchars_decode($field['inputAttributes']); - } + } $appendAttribute($inputAttr, 'class', $defaultInputClass); } diff --git a/app/bundles/FormBundle/Views/Field/select.html.php b/app/bundles/FormBundle/Views/Field/select.html.php index f94a004f908..d4594da0218 100644 --- a/app/bundles/FormBundle/Views/Field/select.html.php +++ b/app/bundles/FormBundle/Views/Field/select.html.php @@ -13,6 +13,7 @@ $containerType = 'select'; include __DIR__.'/field_helper.php'; + if (!empty($properties['multiple'])) { $inputAttr .= ' multiple="multiple"'; } From 40ab3e3c51dd057d8ccf07fe47aa7b77ea006792 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:57:03 +0200 Subject: [PATCH 304/778] Revert "Support syncList form in Focus" This reverts commit 4cf84d6e1ca94374399a10849589b3f25bb52085. --- .../FormBundle/Views/Field/field_helper.php | 1 + plugins/MauticFocusBundle/Config/config.php | 1 - .../MauticFocusBundle/Model/FocusModel.php | 20 +++++-------------- .../Views/Builder/form.html.php | 13 ++++++------ 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/app/bundles/FormBundle/Views/Field/field_helper.php b/app/bundles/FormBundle/Views/Field/field_helper.php index 983718ed92b..0c5c3c04110 100644 --- a/app/bundles/FormBundle/Views/Field/field_helper.php +++ b/app/bundles/FormBundle/Views/Field/field_helper.php @@ -114,6 +114,7 @@ } $appendAttribute($containerAttr, 'class', $defaultContainerClass); + // Setup list parsing if (isset($list) || isset($properties['syncList']) || isset($properties['list']) || isset($properties['optionlist'])) { $parseList = []; diff --git a/plugins/MauticFocusBundle/Config/config.php b/plugins/MauticFocusBundle/Config/config.php index e69b9dca1ba..be561bfdb20 100644 --- a/plugins/MauticFocusBundle/Config/config.php +++ b/plugins/MauticFocusBundle/Config/config.php @@ -143,7 +143,6 @@ 'mautic.helper.templating', 'event_dispatcher', 'mautic.lead.model.lead', - 'mautic.lead.model.field', ], ], ], diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 5d4f28a9e10..17b823f20b0 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -17,7 +17,6 @@ use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\TemplatingHelper; use Mautic\CoreBundle\Model\FormModel; -use Mautic\LeadBundle\Model\FieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use MauticPlugin\MauticFocusBundle\Entity\Focus; @@ -57,11 +56,6 @@ class FocusModel extends FormModel */ protected $leadModel; - /** - * @var FieldModel - */ - protected $leadFieldModel; - /** * FocusModel constructor. * @@ -70,16 +64,14 @@ class FocusModel extends FormModel * @param TemplatingHelper $templating * @param EventDispatcherInterface $dispatcher * @param LeadModel $leadModel - * @param FieldModel $leadFieldModel */ - public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel, FieldModel $leadFieldModel) + public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel) { $this->formModel = $formModel; $this->trackableModel = $trackableModel; $this->templating = $templating; $this->dispatcher = $dispatcher; $this->leadModel = $leadModel; - $this->leadFieldModel = $leadFieldModel; } /** @@ -274,12 +266,10 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, - 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), - 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, ] ) : ''; diff --git a/plugins/MauticFocusBundle/Views/Builder/form.html.php b/plugins/MauticFocusBundle/Views/Builder/form.html.php index 806aae27abf..327ad27bb6b 100644 --- a/plugins/MauticFocusBundle/Views/Builder/form.html.php +++ b/plugins/MauticFocusBundle/Views/Builder/form.html.php @@ -25,7 +25,7 @@ render('MauticFormBundle:Builder:script.html.php', ['form' => $form, 'formName' => $formName]); ?> + EXTRA; echo $view->render('MauticFormBundle:Builder:form.html.php', [ - 'form' => $form, - 'formExtra' => $formExtra, - 'action' => ($preview) ? '#' : null, - 'suffix' => '_focus', - 'contactFields' => $contactFields, - 'companyFields' => $companyFields, + 'form' => $form, + 'formExtra' => $formExtra, + 'action' => ($preview) ? '#' : null, + 'suffix' => '_focus', ] ); ?> From 96e73be9e748ca3027df6f58a186b76f413f97e6 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 15:58:57 +0200 Subject: [PATCH 305/778] Revert "R" This reverts commit 4e18bb0c42445ead58f3849abdba6ac708368fc4. --- commands.php | 80 ------------- progress.json | 1 - tester.php | 324 -------------------------------------------------- 3 files changed, 405 deletions(-) delete mode 100644 commands.php delete mode 100644 progress.json delete mode 100644 tester.php diff --git a/commands.php b/commands.php deleted file mode 100644 index 0fe70b623b7..00000000000 --- a/commands.php +++ /dev/null @@ -1,80 +0,0 @@ -'; - echo '

Specify what task to run. You can run these:'; - echo '

    '; - foreach ($allowedTasks as $task) { - $href = $link . '&task=' . urlencode($task); - echo '
  • ' . $task . '
  • '; - } - echo '

Read more'; - echo '
Please, backup your database before executing the doctrine commands!

'; - die; -} - -$task = urldecode($_GET['task']); -if (!in_array($task, $allowedTasks)) { - echo 'Task ' . $task . ' is not allowed.'; - die; -} -$fullCommand = explode(' ', $task); -$command = $fullCommand[0]; -$argsCount = count($fullCommand) - 1; -$args = array('console', $command); -if ($argsCount) { - for ($i = 1; $i <= $argsCount; $i++) { - $args[] = $fullCommand[$i]; - } -} -echo ''; -echo '

Executing ' . implode(' ', $args) . '

'; - -require_once __DIR__.'/app/autoload.php'; -// require_once __DIR__.'/app/bootstrap.php.cache'; -require_once __DIR__.'/app/AppKernel.php'; -require __DIR__.'/vendor/autoload.php'; -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Output\BufferedOutput; - -defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); -try { - $input = new ArgvInput($args); - $output = new BufferedOutput(); - $kernel = new AppKernel('prod', false); - $app = new Application($kernel); - $app->setAutoExit(false); - $result = $app->run($input, $output); - echo "
\n".$output->fetch().'
'; -} catch (\Exception $exception) { - echo $exception->getMessage(); -} \ No newline at end of file diff --git a/progress.json b/progress.json deleted file mode 100644 index b2422db8246..00000000000 --- a/progress.json +++ /dev/null @@ -1 +0,0 @@ -{"progress":100} \ No newline at end of file diff --git a/tester.php b/tester.php deleted file mode 100644 index b1864131306..00000000000 --- a/tester.php +++ /dev/null @@ -1,324 +0,0 @@ -testerfilename = basename(__FILE__); - $allowedTasks = array( - 'apply', - 'remove' - ); - - - - $task = isset($_REQUEST['task']) ? $_REQUEST['task'] : ''; - $patch = isset($_REQUEST['patch']) ? $_REQUEST['patch'] : ''; - $cmd = isset($_REQUEST['cmd']) ? $_REQUEST['cmd'] : ''; - - // Controller - if (in_array($task, $allowedTasks)) { - @set_time_limit(9999); - if(empty($cmd)) { - $cmd = []; - } - $count = 2 + count($cmd); - $counter = 1; - file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); - $this->$task($patch); - foreach ($cmd as $c => $value) { - $counter++; - file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>floor((100/$count)*$counter)])); - $this->executeCommand($c); - } - file_put_contents(__DIR__ . '/progress.json', json_encode(['progress'=>100])); - } - else - { - $this->start(); - file_put_contents(__DIR__ . '/progress.json', null); - } - } - - function apply($patch = null) - { - if ($patch) { - $patchUrl = $this->url.$patch.'.patch'; - echo $patchUrl; - $result = exec('curl ' . $patchUrl . ' | git apply'); - print_R($result); - } else { - echo 'Apply with no Patch ID'; - } - } - - function remove($patch = null) - { - if ($patch) { - $patchUrl = $this->url.$patch.'.patch'; - $result = exec('curl ' . $patchUrl . ' | git apply -R'); - - return $result; - } else { - echo 'Could not remove Patch'; - } - } - - function executeCommand($cmd){ - - // cache clear by remove dir - if($cmd == "cache:clear"){ - return exec('rm -r app/cache/prod/'); - } - - $fullCommand = explode(' ', $cmd); - $command = $fullCommand[0]; - $argsCount = count($fullCommand) - 1; - $args = array('console', $command); - if ($argsCount) { - for ($i = 1; $i <= $argsCount; $i++) { - $args[] = $fullCommand[$i]; - } - } - defined('IN_MAUTIC_CONSOLE') or define('IN_MAUTIC_CONSOLE', 1); - try { - $input = new ArgvInput($args); - $output = new BufferedOutput(); - $kernel = new AppKernel('prod', false); - $app = new Application($kernel); - $app->setAutoExit(false); - $result = $app->run($input, $output); - echo "
\n".$output->fetch().'
'; - } catch (\Exception $exception) { - echo $exception->getMessage(); - } - } - - - // View - function start() - { ?> - - - - - Mautic Patch Tester - - - - - - -
- - - localfile))) - { - $dir_class = "alert alert-danger"; - $msg = "

This path is not writable. Please change permissions before continuing.

"; - // $continue = "disabled"; - } - else - { - $dir_class = "alert alert-secondary"; - $msg = ""; - $continue = ""; - } - ?> - - - -
-

Start Testing!

-

This app will allow you to immediately begin testing pull requests against your Mautic installation. You will need to make sure this file is in the root of your Mautic test instance.

- -
Current Path
-
- - -
-
-
-
- -
-
-
- -
-
-
-

Apply Pull Request

-
-
-
- - - e.g. Simply enter 3456 for PR #3456 -
-
-
Run after apply pull request
-
- -
-
-
-
- -
-
- - -
-
-
-
-
-
-
- - -
-
-
-

Remove Pull Request

-
-
-
- - - e.g. Simply enter 3456 for PR #3456 -
-
-
Run after remove pull request
-
- -
-
-
-
- -
- - -
-
-
- -
-
-
-
*This app does not yet take into account any pull requests that require database changes.
- - -
- - - - - - - - - Date: Mon, 9 Apr 2018 16:04:09 +0200 Subject: [PATCH 306/778] Init commit --- plugins/MauticFocusBundle/Config/config.php | 1 + .../MauticFocusBundle/Model/FocusModel.php | 20 ++++++++++++++----- .../Views/Builder/form.html.php | 10 ++++++---- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/plugins/MauticFocusBundle/Config/config.php b/plugins/MauticFocusBundle/Config/config.php index be561bfdb20..e69b9dca1ba 100644 --- a/plugins/MauticFocusBundle/Config/config.php +++ b/plugins/MauticFocusBundle/Config/config.php @@ -143,6 +143,7 @@ 'mautic.helper.templating', 'event_dispatcher', 'mautic.lead.model.lead', + 'mautic.lead.model.field', ], ], ], diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 17b823f20b0..5d4f28a9e10 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -17,6 +17,7 @@ use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\TemplatingHelper; use Mautic\CoreBundle\Model\FormModel; +use Mautic\LeadBundle\Model\FieldModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Model\TrackableModel; use MauticPlugin\MauticFocusBundle\Entity\Focus; @@ -56,6 +57,11 @@ class FocusModel extends FormModel */ protected $leadModel; + /** + * @var FieldModel + */ + protected $leadFieldModel; + /** * FocusModel constructor. * @@ -64,14 +70,16 @@ class FocusModel extends FormModel * @param TemplatingHelper $templating * @param EventDispatcherInterface $dispatcher * @param LeadModel $leadModel + * @param FieldModel $leadFieldModel */ - public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel) + public function __construct(\Mautic\FormBundle\Model\FormModel $formModel, TrackableModel $trackableModel, TemplatingHelper $templating, EventDispatcherInterface $dispatcher, LeadModel $leadModel, FieldModel $leadFieldModel) { $this->formModel = $formModel; $this->trackableModel = $trackableModel; $this->templating = $templating; $this->dispatcher = $dispatcher; $this->leadModel = $leadModel; + $this->leadFieldModel = $leadFieldModel; } /** @@ -266,10 +274,12 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, + 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), + 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), ] ) : ''; diff --git a/plugins/MauticFocusBundle/Views/Builder/form.html.php b/plugins/MauticFocusBundle/Views/Builder/form.html.php index 327ad27bb6b..4c19821d99a 100644 --- a/plugins/MauticFocusBundle/Views/Builder/form.html.php +++ b/plugins/MauticFocusBundle/Views/Builder/form.html.php @@ -118,10 +118,12 @@ EXTRA; echo $view->render('MauticFormBundle:Builder:form.html.php', [ - 'form' => $form, - 'formExtra' => $formExtra, - 'action' => ($preview) ? '#' : null, - 'suffix' => '_focus', + 'form' => $form, + 'formExtra' => $formExtra, + 'action' => ($preview) ? '#' : null, + 'suffix' => '_focus', + 'contactFields' => $contactFields, + 'companyFields' => $companyFields, ] ); ?> From 1d6533a2ebfe4cb217363442fa2818a268efd004 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 16:07:10 +0200 Subject: [PATCH 307/778] Fic CS --- plugins/MauticFocusBundle/Model/FocusModel.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/MauticFocusBundle/Model/FocusModel.php b/plugins/MauticFocusBundle/Model/FocusModel.php index 5d4f28a9e10..2b5253018c5 100644 --- a/plugins/MauticFocusBundle/Model/FocusModel.php +++ b/plugins/MauticFocusBundle/Model/FocusModel.php @@ -274,12 +274,12 @@ public function getContent(array $focus, $isPreview = false, $url = '#') $formContent = (!empty($form)) ? $this->templating->getTemplating()->render( 'MauticFocusBundle:Builder:form.html.php', [ - 'form' => $form, - 'style' => $focus['style'], - 'focusId' => $focus['id'], - 'preview' => $isPreview, - 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), - 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), + 'form' => $form, + 'style' => $focus['style'], + 'focusId' => $focus['id'], + 'preview' => $isPreview, + 'contactFields' => $this->leadFieldModel->getFieldListWithProperties(), + 'companyFields' => $this->leadFieldModel->getFieldListWithProperties('company'), ] ) : ''; From 6ebaf7946dc341cd8ca89c6f92abb6e76f21bf50 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Sat, 7 Apr 2018 16:07:20 +0200 Subject: [PATCH 308/778] Update company fields if company exist after form submit --- app/bundles/FormBundle/Model/SubmissionModel.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/bundles/FormBundle/Model/SubmissionModel.php b/app/bundles/FormBundle/Model/SubmissionModel.php index f5b6ca48701..289edc79d93 100644 --- a/app/bundles/FormBundle/Model/SubmissionModel.php +++ b/app/bundles/FormBundle/Model/SubmissionModel.php @@ -1035,6 +1035,9 @@ protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $le list($company, $leadAdded, $companyEntity) = IdentifyCompanyHelper::identifyLeadsCompany($companyFieldMatches, $lead, $this->companyModel); if ($leadAdded) { $lead->addCompanyChangeLogEntry('form', 'Identify Company', 'Lead added to the company, '.$company['companyname'], $company['id']); + } elseif ($companyEntity instanceof Company) { + $this->companyModel->setFieldValues($companyEntity, $companyFieldMatches); + $this->companyModel->saveEntity($companyEntity); } if (!empty($company) and $companyEntity instanceof Company) { From dd16595a53438832e8b0dc12caffe4087b538f35 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Mon, 9 Apr 2018 17:15:49 +0200 Subject: [PATCH 309/778] Add company to check all company fields --- app/bundles/FormBundle/Model/SubmissionModel.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/FormBundle/Model/SubmissionModel.php b/app/bundles/FormBundle/Model/SubmissionModel.php index 289edc79d93..d4f2d94f77f 100644 --- a/app/bundles/FormBundle/Model/SubmissionModel.php +++ b/app/bundles/FormBundle/Model/SubmissionModel.php @@ -885,6 +885,8 @@ protected function createLeadFromSubmit(Form $form, array $leadFieldMatches, $le // Closure to get data and unique fields $getCompanyData = function ($currentFields) use ($companyFields) { $companyData = []; + // force add company contact field to company fields check + $companyFields = array_merge($companyFields, ['company'=> 'company']); foreach ($companyFields as $alias => $properties) { if (isset($currentFields[$alias])) { $value = $currentFields[$alias]; From 44c67e5e108b314a1ea7ade9efac26da9191eb36 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 15:28:08 -0500 Subject: [PATCH 310/778] Prevent `.1` from appending to theme filepath --- app/bundles/CoreBundle/Helper/ThemeHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/CoreBundle/Helper/ThemeHelper.php b/app/bundles/CoreBundle/Helper/ThemeHelper.php index d6e7b8d8fee..ce6d1d365fb 100644 --- a/app/bundles/CoreBundle/Helper/ThemeHelper.php +++ b/app/bundles/CoreBundle/Helper/ThemeHelper.php @@ -137,7 +137,7 @@ public function createThemeHelper($themeName) */ private function getDirectoryName($newName) { - return InputHelper::filename($newName, true); + return InputHelper::filename($newName); } /** From 89a349da0bafe6a9607e1473371b26a9e2a68524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=20=E2=98=95?= Date: Tue, 1 May 2018 08:26:46 -0400 Subject: [PATCH 311/778] [Enhancement] Allow filtering contacts by UTM data for segments. (#5886) * Filter a segment by UTM data. * PHPCS fixes. * Reverse CS fixes for LeadListRepository --- .../LeadBundle/Entity/LeadListRepository.php | 27 ++++++++++ app/bundles/LeadBundle/Model/ListModel.php | 52 ++++++++++++++++--- .../Translations/en_US/messages.ini | 5 ++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 8cdfbae108c..06e349927d2 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -618,6 +618,7 @@ protected function generateSegmentExpression(array $filters, array &$parameters, } else { continue; } + // no break case is_bool($v): $paramType = 'boolean'; break; @@ -1617,6 +1618,32 @@ public function getListFilterExpr($filters, &$parameters, QueryBuilder $q, $isNo $groupExpr->add(sprintf('%s (%s)', $operand, $subQb->getSQL())); + break; + case 'utm_campaign': + case 'utm_content': + case 'utm_medium': + case 'utm_source': + case 'utm_term': + // Special handling of lead lists and utmtags + $func = in_array($func, ['eq', 'in']) ? 'EXISTS' : 'NOT EXISTS'; + + $ignoreAutoFilter = true; + + $table = 'lead_utmtags'; + $column = $details['field']; + + $subQb = $this->createFilterExpressionSubQuery( + $table, + $alias, + $column, + $details['filter'], + $parameters, + $leadId + ); + + $groupExpr->add( + sprintf('%s (%s)', $func, $subQb->getSQL()) + ); break; default: if (!$column) { diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 5647a3a9f4d..e728f7a5849 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -180,7 +180,7 @@ public function createForm($entity, $formFactory, $action = null, $options = []) */ public function getEntity($id = null) { - if ($id === null) { + if (null === $id) { return new LeadList(); } @@ -668,6 +668,46 @@ public function getChoiceFields() 'operators' => $this->getOperatorsForFieldType('multiselect'), 'object' => 'lead', ], + 'utm_campaign' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmcampaign'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_content' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmcontent'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_medium' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmmedium'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_source' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmsource'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], + 'utm_term' => [ + 'label' => $this->translator->trans('mautic.lead.list.filter.utmterm'), + 'properties' => [ + 'type' => 'text', + ], + 'operators' => $this->getOperatorsForFieldType('default'), + 'object' => 'lead', + ], ]; // Add custom choices @@ -702,7 +742,7 @@ public function getChoiceFields() $properties = $field->getProperties(); $properties['type'] = $type; if (in_array($type, ['lookup', 'multiselect', 'boolean'])) { - if ($type == 'boolean') { + if ('boolean' == $type) { //create a lookup list with ID $properties['list'] = [ 0 => $properties['no'], @@ -1010,7 +1050,7 @@ public function rebuildListLeads(LeadList $entity, $limit = 1000, $maxLeads = fa */ public function addLead($lead, $lists, $manuallyAdded = false, $batchProcess = false, $searchListLead = 1, $dateManipulated = null) { - if ($dateManipulated == null) { + if (null == $dateManipulated) { $dateManipulated = new \DateTime(); } @@ -1087,7 +1127,7 @@ public function addLead($lead, $lists, $manuallyAdded = false, $batchProcess = f ); } - if ($listLead != null) { + if (null != $listLead) { if ($manuallyAdded && $listLead->wasManuallyRemoved()) { $listLead->setManuallyRemoved(false); $listLead->setManuallyAdded($manuallyAdded); @@ -1213,7 +1253,7 @@ public function removeLead($lead, $lists, $manuallyRemoved = false, $batchProces 'list' => $listId, ]); - if ($listLead == null) { + if (null == $listLead) { // Lead is not part of this list continue; } @@ -1278,7 +1318,7 @@ public function getLeadsByList($lists, $idOnly = false, $args = []) protected function batchSleep() { $leadSleepTime = $this->coreParametersHelper->getParameter('batch_lead_sleep_time', false); - if ($leadSleepTime === false) { + if (false === $leadSleepTime) { $leadSleepTime = $this->coreParametersHelper->getParameter('batch_sleep_time', 1); } diff --git a/app/bundles/LeadBundle/Translations/en_US/messages.ini b/app/bundles/LeadBundle/Translations/en_US/messages.ini index 4fa31050e67..0b3eb2ed9c0 100755 --- a/app/bundles/LeadBundle/Translations/en_US/messages.ini +++ b/app/bundles/LeadBundle/Translations/en_US/messages.ini @@ -653,6 +653,11 @@ mautic.lead.lead.events.changecompanyscore="Add to company's score" mautic.lead.lead.events.changecompanyscore_descr="This action will add the specified value to the company's existing score" mautic.lead.timeline.displaying_events_for_contact="for contact: %contact% (%id%)" mautic.lead.list.filter.categories="Subscribed Categories" +mautic.lead.list.filter.utmcampaign="UTM Campaign" +mautic.lead.list.filter.utmcontent="UTM Content" +mautic.lead.list.filter.utmmedium="UTM Medium" +mautic.lead.list.filter.utmsource="UTM Source" +mautic.lead.list.filter.utmterm="UTM Term" mautic.lead.audit.created="The contact was created." mautic.lead.audit.deleted="The contact was deleted." mautic.lead.audit.updated="The contact was updated." From a7ed71154e798c339bb8ad0b07e999b5535cda47 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 1 May 2018 10:44:00 -0500 Subject: [PATCH 312/778] Fixed tests for Travis --- .../DashboardBundle/Tests/Controller/DashboardControllerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php index 02f4d2e0b0c..dae5441513d 100644 --- a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php +++ b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php @@ -35,6 +35,7 @@ class DashboardControllerTest extends \PHPUnit_Framework_TestCase private $sessionMock; private $flashBagMock; private $containerMock; + private $controller; protected function setUp() { From e3276bc9d710fdbf136973f0b4b1c7f02c08da0e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 1 May 2018 11:03:29 -0500 Subject: [PATCH 313/778] Fixed tests for Travis --- .../Tests/Controller/DashboardControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php index dae5441513d..54f9b458fef 100644 --- a/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php +++ b/app/bundles/DashboardBundle/Tests/Controller/DashboardControllerTest.php @@ -137,7 +137,7 @@ public function testSaveWithPostAjaxWillSave() ->with('session') ->willReturn($this->sessionMock); - $this->routerMock->expects($this->any(0)) + $this->routerMock->expects($this->any()) ->method('generate') ->willReturn('https://some.url'); From d91e466717ede6724f0032be171eb30a621fd414 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 7 May 2018 20:20:37 -0500 Subject: [PATCH 314/778] Add headers array property to the email entity --- app/bundles/EmailBundle/Entity/Email.php | 28 ++++++++++++++++ app/migrations/Version20180507150753.php | 42 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 app/migrations/Version20180507150753.php diff --git a/app/bundles/EmailBundle/Entity/Email.php b/app/bundles/EmailBundle/Entity/Email.php index d97ae13b763..64ce1d11e54 100644 --- a/app/bundles/EmailBundle/Entity/Email.php +++ b/app/bundles/EmailBundle/Entity/Email.php @@ -185,6 +185,11 @@ class Email extends FormEntity implements VariantEntityInterface, TranslationEnt */ private $sessionId; + /** + * @var array + */ + private $headers = []; + public function __clone() { $this->id = null; @@ -341,6 +346,8 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) ->addJoinColumn('email_id', 'id', false, false, 'CASCADE') ->fetchExtraLazy() ->build(); + + $builder->addField('headers', 'json_array'); } /** @@ -485,6 +492,7 @@ public static function loadApiMetadata(ApiMetadataDriver $metadata) 'unsubscribeForm', 'dynamicContent', 'lists', + 'headers', ] ) ->build(); @@ -1096,6 +1104,26 @@ public function getAssetAttachments() return $this->assetAttachments; } + /** + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * @param array $headers + * + * @return Email + */ + public function setHeaders($headers) + { + $this->headers = $headers; + + return $this; + } + /** * Lifecycle callback to clean URLs in the content. */ diff --git a/app/migrations/Version20180507150753.php b/app/migrations/Version20180507150753.php new file mode 100644 index 00000000000..b12535c6ab5 --- /dev/null +++ b/app/migrations/Version20180507150753.php @@ -0,0 +1,42 @@ +getTable($this->prefix.'emails')->hasColumn('headers')) { + throw new SkipMigrationException('Schema includes this migration'); + } + } + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->addSql("ALTER TABLE {$this->prefix}emails ADD headers LONGTEXT NOT NULL COMMENT '(DC2Type:json_array)'"); + } +} From c855b4d1bab419fa3a3fd07cf6270ccdb519fc7e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 7 May 2018 20:22:20 -0500 Subject: [PATCH 315/778] Update SortableListType to support storing as name => value pairs instead of nested list that is keyed by value --- .../SortableListTransformer.php | 68 ++++++++++++++++++- .../CoreBundle/Form/Type/SortableListType.php | 4 +- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/app/bundles/CoreBundle/Form/DataTransformer/SortableListTransformer.php b/app/bundles/CoreBundle/Form/DataTransformer/SortableListTransformer.php index 64ef6dfe36c..28e686a33ce 100644 --- a/app/bundles/CoreBundle/Form/DataTransformer/SortableListTransformer.php +++ b/app/bundles/CoreBundle/Form/DataTransformer/SortableListTransformer.php @@ -29,15 +29,23 @@ class SortableListTransformer implements DataTransformerInterface */ private $withLabels = true; + /** + * @var bool + */ + private $useKeyValuePairs = false; + /** * SortableListTransformer constructor. * * @param bool $removeEmpty + * @param bool $withLabels + * @param bool $atRootLevel */ - public function __construct($removeEmpty = true, $withLabels = true) + public function __construct($removeEmpty = true, $withLabels = true, $useKeyValuePairs = false) { - $this->removeEmpty = $removeEmpty; - $this->withLabels = $withLabels; + $this->removeEmpty = $removeEmpty; + $this->withLabels = $withLabels; + $this->useKeyValuePairs = $useKeyValuePairs; } /** @@ -47,6 +55,10 @@ public function __construct($removeEmpty = true, $withLabels = true) */ public function transform($array) { + if ($this->useKeyValuePairs) { + return $this->transformKeyValuePair($array); + } + return $this->formatList($array); } @@ -57,6 +69,10 @@ public function transform($array) */ public function reverseTransform($array) { + if ($this->useKeyValuePairs) { + return $this->reverseTransformKeyValuePair($array); + } + return $this->formatList($array); } @@ -82,4 +98,50 @@ private function formatList($array) return $array; } + + /** + * @param $array + * + * @return array + */ + private function transformKeyValuePair($array) + { + if ($array === null) { + return ['list' => []]; + } + + $formattedArray = []; + + foreach ($array as $label => $value) { + $formattedArray[] = [ + 'label' => $label, + 'value' => $value, + ]; + } + + return ['list' => $formattedArray]; + } + + /** + * @param $array + * + * @return array + */ + private function reverseTransformKeyValuePair($array) + { + if ($array === null || !isset($array['list'])) { + return []; + } + + $pairs = []; + foreach ($array['list'] as $pair) { + if (!isset($pair['label'])) { + continue; + } + + $pairs[$pair['label']] = $pair['value']; + } + + return $pairs; + } } diff --git a/app/bundles/CoreBundle/Form/Type/SortableListType.php b/app/bundles/CoreBundle/Form/Type/SortableListType.php index 5a67e5453aa..e24fc009e80 100644 --- a/app/bundles/CoreBundle/Form/Type/SortableListType.php +++ b/app/bundles/CoreBundle/Form/Type/SortableListType.php @@ -76,7 +76,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'error_bubbling' => false, ] ) - )->addModelTransformer(new SortableListTransformer($options['option_notblank'], $options['with_labels'])); + )->addModelTransformer(new SortableListTransformer($options['option_notblank'], $options['with_labels'], $options['key_value_pairs'])); } /** @@ -104,6 +104,8 @@ public function configureOptions(OptionsResolver $resolver) 'with_labels' => false, 'entry_type' => 'text', 'add_value_button' => 'mautic.core.form.list.additem', + // Stores as [label => value] array instead of [list => [[label => the label, value => the value], ...]] + 'key_value_pairs' => false, ] ); From afd3ec814a7d125b3ce2817958e68d2634583a16 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 7 May 2018 20:23:07 -0500 Subject: [PATCH 316/778] Added UI support for headers in the email advanced tab --- .../EmailBundle/Form/Type/EmailType.php | 16 +++++++++ .../Translations/en_US/messages.ini | 2 ++ .../EmailBundle/Views/Email/form.html.php | 33 +++++++------------ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/bundles/EmailBundle/Form/Type/EmailType.php b/app/bundles/EmailBundle/Form/Type/EmailType.php index 70fe5976c9c..97668415939 100644 --- a/app/bundles/EmailBundle/Form/Type/EmailType.php +++ b/app/bundles/EmailBundle/Form/Type/EmailType.php @@ -17,6 +17,7 @@ use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber; use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber; use Mautic\CoreBundle\Form\Type\DynamicContentTrait; +use Mautic\CoreBundle\Form\Type\SortableListType; use Mautic\LeadBundle\Helper\FormFieldHelper; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -172,6 +173,21 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); + $builder->add( + 'headers', + SortableListType::class, + [ + 'required' => false, + 'label' => 'mautic.email.custom_headers', + 'attr' => [ + 'tooltip' => 'mautic.email.custom_headers.tooltip', + ], + 'option_required' => false, + 'with_labels' => true, + 'key_value_pairs' => true, // do not store under a `list` key and use label as the key + ] + ); + $builder->add( 'template', 'theme_list', diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index ab93166edd8..2013ff58267 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -179,6 +179,8 @@ mautic.email.config.webview_text.tooltip="Set the default text for the {webview_ mautic.email.config.webview_text="Text for the {webview_text} token" mautic.email.config.mailer.mailjet.sandbox="Sandbox mode" mautic.email.config.mailer.mailjet.sandbox.mail="Default mail for Sandbox mode" +mautic.email.custom_headers="Custom headers" +mautic.email.custom_headers.tooltip="Define custom headers to set for this email when sent." mautic.email.dashboard.widgets="Email Widgets" mautic.email.default.signature="Best regards, %from_name%" mautic.email.dnc.failed="Too many failures when attempting to deliver the email with a subject of '%subject%'." diff --git a/app/bundles/EmailBundle/Views/Email/form.html.php b/app/bundles/EmailBundle/Views/Email/form.html.php index bb506d879a0..334949f0051 100644 --- a/app/bundles/EmailBundle/Views/Email/form.html.php +++ b/app/bundles/EmailBundle/Views/Email/form.html.php @@ -110,32 +110,23 @@
row($form['fromName']); ?> -
-
row($form['fromAddress']); ?> -
-
- -
-
row($form['replyToAddress']); ?> -
- -
row($form['bccAddress']); ?> -
-
- -
-
-
+
+
label($form['assetAttachments']); ?> +
+
+ +
+
+ widget($form['assetAttachments']); ?>
-
- -
-
- widget($form['assetAttachments']); ?> + +
+
+ row($form['headers']); ?>
From 0e9c679fd87b9dcad438785a945e04da9b50c6a5 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 7 May 2018 20:24:03 -0500 Subject: [PATCH 317/778] Consume custom headers when setting an Email entity and don't let passthrough headers overwrite them --- app/bundles/EmailBundle/Helper/MailHelper.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 3b85141169c..03001727161 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -1438,6 +1438,13 @@ public function setEmail(Email $email, $allowBcc = true, $slots = [], $assetAtta } } + // Set custom headers + if ($headers = $email->getHeaders()) { + foreach ($headers as $name => $value) { + $this->addCustomHeader($name, $value); + } + } + return empty($this->errors); } @@ -1445,9 +1452,16 @@ public function setEmail(Email $email, $allowBcc = true, $slots = [], $assetAtta * Set custom headers. * * @param array $headers + * @param bool $merge */ - public function setCustomHeaders(array $headers) + public function setCustomHeaders(array $headers, $merge = true) { + if ($merge) { + $this->headers = array_merge($this->headers, $headers); + + return; + } + $this->headers = $headers; } From 40019d765d79ff6f2a567eca5633d3b197ae06e1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 17:37:01 -0500 Subject: [PATCH 318/778] Fix custom headers for Sparkpost --- .../Swiftmailer/Transport/SparkpostTransport.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php b/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php index 091d533b968..010aa108ff4 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php +++ b/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php @@ -172,6 +172,14 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) $message = $this->messageToArray($mauticTokens, $mergeVarPlaceholders, true); + // These are reserved headers + unset( + $message['headers']['Subject'], + $message['headers']['MIME-Version'], + $message['headers']['Content-Type'], + $message['headers']['Content-Transfer-Encoding'] + ); + // Sparkpost requires a subject if (empty($message['subject'])) { throw new \Exception($this->translator->trans('mautic.email.subject.notblank', [], 'validators')); @@ -246,6 +254,7 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) 'from' => (!empty($message['from']['name'])) ? $message['from']['name'].' <'.$message['from']['email'].'>' : $message['from']['email'], 'subject' => $message['subject'], + 'headers' => $message['headers'], ]; // Sparkpost will set parts regardless if they are empty or not @@ -276,7 +285,6 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) $sparkPostMessage = [ 'content' => $content, 'recipients' => $recipients, - 'headers' => $message['headers'], 'inline_css' => $inlineCss, 'tags' => $tags, ]; From b5b7fcd014bbc1155bedb1d2ca656e7ede7ba31b Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 17:56:32 -0500 Subject: [PATCH 319/778] Check headers for contact tokens --- app/bundles/EmailBundle/Event/EmailSendEvent.php | 2 +- app/bundles/LeadBundle/EventListener/EmailSubscriber.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Event/EmailSendEvent.php b/app/bundles/EmailBundle/Event/EmailSendEvent.php index 6f9335744bf..f2848b46900 100644 --- a/app/bundles/EmailBundle/Event/EmailSendEvent.php +++ b/app/bundles/EmailBundle/Event/EmailSendEvent.php @@ -330,7 +330,7 @@ public function addTextHeader($name, $value) */ public function getTextHeaders() { - return ($this->helper !== null) ? $this->helper->getCustomHeaders() : $this->headers; + return ($this->helper !== null) ? $this->helper->getCustomHeaders() : $this->textHeaders; } /** diff --git a/app/bundles/LeadBundle/EventListener/EmailSubscriber.php b/app/bundles/LeadBundle/EventListener/EmailSubscriber.php index e0ca5d9d3cf..634f93167ac 100644 --- a/app/bundles/LeadBundle/EventListener/EmailSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/EmailSubscriber.php @@ -78,6 +78,8 @@ public function onEmailGenerate(EmailSendEvent $event) $content = $event->getSubject(); $content .= $event->getContent(); $content .= $event->getPlainText(); + $content .= implode(' ', $event->getTextHeaders()); + $lead = $event->getLead(); $tokenList = TokenHelper::findLeadTokens($content, $lead); From 954df41ca5cbd611331faba32acd8b8c97d21ae0 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 14:24:51 -0500 Subject: [PATCH 320/778] Add support for setting custom mailer headers in the configuration --- app/bundles/EmailBundle/Config/config.php | 1 + .../EmailBundle/Form/Type/ConfigType.php | 201 ++++++++++-------- app/bundles/EmailBundle/Helper/MailHelper.php | 26 +++ .../Translations/en_US/messages.ini | 2 + .../_config_emailconfig_widget.html.php | 9 +- 5 files changed, 153 insertions(+), 86 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 8563a5540bb..3096caa69b8 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -654,6 +654,7 @@ 'mailer_encryption' => null, //tls or ssl, 'mailer_auth_mode' => null, //plain, login or cram-md5 'mailer_amazon_region' => 'email-smtp.us-east-1.amazonaws.com', + 'mailer_custom_headers' => [], 'mailer_spool_type' => 'memory', //memory = immediate; file = queue 'mailer_spool_path' => '%kernel.root_dir%/spool', 'mailer_spool_msg_limit' => null, diff --git a/app/bundles/EmailBundle/Form/Type/ConfigType.php b/app/bundles/EmailBundle/Form/Type/ConfigType.php index 51da47843c2..d5bfceb0f96 100644 --- a/app/bundles/EmailBundle/Form/Type/ConfigType.php +++ b/app/bundles/EmailBundle/Form/Type/ConfigType.php @@ -11,6 +11,7 @@ namespace Mautic\EmailBundle\Form\Type; +use Mautic\CoreBundle\Form\Type\SortableListType; use Mautic\EmailBundle\Model\TransportType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -62,8 +63,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.unsubscribe_text.tooltip', ], - 'required' => false, - 'data' => (array_key_exists('unsubscribe_text', $options['data']) && !empty($options['data']['unsubscribe_text'])) + 'required' => false, + 'data' => (array_key_exists('unsubscribe_text', $options['data']) && !empty($options['data']['unsubscribe_text'])) ? $options['data']['unsubscribe_text'] : $this->translator->trans( 'mautic.email.unsubscribe.text', @@ -82,8 +83,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.webview_text.tooltip', ], - 'required' => false, - 'data' => (array_key_exists('webview_text', $options['data']) && !empty($options['data']['webview_text'])) + 'required' => false, + 'data' => (array_key_exists('webview_text', $options['data']) && !empty($options['data']['webview_text'])) ? $options['data']['webview_text'] : $this->translator->trans( 'mautic.email.webview.text', @@ -102,8 +103,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.unsubscribe_message.tooltip', ], - 'required' => false, - 'data' => (array_key_exists('unsubscribe_message', $options['data']) && !empty($options['data']['unsubscribe_message'])) + 'required' => false, + 'data' => (array_key_exists('unsubscribe_message', $options['data']) && !empty($options['data']['unsubscribe_message'])) ? $options['data']['unsubscribe_message'] : $this->translator->trans( 'mautic.email.unsubscribed.success', @@ -125,8 +126,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.resubscribe_message.tooltip', ], - 'required' => false, - 'data' => (array_key_exists('resubscribe_message', $options['data']) && !empty($options['data']['resubscribe_message'])) + 'required' => false, + 'data' => (array_key_exists('resubscribe_message', $options['data']) && !empty($options['data']['resubscribe_message'])) ? $options['data']['resubscribe_message'] : $this->translator->trans( 'mautic.email.resubscribed.success', @@ -148,8 +149,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.default_signature_text.tooltip', ], - 'required' => false, - 'data' => (!empty($options['data']['default_signature_text'])) + 'required' => false, + 'data' => (!empty($options['data']['default_signature_text'])) ? $options['data']['default_signature_text'] : $this->translator->trans( 'mautic.email.default.signature', @@ -164,11 +165,12 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_from_name', 'text', [ - 'label' => 'mautic.email.config.mailer.from.name', - 'label_attr' => ['class' => 'control-label'], - 'attr' => [ - 'class' => 'form-control', - 'tooltip' => 'mautic.email.config.mailer.from.name.tooltip', + 'label' => 'mautic.email.config.mailer.from.name', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.email.config.mailer.from.name.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'constraints' => [ new NotBlank( @@ -184,11 +186,12 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_from_email', 'text', [ - 'label' => 'mautic.email.config.mailer.from.email', - 'label_attr' => ['class' => 'control-label'], - 'attr' => [ - 'class' => 'form-control', - 'tooltip' => 'mautic.email.config.mailer.from.email.tooltip', + 'label' => 'mautic.email.config.mailer.from.email', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.email.config.mailer.from.email.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'constraints' => [ new NotBlank( @@ -212,10 +215,11 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label' => 'mautic.email.config.mailer.return.path', 'label_attr' => ['class' => 'control-label'], 'attr' => [ - 'class' => 'form-control', - 'tooltip' => 'mautic.email.config.mailer.return.path.tooltip', + 'class' => 'form-control', + 'tooltip' => 'mautic.email.config.mailer.return.path.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], - 'required' => false, + 'required' => false, ] ); @@ -223,12 +227,13 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_transport', ChoiceType::class, [ - 'choices' => $this->transportType->getTransportTypes(), - 'label' => 'mautic.email.config.mailer.transport', - 'required' => false, - 'attr' => [ - 'class' => 'form-control', - 'tooltip' => 'mautic.email.config.mailer.transport.tooltip', + 'choices' => $this->transportType->getTransportTypes(), + 'label' => 'mautic.email.config.mailer.transport', + 'required' => false, + 'attr' => [ + 'class' => 'form-control', + 'tooltip' => 'mautic.email.config.mailer.transport.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'empty_value' => false, ] @@ -244,8 +249,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.convert.embed.images.tooltip', ], - 'data' => empty($options['data']['mailer_convert_embed_images']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['mailer_convert_embed_images']) ? false : true, + 'required' => false, ] ); @@ -259,8 +264,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.append.tracking.pixel.tooltip', ], - 'data' => empty($options['data']['mailer_append_tracking_pixel']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['mailer_append_tracking_pixel']) ? false : true, + 'required' => false, ] ); @@ -274,8 +279,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.disable.trackable.urls.tooltip', ], - 'data' => empty($options['data']['disable_trackable_urls']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['disable_trackable_urls']) ? false : true, + 'required' => false, ] ); @@ -292,8 +297,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'data-show-on' => $smtpServiceShowConditions, 'tooltip' => 'mautic.email.config.mailer.host.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], - 'required' => false, + 'required' => false, ] ); @@ -301,17 +307,18 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_amazon_region', 'choice', [ - 'choices' => [ + 'choices' => [ 'email-smtp.eu-west-1.amazonaws.com' => 'mautic.email.config.mailer.amazon_host.eu_west_1', 'email-smtp.us-east-1.amazonaws.com' => 'mautic.email.config.mailer.amazon_host.us_east_1', 'email-smtp.us-west-2.amazonaws.com' => 'mautic.email.config.mailer.amazon_host.eu_west_2', ], - 'label' => 'mautic.email.config.mailer.amazon_host', - 'required' => false, - 'attr' => [ + 'label' => 'mautic.email.config.mailer.amazon_host', + 'required' => false, + 'attr' => [ 'class' => 'form-control', 'data-show-on' => $amazonRegionShowConditions, 'tooltip' => 'mautic.email.config.mailer.amazon_host.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'empty_value' => false, ] @@ -327,8 +334,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'data-show-on' => $smtpServiceShowConditions, 'tooltip' => 'mautic.email.config.mailer.port.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], - 'required' => false, + 'required' => false, ] ); @@ -336,18 +344,19 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_auth_mode', 'choice', [ - 'choices' => [ + 'choices' => [ 'plain' => 'mautic.email.config.mailer_auth_mode.plain', 'login' => 'mautic.email.config.mailer_auth_mode.login', 'cram-md5' => 'mautic.email.config.mailer_auth_mode.cram-md5', ], - 'label' => 'mautic.email.config.mailer.auth.mode', - 'label_attr' => ['class' => 'control-label'], - 'required' => false, - 'attr' => [ + 'label' => 'mautic.email.config.mailer.auth.mode', + 'label_attr' => ['class' => 'control-label'], + 'required' => false, + 'attr' => [ 'class' => 'form-control', 'data-show-on' => $smtpServiceShowConditions, 'tooltip' => 'mautic.email.config.mailer.auth.mode.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'empty_value' => 'mautic.email.config.mailer_auth_mode.none', ] @@ -388,9 +397,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-show-on' => $mailerLoginUserShowConditions, 'data-hide-on' => $mailerLoginUserHideConditions, 'tooltip' => 'mautic.email.config.mailer.user.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', 'autocomplete' => 'off', ], - 'required' => false, + 'required' => false, ] ); @@ -408,8 +418,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-hide-on' => $mailerLoginPasswordHideConditions, 'tooltip' => 'mautic.email.config.mailer.password.tooltip', 'autocomplete' => 'off', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], - 'required' => false, + 'required' => false, ] ); @@ -426,8 +437,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'tooltip' => 'mautic.email.config.mailer.apikey.tooltop', 'autocomplete' => 'off', 'placeholder' => 'mautic.email.config.mailer.apikey.placeholder', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], - 'required' => false, + 'required' => false, ] ); @@ -435,16 +447,17 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_encryption', 'choice', [ - 'choices' => [ + 'choices' => [ 'ssl' => 'mautic.email.config.mailer_encryption.ssl', 'tls' => 'mautic.email.config.mailer_encryption.tls', ], - 'label' => 'mautic.email.config.mailer.encryption', - 'required' => false, - 'attr' => [ + 'label' => 'mautic.email.config.mailer.encryption', + 'required' => false, + 'attr' => [ 'class' => 'form-control', 'data-show-on' => $smtpServiceShowConditions, 'tooltip' => 'mautic.email.config.mailer.encryption.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'empty_value' => 'mautic.email.config.mailer_encryption.none', ] @@ -471,7 +484,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, 'attr' => [ 'class' => 'btn btn-info', - 'onclick' => 'Mautic.testEmailServerConnection(true)', + 'onclick' => 'Mautic.sendTestEmail()', ], ] ); @@ -486,9 +499,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.mailjet.sandbox', 'data-show-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getMailjetService().']}', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], - 'data' => empty($options['data']['mailer_mailjet_sandbox']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['mailer_mailjet_sandbox']) ? false : true, + 'required' => false, ] ); @@ -496,13 +510,14 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_mailjet_sandbox_default_mail', 'text', [ - 'label' => 'mautic.email.config.mailer.mailjet.sandbox.mail', - 'label_attr' => ['class' => 'control-label'], - 'attr' => [ + 'label' => 'mautic.email.config.mailer.mailjet.sandbox.mail', + 'label_attr' => ['class' => 'control-label'], + 'attr' => [ 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.mailjet.sandbox.mail', 'data-show-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getMailjetService().']}', 'data-hide-on' => '{"config_emailconfig_mailer_mailjet_sandbox_0":"checked"}', + 'onchange' => 'Mautic.disableSendTestEmailButton()', ], 'constraints' => [ new Email( @@ -511,7 +526,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ), ], - 'required' => false, + 'required' => false, ] ); @@ -521,14 +536,14 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_spool_type', 'choice', [ - 'choices' => [ + 'choices' => [ 'memory' => 'mautic.email.config.mailer_spool_type.memory', 'file' => 'mautic.email.config.mailer_spool_type.file', ], - 'label' => 'mautic.email.config.mailer.spool.type', - 'label_attr' => ['class' => 'control-label'], - 'required' => false, - 'attr' => [ + 'label' => 'mautic.email.config.mailer.spool.type', + 'label_attr' => ['class' => 'control-label'], + 'required' => false, + 'attr' => [ 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.spool.type.tooltip', ], @@ -536,6 +551,22 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); + $builder->add( + 'mailer_custom_headers', + SortableListType::class, + [ + 'required' => false, + 'label' => 'mautic.email.custom_headers', + 'attr' => [ + 'tooltip' => 'mautic.email.custom_headers.config.tooltip', + 'onchange' => 'Mautic.disableSendTestEmailButton()', + ], + 'option_required' => false, + 'with_labels' => true, + 'key_value_pairs' => true, // do not store under a `list` key and use label as the key + ] + ); + $builder->add( 'mailer_spool_path', 'text', @@ -547,7 +578,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-hide-on' => $spoolConditions, 'tooltip' => 'mautic.email.config.mailer.spool.path.tooltip', ], - 'required' => false, + 'required' => false, ] ); @@ -562,7 +593,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-hide-on' => $spoolConditions, 'tooltip' => 'mautic.email.config.mailer.spool.msg.limit.tooltip', ], - 'required' => false, + 'required' => false, ] ); @@ -577,7 +608,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-hide-on' => $spoolConditions, 'tooltip' => 'mautic.email.config.mailer.spool.time.limit.tooltip', ], - 'required' => false, + 'required' => false, ] ); @@ -592,7 +623,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-hide-on' => $spoolConditions, 'tooltip' => 'mautic.email.config.mailer.spool.recover.timeout.tooltip', ], - 'required' => false, + 'required' => false, ] ); @@ -607,7 +638,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'data-hide-on' => $spoolConditions, 'tooltip' => 'mautic.email.config.mailer.spool.clear.timeout.tooltip', ], - 'required' => false, + 'required' => false, ] ); @@ -631,8 +662,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.mailer.is.owner.tooltip', ], - 'data' => empty($options['data']['mailer_is_owner']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['mailer_is_owner']) ? false : true, + 'required' => false, ] ); $builder->add( @@ -652,7 +683,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'email_frequency_time', 'choice', [ - 'choices' => [ + 'choices' => [ 'DAY' => 'day', 'WEEK' => 'week', 'MONTH' => 'month', @@ -676,8 +707,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.show.contact.segments.tooltip', ], - 'data' => empty($options['data']['show_contact_segments']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['show_contact_segments']) ? false : true, + 'required' => false, ] ); $builder->add( @@ -690,8 +721,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.show.preference.options.tooltip', ], - 'data' => empty($options['data']['show_contact_preferences']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['show_contact_preferences']) ? false : true, + 'required' => false, ] ); $builder->add( @@ -704,8 +735,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.show.contact.frequency.tooltip', ], - 'data' => empty($options['data']['show_contact_frequency']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['show_contact_frequency']) ? false : true, + 'required' => false, ] ); $builder->add( @@ -718,8 +749,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.show.contact.pause.dates.tooltip', ], - 'data' => empty($options['data']['show_contact_pause_dates']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['show_contact_pause_dates']) ? false : true, + 'required' => false, ] ); $builder->add( @@ -732,8 +763,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.show.contact.categories.tooltip', ], - 'data' => empty($options['data']['show_contact_categories']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['show_contact_categories']) ? false : true, + 'required' => false, ] ); $builder->add( @@ -746,8 +777,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'tooltip' => 'mautic.email.config.show.contact.preferred.channels', ], - 'data' => empty($options['data']['show_contact_preferred_channels']) ? false : true, - 'required' => false, + 'data' => empty($options['data']['show_contact_preferred_channels']) ? false : true, + 'required' => false, ] ); } diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 03001727161..4e8eba2785f 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -193,6 +193,11 @@ class MailHelper */ protected $headers = []; + /** + * @var array + */ + private $systemHeaders = []; + /** * @var array */ @@ -363,6 +368,8 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne } if (empty($this->fatal)) { + $this->mergeSystemHeaders(); + if (!$isQueueFlush) { // Only add unsubscribe header to one-off sends as tokenized sends are built by the transport $this->addUnsubscribeHeader(); @@ -2071,6 +2078,25 @@ protected function getContactOwnerSignature($owner) ); } + /** + * Merge system headers into local headers if this send is not based on an Email entity. + */ + private function mergeSystemHeaders() + { + if ($this->email) { + // We are purposively ignoring system headers if using an Email entity + return; + } + + if (!$systemHeaders = $this->factory->getParameter('mailer_custom_headers', [])) { + return; + } + + foreach ($systemHeaders as $name => $value) { + $this->addCustomHeader($name, $value); + } + } + /** * Validates a given address to ensure RFC 2822, 3.6.2 specs. * diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index 2013ff58267..fd846f063ad 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -179,7 +179,9 @@ mautic.email.config.webview_text.tooltip="Set the default text for the {webview_ mautic.email.config.webview_text="Text for the {webview_text} token" mautic.email.config.mailer.mailjet.sandbox="Sandbox mode" mautic.email.config.mailer.mailjet.sandbox.mail="Default mail for Sandbox mode" +mautic.email.config.save_to_test="Apply changes to the configuration to send a test email." mautic.email.custom_headers="Custom headers" +mautic.email.custom_headers.config.tooltip="Define custom headers to use for any outgoing email that is not associated with a Mautic Email. This includes password reset emails, emails to Mautic users, form post results, directly composed email to contacts, etc. If custom headers are required for an Email (used in campaigns or broadcasts), set customer headers in the Advanced tab of the Email." mautic.email.custom_headers.tooltip="Define custom headers to set for this email when sent." mautic.email.dashboard.widgets="Email Widgets" mautic.email.default.signature="Best regards, %from_name%" diff --git a/app/bundles/EmailBundle/Views/FormTheme/Config/_config_emailconfig_widget.html.php b/app/bundles/EmailBundle/Views/FormTheme/Config/_config_emailconfig_widget.html.php index 1449225da36..e35a9aea5cd 100644 --- a/app/bundles/EmailBundle/Views/FormTheme/Config/_config_emailconfig_widget.html.php +++ b/app/bundles/EmailBundle/Views/FormTheme/Config/_config_emailconfig_widget.html.php @@ -43,7 +43,10 @@ widget($fields['mailer_test_send_button']); ?>
-
+
+
+
trans('mautic.email.config.save_to_test'); ?>
+
@@ -74,6 +77,10 @@
+
+ rowIfExists($fields, 'mailer_custom_headers', $template); ?> +
+
From 5413977a1b0fcf6c2706eef23776fe9b65e56ff8 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 14:26:28 -0500 Subject: [PATCH 321/778] Disable send test email button until configuration is saved. Then use MailHelper to send the email so that headers and transports that do not use setUser, etc methods are used to deliver the email. --- app/bundles/EmailBundle/Assets/js/config.js | 24 +++++++- .../EmailBundle/Controller/AjaxController.php | 56 ++++++++++++------- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/app/bundles/EmailBundle/Assets/js/config.js b/app/bundles/EmailBundle/Assets/js/config.js index 8e780449960..f233d59f3f0 100644 --- a/app/bundles/EmailBundle/Assets/js/config.js +++ b/app/bundles/EmailBundle/Assets/js/config.js @@ -63,7 +63,7 @@ Mautic.testMonitoredEmailServerConnection = function(mailbox) { }); }; -Mautic.testEmailServerConnection = function(sendEmail) { +Mautic.testEmailServerConnection = function() { var data = { amazon_region: mQuery('#config_emailconfig_mailer_amazon_region').val(), api_key: mQuery('#config_emailconfig_mailer_api_key').val(), @@ -74,7 +74,6 @@ Mautic.testEmailServerConnection = function(sendEmail) { host: mQuery('#config_emailconfig_mailer_host').val(), password: mQuery('#config_emailconfig_mailer_password').val(), port: mQuery('#config_emailconfig_mailer_port').val(), - send_test: (typeof sendEmail !== 'undefined') ? sendEmail : false, transport: mQuery('#config_emailconfig_mailer_transport').val(), user: mQuery('#config_emailconfig_mailer_user').val() }; @@ -85,7 +84,26 @@ Mautic.testEmailServerConnection = function(sendEmail) { var theClass = (response.success) ? 'has-success' : 'has-error'; var theMessage = response.message; mQuery('#mailerTestButtonContainer').removeClass('has-success has-error').addClass(theClass); - mQuery('#mailerTestButtonContainer .help-block').html(theMessage); + mQuery('#mailerTestButtonContainer .help-block .status-msg').html(theMessage); mQuery('#mailerTestButtonContainer .fa-spinner').addClass('hide'); }); }; + +Mautic.sendTestEmail = function() { + mQuery('#mailerTestButtonContainer .fa-spinner').removeClass('hide'); + + Mautic.ajaxActionRequest('email:sendTestEmail', {}, function(response) { + var theClass = (response.success) ? 'has-success' : 'has-error'; + var theMessage = response.message; + mQuery('#mailerTestButtonContainer').removeClass('has-success has-error').addClass(theClass); + mQuery('#mailerTestButtonContainer .help-block .status-msg').html(theMessage); + mQuery('#mailerTestButtonContainer .fa-spinner').addClass('hide'); + }); +}; + +Mautic.disableSendTestEmailButton = function() { + mQuery('#mailerTestButtonContainer .help-block .status-msg').html(''); + mQuery('#mailerTestButtonContainer .help-block .save-config-msg').removeClass('hide'); + mQuery('#config_emailconfig_mailer_test_send_button').prop('disabled', true).addClass('disabled'); + +}; diff --git a/app/bundles/EmailBundle/Controller/AjaxController.php b/app/bundles/EmailBundle/Controller/AjaxController.php index 6592ad4f412..200b8577898 100644 --- a/app/bundles/EmailBundle/Controller/AjaxController.php +++ b/app/bundles/EmailBundle/Controller/AjaxController.php @@ -14,6 +14,8 @@ use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController; use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait; use Mautic\CoreBundle\Controller\VariantAjaxControllerTrait; +use Mautic\CoreBundle\Translation\Translator; +use Mautic\EmailBundle\Helper\MailHelper; use Mautic\EmailBundle\Helper\PlainTextHelper; use Mautic\EmailBundle\Model\EmailModel; use Symfony\Component\HttpFoundation\JsonResponse; @@ -257,26 +259,8 @@ protected function testEmailServerConnectionAction(Request $request) try { $mailer->start(); - $translator = $this->get('translator'); - - if ($settings['send_test'] == 'true') { - $message = new \Swift_Message( - $translator->trans('mautic.email.config.mailer.transport.test_send.subject'), - $translator->trans('mautic.email.config.mailer.transport.test_send.body') - ); - - $userFullName = trim($user->getFirstName().' '.$user->getLastName()); - if (empty($userFullName)) { - $userFullName = null; - } - $message->setFrom([$settings['from_email'] => $settings['from_name']]); - $message->setTo([$user->getEmail() => $userFullName]); - - $mailer->send($message); - } - $dataArray['success'] = 1; - $dataArray['message'] = $translator->trans('mautic.core.success'); + $dataArray['message'] = $this->get('translator')->trans('mautic.core.success'); } catch (\Exception $e) { $dataArray['message'] = $e->getMessage().'
'.$logger->dump(); } @@ -286,6 +270,36 @@ protected function testEmailServerConnectionAction(Request $request) return $this->sendJsonResponse($dataArray); } + /** + * @param Request $request + */ + protected function sendTestEmailAction(Request $request) + { + /** @var MailHelper $mailer */ + $mailer = $this->get('mautic.helper.mailer'); + /** @var Translator $translator */ + $translator = $this->get('translator'); + + $mailer->setSubject($translator->trans('mautic.email.config.mailer.transport.test_send.subject')); + $mailer->setBody($translator->trans('mautic.email.config.mailer.transport.test_send.body')); + + $user = $this->get('mautic.helper.user')->getUser(); + $userFullName = trim($user->getFirstName().' '.$user->getLastName()); + if (empty($userFullName)) { + $userFullName = null; + } + $mailer->setTo([$user->getEmail() => $userFullName]); + + $success = 1; + $message = $translator->trans('mautic.core.success'); + if (!$mailer->send(true)) { + $success = 0; + $message = implode('; ', $mailer->getErrors()); + } + + return $this->sendJsonResponse(['success' => $success, 'message' => $message]); + } + /** * @param Request $request */ @@ -301,8 +315,8 @@ protected function getEmailCountStatsAction(Request $request) $queued = $model->getQueuedCounts($email); $data = [ - 'success' => 1, - 'pending' => 'list' === $email->getEmailType() && $pending ? $this->translator->trans( + 'success' => 1, + 'pending' => 'list' === $email->getEmailType() && $pending ? $this->translator->trans( 'mautic.email.stat.leadcount', ['%count%' => $pending] ) : 0, From 40875b77edb7bcc516b7e8d6cc4679097d73a501 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 14:46:39 -0500 Subject: [PATCH 322/778] Added tests for MailHelper headers --- .../Tests/Helper/MailHelperTest.php | 113 ++++++++++++++++-- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php b/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php index 61d80ff1685..d6d4bbfec00 100644 --- a/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php +++ b/app/bundles/EmailBundle/Tests/Helper/MailHelperTest.php @@ -467,6 +467,99 @@ public function testQueueModeIsReset() ); } + public function testGlobalHeadersAreSet() + { + $parameterMap = [ + ['mailer_custom_headers', [], ['X-Mautic-Test' => 'test', 'X-Mautic-Test2' => 'test']], + ]; + $mockFactory = $this->getMockFactory(true, $parameterMap); + + $transport = new SmtpTransport(); + $swiftMailer = new \Swift_Mailer($transport); + + $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + $mailer->setBody('{signature}'); + $mailer->addTo($this->contacts[0]['email']); + $mailer->send(); + + $customHeadersFounds = []; + + /** @var \Swift_Mime_Headers_ParameterizedHeader[] $headers */ + $headers = $mailer->message->getHeaders()->getAll(); + foreach ($headers as $header) { + if (strpos($header->getFieldName(), 'X-Mautic-Test') !== false) { + $customHeadersFounds[] = $header->getFieldName(); + + $this->assertEquals('test', $header->getValue()); + } + } + + $this->assertCount(2, $customHeadersFounds); + } + + public function testGlobalHeadersAreIgnoredIfEmailEntityIsSet() + { + $parameterMap = [ + ['mailer_custom_headers', [], ['X-Mautic-Test' => 'test', 'X-Mautic-Test2' => 'test']], + ]; + $mockFactory = $this->getMockFactory(true, $parameterMap); + + $transport = new SmtpTransport(); + $swiftMailer = new \Swift_Mailer($transport); + + $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + $mailer->addTo($this->contacts[0]['email']); + + $email = new Email(); + $email->setSubject('Test'); + $email->setCustomHtml('{signature}'); + $mailer->setEmail($email); + $mailer->send(); + + /** @var \Swift_Mime_Headers_ParameterizedHeader[] $headers */ + $headers = $mailer->message->getHeaders()->getAll(); + foreach ($headers as $header) { + if (strpos($header->getFieldName(), 'X-Mautic-Test') !== false) { + $this->fail('System headers were not supposed to be set'); + } + } + } + + public function testEmailHeadersAreSet() + { + $parameterMap = [ + ['mailer_custom_headers', [], ['X-Mautic-Test' => 'test', 'X-Mautic-Test2' => 'test']], + ]; + $mockFactory = $this->getMockFactory(true, $parameterMap); + + $transport = new SmtpTransport(); + $swiftMailer = new \Swift_Mailer($transport); + + $mailer = new MailHelper($mockFactory, $swiftMailer, ['nobody@nowhere.com' => 'No Body']); + $mailer->addTo($this->contacts[0]['email']); + + $email = new Email(); + $email->setSubject('Test'); + $email->setCustomHtml('{signature}'); + $email->setHeaders(['X-Mautic-Test3' => 'test2', 'X-Mautic-Test4' => 'test2']); + $mailer->setEmail($email); + $mailer->send(); + + $customHeadersFounds = []; + + /** @var \Swift_Mime_Headers_ParameterizedHeader[] $headers */ + $headers = $mailer->message->getHeaders()->getAll(); + foreach ($headers as $header) { + if (strpos($header->getFieldName(), 'X-Mautic-Test') !== false) { + $customHeadersFounds[] = $header->getFieldName(); + + $this->assertEquals('test2', $header->getValue()); + } + } + + $this->assertCount(2, $customHeadersFounds); + } + protected function mockEmptyMailHelper($useSmtp = true) { $mockFactory = $this->getMockFactory(); @@ -476,7 +569,7 @@ protected function mockEmptyMailHelper($useSmtp = true) return new MailHelper($mockFactory, $swiftMailer); } - protected function getMockFactory($mailIsOwner = true) + protected function getMockFactory($mailIsOwner = true, $parameterMap = []) { $mockLeadRepository = $this->getMockBuilder(LeadRepository::class) ->disableOriginalConstructor() @@ -502,15 +595,19 @@ protected function getMockFactory($mailIsOwner = true) $mockFactory = $this->getMockBuilder(MauticFactory::class) ->disableOriginalConstructor() ->getMock(); + + $parameterMap = array_merge( + [ + ['mailer_return_path', false, null], + ['mailer_spool_type', false, 'memory'], + ['mailer_is_owner', false, $mailIsOwner], + ], + $parameterMap + ); + $mockFactory->method('getParameter') ->will( - $this->returnValueMap( - [ - ['mailer_return_path', false, null], - ['mailer_spool_type', false, 'memory'], - ['mailer_is_owner', false, $mailIsOwner], - ] - ) + $this->returnValueMap($parameterMap) ); $mockFactory->method('getModel') ->will( From 7c169c148925617a0cd0d30bbb4dc8dfca660767 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 10:43:33 -0500 Subject: [PATCH 323/778] Include system headers in getCustomHeaders for listeners to have available --- app/bundles/EmailBundle/Helper/MailHelper.php | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 4e8eba2785f..afa2cfba1f9 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -368,8 +368,6 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne } if (empty($this->fatal)) { - $this->mergeSystemHeaders(); - if (!$isQueueFlush) { // Only add unsubscribe header to one-off sends as tokenized sends are built by the transport $this->addUnsubscribeHeader(); @@ -439,18 +437,7 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne } } - // Set custom headers - if (!empty($this->headers)) { - $headers = $this->message->getHeaders(); - foreach ($this->headers as $headerKey => $headerValue) { - if ($headers->has($headerKey)) { - $header = $headers->get($headerKey); - $header->setFieldBodyModel($headerValue); - } else { - $headers->addTextHeader($headerKey, $headerValue); - } - } - } + $this->setMessageHeaders(); try { $failures = []; @@ -1486,7 +1473,10 @@ public function addCustomHeader($name, $value) */ public function getCustomHeaders() { - return $this->headers; + $headers = $this->headers; + $systemHeaders = $this->getSystemHeaders(); + + return array_merge($headers, $systemHeaders); } /** @@ -2079,21 +2069,40 @@ protected function getContactOwnerSignature($owner) } /** - * Merge system headers into local headers if this send is not based on an Email entity. + * @return array */ - private function mergeSystemHeaders() + private function getSystemHeaders() { if ($this->email) { // We are purposively ignoring system headers if using an Email entity - return; + return []; } if (!$systemHeaders = $this->factory->getParameter('mailer_custom_headers', [])) { - return; + return []; } - foreach ($systemHeaders as $name => $value) { - $this->addCustomHeader($name, $value); + return $systemHeaders; + } + + /** + * Merge system headers into custom headers if applicable. + */ + private function setMessageHeaders() + { + $headers = $this->getCustomHeaders(); + + // Set custom headers + if (!empty($headers)) { + $messageHeaders = $this->message->getHeaders(); + foreach ($headers as $headerKey => $headerValue) { + if ($messageHeaders->has($headerKey)) { + $header = $messageHeaders->get($headerKey); + $header->setFieldBodyModel($headerValue); + } else { + $messageHeaders->addTextHeader($headerKey, $headerValue); + } + } } } From 24bb3874d2a16da3f041d28e8d98f2c126096358 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 12:24:38 -0500 Subject: [PATCH 324/778] Add sendgrid support for custom headers --- app/bundles/EmailBundle/Config/config.php | 4 ++ .../SendGrid/Mail/SendGridMailHeader.php | 32 ++++++++++++ .../SendGrid/SendGridApiMessage.php | 20 ++++++- .../SendGrid/Mail/SendGridMailHeaderTest.php | 52 +++++++++++++++++++ .../SendGrid/SendGridApiMessageTest.php | 11 +++- 5 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php create mode 100644 app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 3096caa69b8..c46c0f39d50 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -402,6 +402,9 @@ 'mautic.transport.sendgrid_api.mail.attachment' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailAttachment::class, ], + 'mautic.transport.sendgrid_api.mail.header' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailHeader::class, + ], 'mautic.transport.sendgrid_api.message' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\SendGrid\SendGridApiMessage::class, 'arguments' => [ @@ -409,6 +412,7 @@ 'mautic.transport.sendgrid_api.mail.personalization', 'mautic.transport.sendgrid_api.mail.metadata', 'mautic.transport.sendgrid_api.mail.attachment', + 'mautic.transport.sendgrid_api.mail.header', ], ], 'mautic.transport.sendgrid_api.response' => [ diff --git a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php new file mode 100644 index 00000000000..fe248bc9d39 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php @@ -0,0 +1,32 @@ +getHeaders()->getAll(); + /** @var \Swift_Mime_Header $header */ + foreach ($headers as $header) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { + $mail->addHeader($header->getFieldName(), $header->getFieldBodyModel()); + } + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php b/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php index dddd3bac928..5c055a7971f 100644 --- a/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php +++ b/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php @@ -13,6 +13,7 @@ use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailAttachment; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailBase; +use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailHeader; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailMetadata; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailPersonalization; use SendGrid\Mail; @@ -39,16 +40,32 @@ class SendGridApiMessage */ private $sendGridMailAttachment; + /** + * @var SendGridMailHeader + */ + private $sendGridMailHeader; + + /** + * SendGridApiMessage constructor. + * + * @param SendGridMailBase $sendGridMailBase + * @param SendGridMailPersonalization $sendGridMailPersonalization + * @param SendGridMailMetadata $sendGridMailMetadata + * @param SendGridMailAttachment $sendGridMailAttachment + * @param SendGridMailHeader $sendGridMailHeader + */ public function __construct( SendGridMailBase $sendGridMailBase, SendGridMailPersonalization $sendGridMailPersonalization, SendGridMailMetadata $sendGridMailMetadata, - SendGridMailAttachment $sendGridMailAttachment + SendGridMailAttachment $sendGridMailAttachment, + SendGridMailHeader $sendGridMailHeader ) { $this->sendGridMailBase = $sendGridMailBase; $this->sendGridMailPersonalization = $sendGridMailPersonalization; $this->sendGridMailMetadata = $sendGridMailMetadata; $this->sendGridMailAttachment = $sendGridMailAttachment; + $this->sendGridMailHeader = $sendGridMailHeader; } /** @@ -63,6 +80,7 @@ public function getMessage(\Swift_Mime_Message $message) $this->sendGridMailPersonalization->addPersonalizedDataToMail($mail, $message); $this->sendGridMailMetadata->addMetadataToMail($mail, $message); $this->sendGridMailAttachment->addAttachmentsToMail($mail, $message); + $this->sendGridMailHeader->addHeadersToMail($mail, $message); return $mail; } diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php new file mode 100644 index 00000000000..190fd66bfab --- /dev/null +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php @@ -0,0 +1,52 @@ +getMockBuilder(\Swift_Mime_Message::class) + ->getMock(); + + $header = $this->createMock(\Swift_Mime_Header::class); + $header->expects($this->once()) + ->method('getFieldName') + ->willReturn('name'); + $header->expects($this->once()) + ->method('getFieldBodyModel') + ->willReturn('body'); + $header->expects($this->once()) + ->method('getFieldType') + ->willReturn(\Swift_Mime_Header::TYPE_TEXT); + + $headerSet = $this->createMock(\Swift_Mime_HeaderSet::class); + $headerSet->expects($this->once()) + ->method('getAll') + ->willReturn([$header]); + + $message->expects($this->once()) + ->method('getHeaders') + ->willReturn($headerSet); + + $mail = new Mail('from', 'subject', 'to', 'content'); + + $sendGridMailHeader->addHeadersToMail($mail, $message); + + $this->assertEquals(['name' => 'body'], $mail->getHeaders()); + } +} diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php index dd73c7b86a9..b3e84e90978 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php @@ -13,6 +13,7 @@ use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailAttachment; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailBase; +use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailHeader; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailMetadata; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailPersonalization; use Mautic\EmailBundle\Swiftmailer\SendGrid\SendGridApiMessage; @@ -38,6 +39,10 @@ public function testGetMail() ->disableOriginalConstructor() ->getMock(); + $sendGridMailHeader = $this->getMockBuilder(SendGridMailHeader::class) + ->disableOriginalConstructor() + ->getMock(); + $mail = $this->getMockBuilder(Mail::class) ->disableOriginalConstructor() ->getMock(); @@ -46,7 +51,7 @@ public function testGetMail() ->disableOriginalConstructor() ->getMock(); - $sendGridApiMessage = new SendGridApiMessage($sendGridMailBase, $sendGridMailPersonalization, $sendGridMailMetadata, $sendGridMailAttachment); + $sendGridApiMessage = new SendGridApiMessage($sendGridMailBase, $sendGridMailPersonalization, $sendGridMailMetadata, $sendGridMailAttachment, $sendGridMailHeader); $sendGridMailBase->expects($this->once()) ->method('getSendGridMail') @@ -65,6 +70,10 @@ public function testGetMail() ->method('addAttachmentsToMail') ->with($mail, $message); + $sendGridMailHeader->expects($this->once()) + ->method('addHeadersToMail') + ->with($mail, $message); + $result = $sendGridApiMessage->getMessage($message); $this->assertSame($mail, $result); From e4667b755e5b871dd0c2d9e35677db8fd9a81aa1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 12:42:33 -0500 Subject: [PATCH 325/778] Exclude reserved headers --- .../SendGrid/Mail/SendGridMailHeader.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php index fe248bc9d39..ec1e1d6fb99 100644 --- a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php +++ b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php @@ -15,6 +15,26 @@ class SendGridMailHeader { + /** + * https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#personalizations_headers. + * + * @var array + */ + private $reservedKeys = [ + 'x-sg-id', + 'x-sg-eid', + 'received', + 'dkim-signature', + 'Content-Type', + 'Content-Transfer-Encoding', + 'To', + 'From', + 'Subject', + 'Reply-To', + 'CC', + 'BCC', + ]; + /** * @param Mail $mail * @param \Swift_Mime_Message $message @@ -24,7 +44,7 @@ public function addHeadersToMail(Mail $mail, \Swift_Mime_Message $message) $headers = $message->getHeaders()->getAll(); /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && !in_array($header->getFieldName(), $this->reservedKeys)) { $mail->addHeader($header->getFieldName(), $header->getFieldBodyModel()); } } From 37302eb6807d26304465ba4093d5d72a241d4bd8 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 12:57:18 -0500 Subject: [PATCH 326/778] Fixed error feedback to the UI --- app/bundles/EmailBundle/Controller/AjaxController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Controller/AjaxController.php b/app/bundles/EmailBundle/Controller/AjaxController.php index 200b8577898..c4e5f269b76 100644 --- a/app/bundles/EmailBundle/Controller/AjaxController.php +++ b/app/bundles/EmailBundle/Controller/AjaxController.php @@ -294,7 +294,9 @@ protected function sendTestEmailAction(Request $request) $message = $translator->trans('mautic.core.success'); if (!$mailer->send(true)) { $success = 0; - $message = implode('; ', $mailer->getErrors()); + $errors = $mailer->getErrors(); + unset($errors['failures']); + $message = implode('; ', $errors); } return $this->sendJsonResponse(['success' => $success, 'message' => $message]); From b6c28ebf6752ee2a98e001ce345410fb36d9a1b6 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 12:58:13 -0500 Subject: [PATCH 327/778] Exclude reserved headers and don't set headers for Sparkpost if there aren't any to prevent Sparkpost error --- .../Transport/AbstractTokenArrayTransport.php | 19 ++++++++++++++++++- .../Transport/SparkpostTransport.php | 13 ++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Transport/AbstractTokenArrayTransport.php b/app/bundles/EmailBundle/Swiftmailer/Transport/AbstractTokenArrayTransport.php index 7e5e3b5fef0..e114c4a9636 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Transport/AbstractTokenArrayTransport.php +++ b/app/bundles/EmailBundle/Swiftmailer/Transport/AbstractTokenArrayTransport.php @@ -36,6 +36,23 @@ abstract class AbstractTokenArrayTransport implements TokenTransportInterface */ protected $started = false; + /** + * @var array + */ + protected $standardHeaderKeys = [ + 'MIME-Version', + 'received', + 'dkim-signature', + 'Content-Type', + 'Content-Transfer-Encoding', + 'To', + 'From', + 'Subject', + 'Reply-To', + 'CC', + 'BCC', + ]; + /** * @var MauticFactory * @@ -254,7 +271,7 @@ protected function messageToArray($search = [], $replace = [], $binaryAttachment $headers = $this->message->getHeaders()->getAll(); /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && !in_array($header->getFieldName(), $this->standardHeaderKeys)) { $message['headers'][$header->getFieldName()] = $header->getFieldBodyModel(); } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php b/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php index 010aa108ff4..c093c4f406d 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php +++ b/app/bundles/EmailBundle/Swiftmailer/Transport/SparkpostTransport.php @@ -172,14 +172,6 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) $message = $this->messageToArray($mauticTokens, $mergeVarPlaceholders, true); - // These are reserved headers - unset( - $message['headers']['Subject'], - $message['headers']['MIME-Version'], - $message['headers']['Content-Type'], - $message['headers']['Content-Transfer-Encoding'] - ); - // Sparkpost requires a subject if (empty($message['subject'])) { throw new \Exception($this->translator->trans('mautic.email.subject.notblank', [], 'validators')); @@ -254,9 +246,12 @@ public function getSparkPostMessage(\Swift_Mime_Message $message) 'from' => (!empty($message['from']['name'])) ? $message['from']['name'].' <'.$message['from']['email'].'>' : $message['from']['email'], 'subject' => $message['subject'], - 'headers' => $message['headers'], ]; + if (!empty($message['headers'])) { + $content['headers'] = $message['headers']; + } + // Sparkpost will set parts regardless if they are empty or not if (!empty($message['html'])) { $content['html'] = $message['html']; From 6d51623b19a75ed76d41a5e4e719f8d42f3b38a8 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 13:31:02 -0500 Subject: [PATCH 328/778] Fixed tests --- .../Swiftmailer/SendGrid/Mail/SendGridMailHeader.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php index ec1e1d6fb99..4f25b64724a 100644 --- a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php +++ b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php @@ -44,8 +44,9 @@ public function addHeadersToMail(Mail $mail, \Swift_Mime_Message $message) $headers = $message->getHeaders()->getAll(); /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && !in_array($header->getFieldName(), $this->reservedKeys)) { - $mail->addHeader($header->getFieldName(), $header->getFieldBodyModel()); + $headerName = $header->getFieldName(); + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && !in_array($headerName, $this->reservedKeys)) { + $mail->addHeader($headerName, $header->getFieldBodyModel()); } } } From e5c9b92361bb697d6ca7ddac9e15c1c9f927d3cf Mon Sep 17 00:00:00 2001 From: Don Gilbert Date: Fri, 11 May 2018 16:07:07 -0400 Subject: [PATCH 329/778] Change default behavior for Send form results action to NOT send a copy to the contact who submitted the form. --- .../FormBundle/Form/Type/SubmitActionEmailType.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/bundles/FormBundle/Form/Type/SubmitActionEmailType.php b/app/bundles/FormBundle/Form/Type/SubmitActionEmailType.php index c47ca6f74be..0b972f8ef6c 100644 --- a/app/bundles/FormBundle/Form/Type/SubmitActionEmailType.php +++ b/app/bundles/FormBundle/Form/Type/SubmitActionEmailType.php @@ -100,7 +100,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ); if ($this->coreParametersHelper->getParameter('mailer_spool_type') == 'file') { - $default = (isset($options['data']['immediately'])) ? $options['data']['immediately'] : false; + $default = isset($options['data']['immediately']) ? $options['data']['immediately'] : false; $builder->add( 'immediately', YesNoButtonGroupType::class, @@ -122,7 +122,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ); } - $default = (isset($options['data']['copy_lead'])) ? $options['data']['copy_lead'] : true; + $default = isset($options['data']['copy_lead']) ? $options['data']['copy_lead'] : false; $builder->add( 'copy_lead', YesNoButtonGroupType::class, @@ -132,7 +132,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $default = (isset($options['data']['set_replyto'])) ? $options['data']['set_replyto'] : true; + $default = isset($options['data']['set_replyto']) ? $options['data']['set_replyto'] : true; $builder->add( 'set_replyto', 'yesno_button_group', @@ -145,7 +145,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $default = (isset($options['data']['email_to_owner'])) ? $options['data']['email_to_owner'] : false; + $default = isset($options['data']['email_to_owner']) ? $options['data']['email_to_owner'] : false; $builder->add( 'email_to_owner', YesNoButtonGroupType::class, From d4b6be18a977eab214ff044653cc305233441002 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Mon, 14 May 2018 17:51:44 +0200 Subject: [PATCH 330/778] Webhook payload should be type of TEXT, not INT --- app/bundles/WebhookBundle/Entity/WebhookQueue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/WebhookBundle/Entity/WebhookQueue.php b/app/bundles/WebhookBundle/Entity/WebhookQueue.php index 68b5e84a77f..602728a8278 100644 --- a/app/bundles/WebhookBundle/Entity/WebhookQueue.php +++ b/app/bundles/WebhookBundle/Entity/WebhookQueue.php @@ -59,7 +59,7 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) ->addJoinColumn('webhook_id', 'id', false, false, 'CASCADE') ->build(); $builder->addNullableField('dateAdded', Type::DATETIME, 'date_added'); - $builder->addField('payload', Type::INTEGER); + $builder->addField('payload', Type::TEXT); $builder->createManyToOne('event', 'Event') ->inversedBy('queues') ->addJoinColumn('event_id', 'id', false, false, 'CASCADE') From 1422c351cdf8af44d0172bd715998ffba18ad0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Wed, 25 Apr 2018 18:21:31 +0200 Subject: [PATCH 331/778] WIP --- .../Momentum/Adapter/MomentumAdapter.php | 39 ++++ .../Adapter/MomentumAdapterInterface.php | 18 ++ ...omentumSwiftMessageValidationException.php | 10 + .../Momentum/Facade/MomentumFacade.php | 78 ++++++++ .../Facade/MomentumFacadeInterface.php | 12 ++ .../Service/MomentumSwiftMessageService.php | 44 +++++ .../MomentumSwiftMessageServiceInterface.php | 25 +++ .../MomentumSwiftMessageValidator.php | 20 ++ ...MomentumSwiftMessageValidatorInterface.php | 18 ++ .../SendGrid/SendGridApiFacade.php | 3 +- .../Sparkpost/SparkpostFactory.php | 37 ++++ .../Sparkpost/SparkpostFactoryInterface.php | 18 ++ .../SwiftmailerFacadeInterface.php | 16 ++ .../Transport/MomentumApiTransport.php | 181 ++++++++++++++++++ 14 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapter.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/MomentumSwiftMessageValidationException.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacadeInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageService.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/MomentumSwiftMessageValidator.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/MomentumSwiftMessageValidatorInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/SwiftmailerFacadeInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Transport/MomentumApiTransport.php diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapter.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapter.php new file mode 100644 index 00000000000..a9521d38270 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapter.php @@ -0,0 +1,39 @@ +sparkpost = $sparkpostFactory->create($apiKey); + } + + /** + * @param array $message + * + * @return SparkPostPromise + */ + public function send(array $message = []) + { + return $this->sparkpost->transmissions->post($message); + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php new file mode 100644 index 00000000000..c58f8e5c3b8 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php @@ -0,0 +1,18 @@ +momentumAdapter = $momentumAdapter; + $this->momentumSwiftMessageService = $momentumSwiftMessageService; + } + + /** + * @param \Swift_Mime_Message $message + * + * @throws MomentumSwiftMessageValidationException + */ + public function send(\Swift_Mime_Message $message) + { + try { + $this->momentumSwiftMessageService->validate($message); + $momentumMessage = $this->momentumSwiftMessageService->getMomentumMessage($message); + + $response = $this->momentumAdapter->send($momentumMessage); + $response = $response->wait(); + if (200 == (int) $response->getStatusCode()) { + $results = $response->getBody(); + if (!$sendCount = $results['results']['total_accepted_recipients']) { + $this->processResponseErrors($momentumMessage, $results); + } + } + } catch (\Exception $exception) { + } + } + + /** + * @param array $momentumMessage + * @param array $results + */ + private function processResponseErrors(array $momentumMessage, array $results) + { + if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { + $comments = $response['errors'][0]['description']; + $emailAddress = $momentumMessage['recipients']['to'][0]['email']; + $metadata = $this->getMetadata(); + + if (isset($metadata[$emailAddress]) && isset($metadata[$emailAddress]['leadId'])) { + $emailId = (!empty($metadata[$emailAddress]['emailId'])) ? $metadata[$emailAddress]['emailId'] : null; + $this->transportCallback->addFailureByContactId($metadata[$emailAddress]['leadId'], $comments, DoNotContact::BOUNCED, $emailId); + } + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacadeInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacadeInterface.php new file mode 100644 index 00000000000..410239b04ac --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacadeInterface.php @@ -0,0 +1,12 @@ +translator = $translator; + } + + public function getMomentumMessage(\Swift_Mime_Message $message) + { + } + + /** + * @param \Swift_Mime_Message $message + * + * @throws MomentumSwiftMessageValidationException + */ + public function validate(\Swift_Mime_Message $message) + { + if (empty($message['subject'])) { + throw new MomentumSwiftMessageValidationException($this->translator->trans('mautic.email.subject.notblank', [], 'validators')); + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php new file mode 100644 index 00000000000..2e5cd515423 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php @@ -0,0 +1,25 @@ +client = $client; + } + + /** + * @param string $apiKey + * + * @return SparkPost + */ + public function create($apiKey) + { + return new SparkPost($this->client, ['key' => $apiKey]); + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php new file mode 100644 index 00000000000..127a880944d --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php @@ -0,0 +1,18 @@ +momentumApiFacade = $momentumApiFacade; + $this->sendGridApiCallback = $sendGridApiCallback; + } + + /** + * Test if this Transport mechanism has starte-d. + * + * @return bool + */ + public function isStarted() + { + return $this->started; + } + + /** + * Start this Transport mechanism. + * + * @throws \Swift_TransportException + */ + public function start() + { + $this->started = true; + } + + /** + * Stop this Transport mechanism. + */ + public function stop() + { + } + + /** + * Send the given Message. + * + * Recipient/sender data will be retrieved from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param Swift_Mime_Message $message + * @param string[] $failedRecipients An array of failures by-reference + * + * @return int + * + * @throws \Swift_TransportException + */ + public function send(Swift_Mime_Message $message, &$failedRecipients = null) + { + $this->momentumApiFacade->send($message); + + return count($message->getTo()); + } + + /** + * Register a plugin in the Transport. + * + * @param Swift_Events_EventListener $plugin + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + $this->getDispatcher()->bindEventListener($plugin); + } + + /** + * @return \Swift_Events_SimpleEventDispatcher + */ + private function getDispatcher() + { + if ($this->swiftEventDispatcher === null) { + $this->swiftEventDispatcher = new \Swift_Events_SimpleEventDispatcher(); + } + + return $this->swiftEventDispatcher; + } + + /** + * Return the max number of to addresses allowed per batch. If there is no limit, return 0. + * + * @return int + */ + public function getMaxBatchLimit() + { + //Sengrid allows to include max 1000 email address into 1 batch + return 1000; + } + + /** + * Get the count for the max number of recipients per batch. + * + * @param \Swift_Message $message + * @param int $toBeAdded Number of emails about to be added + * @param string $type Type of emails being added (to, cc, bcc) + * + * @return int + */ + public function getBatchRecipientCount(\Swift_Message $message, $toBeAdded = 1, $type = 'to') + { + //Sengrid counts all email address (to, cc and bcc) + //https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#message.personalizations + return count($message->getTo()) + count($message->getCc()) + count($message->getBcc()) + $toBeAdded; + } + + /** + * Function required to check that $this->message is instanceof MauticMessage, return $this->message->getMetadata() if it is and array() if not. + * + * @throws \Exception + */ + public function getMetadata() + { + throw new \Exception('Not implemented'); + } + + /** + * Returns a "transport" string to match the URL path /mailer/{transport}/callback. + * + * @return mixed + */ + public function getCallbackPath() + { + return 'momentum_api'; + } + + /** + * Processes the response. + * + * @param Request $request + */ + public function processCallbackRequest(Request $request) + { + $this->sendGridApiCallback->processCallbackRequest($request); + } +} From b8eb53b40e1cf1031f8dc2cf6c96e9cec66ba91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 3 May 2018 13:04:32 +0200 Subject: [PATCH 332/778] Momentum integration --- app/bundles/EmailBundle/Config/config.php | 69 ++++++++++ .../EmailBundle/Model/TransportType.php | 1 + .../Swiftmailer/Guzzle/ClientFactory.php | 22 +++ .../Guzzle/ClientFactoryInterface.php | 19 +++ .../Swiftmailer/Momentum/Adapter/Adapter.php | 41 ++++++ .../Momentum/Adapter/AdapterInterface.php | 19 +++ .../Momentum/Adapter/MomentumAdapter.php | 39 ------ .../Adapter/MomentumAdapterInterface.php | 18 --- .../Momentum/Callback/CallbackEnum.php | 82 ++++++++++++ .../Momentum/Callback/MomentumCallback.php | 36 +++++ .../Momentum/Callback/ResponseItem.php | 71 ++++++++++ .../Momentum/Callback/ResponseItems.php | 98 ++++++++++++++ .../Momentum/DTO/MomentumMessage.php | 125 ++++++++++++++++++ .../Momentum/DTO/TransmissionDTO.php | 96 ++++++++++++++ .../DTO/TransmissionDTO/ContentDTO.php | 115 ++++++++++++++++ .../TransmissionDTO/ContentDTO/FromDTO.php | 56 ++++++++ .../DTO/TransmissionDTO/OptionsDTO.php | 79 +++++++++++ .../DTO/TransmissionDTO/RecipientsDTO.php | 123 +++++++++++++++++ .../RecipientsDTO/AddressDTO.php | 65 +++++++++ .../Facade/MomentumSendException.php | 10 ++ ...omentumSwiftMessageValidationException.php | 10 -- .../SwiftMessageValidationException.php | 10 ++ .../Momentum/Facade/MomentumFacade.php | 64 +++++---- .../Service/MomentumSwiftMessageService.php | 44 ------ .../MomentumSwiftMessageServiceInterface.php | 25 ---- .../Momentum/Service/SwiftMessageService.php | 67 ++++++++++ .../Service/SwiftMessageServiceInterface.php | 18 +++ .../MomentumSwiftMessageValidator.php | 20 --- ...MomentumSwiftMessageValidatorInterface.php | 18 --- .../SwiftMessageValidator.php | 40 ++++++ .../SwiftMessageValidatorInterface.php | 18 +++ ...ApiTransport.php => MomentumTransport.php} | 31 ++--- .../Callback/SendGridApiCallbackTest.php | 4 +- .../Translations/en_US/messages.ini | 1 + 34 files changed, 1339 insertions(+), 215 deletions(-) create mode 100644 app/bundles/EmailBundle/Swiftmailer/Guzzle/ClientFactory.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Guzzle/ClientFactoryInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapter.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/CallbackEnum.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItem.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItems.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/FromDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/OptionsDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/MomentumSwiftMessageValidationException.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Validator/SwiftMessageValidator/SwiftMessageValidationException.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageService.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageServiceInterface.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/MomentumSwiftMessageValidator.php delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/MomentumSwiftMessageValidatorInterface.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/SwiftMessageValidator/SwiftMessageValidator.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/SwiftMessageValidator/SwiftMessageValidatorInterface.php rename app/bundles/EmailBundle/Swiftmailer/Transport/{MomentumApiTransport.php => MomentumTransport.php} (82%) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index c46c0f39d50..7a687239bed 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -363,6 +363,62 @@ 'setPassword' => ['%mautic.mailer_password%'], ], ], + 'mautic.transport.momentum' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Transport\MomentumTransport::class, + 'arguments' => [ + 'mautic.transport.momentum.callback', + 'mautic.transport.momentum.facade', + ], + ], + 'mautic.transport.momentum.adapter' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\Adapter::class, + 'arguments' => [ + 'mautic.transport.momentum.sparkpost', + ], + ], + 'mautic.transport.momentum.service.swift_message' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Service\SwiftMessageService::class, + 'arguments' => [ + 'translator', + ], + ], + 'mautic.transport.momentum.validator.swift_message' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Validator\SwiftMessageValidator\SwiftMessageValidator::class, + 'arguments' => [ + 'translator', + ], + ], + 'mautic.transport.momentum.callback' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Callback\MomentumCallback::class, + 'arguments' => [ + 'mautic.email.model.transport_callback', + ], + ], + 'mautic.transport.momentum.facade' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Facade\MomentumFacade::class, + 'arguments' => [ + 'mautic.transport.momentum.adapter', + 'mautic.transport.momentum.service.swift_message', + 'mautic.transport.momentum.validator.swift_message', + ], + ], + 'mautic.transport.momentum.sparkpost' => [ + 'class' => \SparkPost\SparkPost::class, + 'factory' => ['@mautic.sparkpost.factory', 'create'], + 'arguments' => [ + '%mautic.momentum_api_key%', + ], + 'methodCalls' => [ + 'setOptions' => [ + 'host' => '%mautic.momentum_host%', + 'protocol' => '%mautic.momentum_protocol%', + 'port' => '%mautic.momentum_port%', + 'key' => '%mautic.momentum_api_key%', + 'version' => '%mautic.momentum_version%', + 'async' => '%mautic.momentum_async%', + ], + ], + ], 'mautic.transport.sendgrid' => [ 'class' => 'Mautic\EmailBundle\Swiftmailer\Transport\SendgridTransport', 'serviceAlias' => 'swiftmailer.mailer.transport.%s', @@ -469,6 +525,19 @@ 'mautic.email.model.transport_callback', ], ], + 'mautic.sparkpost.factory' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Sparkpost\SparkpostFactory::class, + 'arguments' => [ + 'mautic.guzzle.client', + ], + ], + 'mautic.guzzle.client.factory' => [ + 'class' => \Mautic\EmailBundle\Swiftmailer\Guzzle\ClientFactory::class, + ], + 'mautic.guzzle.client' => [ + 'class' => \Http\Adapter\Guzzle6\Client::class, + 'factory' => ['@mautic.guzzle.client.factory', 'create'], + ], 'mautic.helper.mailbox' => [ 'class' => 'Mautic\EmailBundle\MonitoredEmail\Mailbox', 'arguments' => [ diff --git a/app/bundles/EmailBundle/Model/TransportType.php b/app/bundles/EmailBundle/Model/TransportType.php index 66ab48f1ac9..a262e07f45a 100644 --- a/app/bundles/EmailBundle/Model/TransportType.php +++ b/app/bundles/EmailBundle/Model/TransportType.php @@ -19,6 +19,7 @@ public function getTransportTypes() 'mautic.transport.sendgrid_api' => 'mautic.email.config.mailer_transport.sendgrid_api', 'sendmail' => 'mautic.email.config.mailer_transport.sendmail', 'mautic.transport.sparkpost' => 'mautic.email.config.mailer_transport.sparkpost', + 'mautic.transport.momentum' => 'mautic.email.config.mailer_transport.momentum', ]; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Guzzle/ClientFactory.php b/app/bundles/EmailBundle/Swiftmailer/Guzzle/ClientFactory.php new file mode 100644 index 00000000000..f6e151504fc --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Guzzle/ClientFactory.php @@ -0,0 +1,22 @@ +sparkpost = $momentumSparkpost; + } + + /** + * @param TransmissionDTO $transmissionDTO + * + * @return SparkPostPromise + */ + public function createTransmission(TransmissionDTO $transmissionDTO) + { + dump($this->sparkpost); + exit; + + return $this->sparkpost->transmissions->post(json_encode($transmissionDTO)); + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php new file mode 100644 index 00000000000..2cdac6f4bc7 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php @@ -0,0 +1,19 @@ +sparkpost = $sparkpostFactory->create($apiKey); - } - - /** - * @param array $message - * - * @return SparkPostPromise - */ - public function send(array $message = []) - { - return $this->sparkpost->transmissions->post($message); - } -} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php deleted file mode 100644 index c58f8e5c3b8..00000000000 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/MomentumAdapterInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - DoNotContact::BOUNCED, + self::DROPPED => DoNotContact::BOUNCED, + self::SPAM_REPORT => DoNotContact::BOUNCED, + self::UNSUBSCRIBE => DoNotContact::UNSUBSCRIBED, + self::GROUP_UNSUBSCRIBE => DoNotContact::UNSUBSCRIBED, + ]; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php new file mode 100644 index 00000000000..07184405fc0 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php @@ -0,0 +1,36 @@ +transportCallback = $transportCallback; + } + + public function processCallbackRequest(Request $request) + { + $responseItems = new ResponseItems($request); + foreach ($responseItems as $item) { + $this->transportCallback->addFailureByAddress($item->getEmail(), $item->getReason(), $item->getDncReason()); + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItem.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItem.php new file mode 100644 index 00000000000..7e7c23694c9 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItem.php @@ -0,0 +1,71 @@ +email = $item['email']; + $this->reason = !empty($item['reason']) ? $item['reason'] : null; + $this->dncReason = CallbackEnum::convertEventToDncReason($item['event']); + } + + /** + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * @return string + */ + public function getReason() + { + return $this->reason; + } + + /** + * @return int + */ + public function getDncReason() + { + return $this->dncReason; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItems.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItems.php new file mode 100644 index 00000000000..91d788a73f5 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/ResponseItems.php @@ -0,0 +1,98 @@ +request->all(); + foreach ($payload as $item) { + if (empty($item['event']) || !CallbackEnum::shouldBeEventProcessed($item['event'])) { + continue; + } + try { + $this->items[] = new ResponseItem($item); + } catch (ResponseItemException $e) { + } + } + } + + /** + * Return the current element. + * + * @see http://php.net/manual/en/iterator.current.php + * + * @return ResponseItem + */ + public function current() + { + return $this->items[$this->position]; + } + + /** + * Move forward to next element. + * + * @see http://php.net/manual/en/iterator.next.php + */ + public function next() + { + ++$this->position; + } + + /** + * Return the key of the current element. + * + * @see http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->position; + } + + /** + * Checks if current position is valid. + * + * @see http://php.net/manual/en/iterator.valid.php + * + * @return bool + */ + public function valid() + { + return isset($this->items[$this->position]); + } + + /** + * Rewind the Iterator to the first element. + * + * @see http://php.net/manual/en/iterator.rewind.php + */ + public function rewind() + { + $this->position = 0; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php new file mode 100644 index 00000000000..a078148b68b --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php @@ -0,0 +1,125 @@ +setContent($swiftMessage); + $this->setHeaders($swiftMessage); + $this->setRecipients($swiftMessage); + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + return [ + 'content' => $this->content, + 'headers' => $this->headers, + 'recipients' => $this->recipients, + 'tags' => $this->tags, + ]; + } + + /** + * @param \Swift_Mime_Message $message + */ + private function setContent(\Swift_Mime_Message $message) + { + $from = $message->getFrom(); + $fromEmail = current(array_keys($from)); + $fromName = $from[$fromEmail]; + $this->content = [ + 'from' => (!empty($fromName) ? ($fromName.' <'.$fromEmail.'>') : $fromEmail), + 'subject' => $message->getSubject(), + ]; + if (!empty($message->getBody())) { + $this->content['html'] = $message->getBody(); + } + $messageText = PlainTextMassageHelper::getPlainTextFromMessage($message); + if (!empty($messageText)) { + $this->content['text'] = $messageText; + } + $encoder = new \Swift_Mime_ContentEncoder_Base64ContentEncoder(); + foreach ($message->getChildren() as $child) { + if ($child instanceof \Swift_Image) { + $this->content['inline_images'][] = [ + 'type' => $child->getContentType(), + 'name' => $child->getId(), + 'data' => $encoder->encodeString($child->getBody()), + ]; + } + } + } + + /** + * @param \Swift_Mime_Message $message + */ + private function setHeaders(\Swift_Mime_Message $message) + { + $headers = $message->getHeaders()->getAll(); + /** @var \Swift_Mime_Header $header */ + foreach ($headers as $header) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { + $this->headers[$header->getFieldName()] = $header->getFieldBodyModel(); + } + } + } + + /** + * @param \Swift_Mime_Message $message + */ + private function setRecipients(\Swift_Mime_Message $message) + { + foreach ($message->getTo() as $email => $name) { + $recipient = [ + 'address' => $email, + 'substitution_data' => [], + 'metadata' => [], + ]; + + // Apparently Sparkpost (legacy from Sparkpost implementation) doesn't like empty substitution_data or metadata + if (empty($recipient['substitution_data'])) { + unset($recipient['substitution_data']); + } + if (empty($recipient['metadata'])) { + unset($recipient['metadata']); + } + + $this->recipients[] = $recipient; + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php new file mode 100644 index 00000000000..3fea3a4ea8e --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php @@ -0,0 +1,96 @@ +recipients = $recipients; + $this->content = $content; + $this->options = $options; + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + $json = [ + 'return_path' => $this->returnPath, + 'recipients' => json_encode($this->recipients), + 'content' => json_encode($this->content), + ]; + if ($this->options !== null) { + $json['options'] = json_encode($this->options); + } + if ($this->campaignId !== null) { + $json['campaign_id'] = $this->campaignId; + } + if ($this->description !== null) { + $json['description'] = $this->description; + } + if (count($this->metadata) !== 0) { + $json['metadata'] = $this->metadata; + } + if (count($this->substitutionData) !== 0) { + $json['substitution_data'] = $this->substitutionData; + } + + return $json; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php new file mode 100644 index 00000000000..7a84a868e58 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php @@ -0,0 +1,115 @@ +subject = $subject; + $this->from = $from; + } + + /** + * @param null|string $html + * + * @return ContentDTO + */ + public function setHtml($html) + { + $this->html = $html; + + return $this; + } + + /** + * @param null|string $text + * + * @return ContentDTO + */ + public function setText($text) + { + $this->text = $text; + + return $this; + } + + /** + * @param string $key + * @param string $value + * + * @return ContentDTO + */ + public function addHeader($key, $value) + { + $this->headers[$key] = $value; + + return $this; + } + + /** + * @return array|mixed + */ + public function jsonSerialize() + { + $json = [ + 'subject' => $this->subject, + 'from' => $this->from, + ]; + if ($this->html !== null) { + $json['html'] = $this->html; + } + if ($this->text !== null) { + $json['text'] = $this->text; + } + if ($this->replyTo !== null) { + $json['reply_to'] = $this->replyTo; + } + if (count($this->headers) !== 0) { + $json['headers'] = $this->headers; + } + + return $json; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/FromDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/FromDTO.php new file mode 100644 index 00000000000..bd743ac92c4 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/FromDTO.php @@ -0,0 +1,56 @@ +email = $email; + } + + /** + * @param string|null $name + * + * @return FromDTO + */ + public function setName($name = null) + { + $this->name = $name; + + return $this; + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + $json = [ + 'email' => $this->email, + ]; + if ($this->name !== null) { + $json['name'] = $this->name; + } + + return $json; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/OptionsDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/OptionsDTO.php new file mode 100644 index 00000000000..072e27423cb --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/OptionsDTO.php @@ -0,0 +1,79 @@ +startTime !== null) { + $json['start_time'] = $this->startTime; + } + if ($this->openTracking !== null) { + $json['open_tracking'] = $this->openTracking; + } + if ($this->clickTracking !== null) { + $json['click_tracking'] = $this->clickTracking; + } + + return $json; + } + + /** + * @param null|string $startTime + * + * @return OptionsDTO + */ + public function setStartTime($startTime = null) + { + $this->startTime = $startTime; + + return $this; + } + + /** + * @param bool|null $openTracking + * + * @return OptionsDTO + */ + public function setOpenTracking($openTracking = null) + { + $this->openTracking = $openTracking; + + return $this; + } + + /** + * @param bool|null $clickTracking + * + * @return OptionsDTO + */ + public function setClickTracking($clickTracking = null) + { + $this->clickTracking = $clickTracking; + + return $this; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php new file mode 100644 index 00000000000..4d06975e827 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php @@ -0,0 +1,123 @@ +returnPath = $returnPath; + + return $this; + } + + /** + * @param AddressDTO $address + * + * @return RecipientsDTO + */ + public function addAddress(AddressDTO $address) + { + $this->addresses[] = $address; + + return $this; + } + + /** + * @param string $key + * @param string $value + * + * @return RecipientsDTO + */ + public function addTag($key, $value) + { + $this->tags[$key] = $value; + + return $this; + } + + /** + * @param string $key + * @param mixed $value + * + * @return RecipientsDTO + */ + public function addMetadata($key, $value) + { + $this->metadata[$key] = $value; + + return $this; + } + + /** + * @param string $key + * @param mixed $value + * + * @return $this + */ + public function addSubstitutionData($key, $value) + { + $this->substitutionData[$key] = $value; + + return $this; + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + $json = [ + 'address' => $this->addresses, + ]; + if (count($this->tags) !== 0) { + $json['tags'] = $this->tags; + } + if (count($this->metadata) !== 0) { + $json['metadata'] = $this->metadata; + } + if (count($this->substitutionData) !== 0) { + $json['substitution_data'] = $this->substitutionData; + } + if ($this->returnPath !== null) { + $json['return_path'] = $this->returnPath; + } + + return $json; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php new file mode 100644 index 00000000000..82d427af596 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php @@ -0,0 +1,65 @@ +email = $email; + if ($isBcc === false) { + $this->headerTo = $email; + } + } + + /** + * @param null|string $name + * + * @return AddressDTO + */ + public function setName($name = null) + { + $this->name = $name; + + return $this; + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + $json = [ + 'email' => $this->email, + ]; + if ($this->name !== null) { + $json['name'] = $this->name; + } + + return $json; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php new file mode 100644 index 00000000000..db4eca8f403 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php @@ -0,0 +1,10 @@ +momentumAdapter = $momentumAdapter; - $this->momentumSwiftMessageService = $momentumSwiftMessageService; + $this->adapter = $adapter; + $this->swiftMessageService = $swiftMessageService; + $this->swiftMessageValidator = $swiftMessageValidator; } /** * @param \Swift_Mime_Message $message * - * @throws MomentumSwiftMessageValidationException + * @throws SwiftMessageValidationException + * @throws MomentumSendException */ public function send(\Swift_Mime_Message $message) { try { - $this->momentumSwiftMessageService->validate($message); - $momentumMessage = $this->momentumSwiftMessageService->getMomentumMessage($message); - - $response = $this->momentumAdapter->send($momentumMessage); - $response = $response->wait(); + $this->swiftMessageValidator->validate($message); + $transmission = $this->swiftMessageService->transformToTransmission($message); + $response = $this->adapter->createTransmission($transmission); + $response = $response->wait(); if (200 == (int) $response->getStatusCode()) { $results = $response->getBody(); if (!$sendCount = $results['results']['total_accepted_recipients']) { - $this->processResponseErrors($momentumMessage, $results); + $this->processResponseErrors($transmission, $results); } } } catch (\Exception $exception) { + if ($exception instanceof SwiftMessageValidationException) { + throw $exception; + } + throw new MomentumSendException(); } } /** - * @param array $momentumMessage - * @param array $results + * @param TransmissionDTO $transmissionDTO + * @param array $results */ - private function processResponseErrors(array $momentumMessage, array $results) + private function processResponseErrors(TransmissionDTO $transmissionDTO, array $results) { + /* if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { $comments = $response['errors'][0]['description']; $emailAddress = $momentumMessage['recipients']['to'][0]['email']; @@ -73,6 +89,6 @@ private function processResponseErrors(array $momentumMessage, array $results) $emailId = (!empty($metadata[$emailAddress]['emailId'])) ? $metadata[$emailAddress]['emailId'] : null; $this->transportCallback->addFailureByContactId($metadata[$emailAddress]['leadId'], $comments, DoNotContact::BOUNCED, $emailId); } - } + }*/ } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageService.php deleted file mode 100644 index b4f685040c5..00000000000 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageService.php +++ /dev/null @@ -1,44 +0,0 @@ -translator = $translator; - } - - public function getMomentumMessage(\Swift_Mime_Message $message) - { - } - - /** - * @param \Swift_Mime_Message $message - * - * @throws MomentumSwiftMessageValidationException - */ - public function validate(\Swift_Mime_Message $message) - { - if (empty($message['subject'])) { - throw new MomentumSwiftMessageValidationException($this->translator->trans('mautic.email.subject.notblank', [], 'validators')); - } - } -} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php deleted file mode 100644 index 2e5cd515423..00000000000 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/MomentumSwiftMessageServiceInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -translator = $translator; + } + + /** + * @param \Swift_Mime_Message $message + * + * @return TransmissionDTO + */ + public function transformToTransmission(\Swift_Mime_Message $message) + { + $recipients = new TransmissionDTO\RecipientsDTO(); + foreach ($message->getTo() as $email => $name) { + $address = new TransmissionDTO\RecipientsDTO\AddressDTO($email); + $recipients->addAddress($address); + } + $messageFrom = $message->getFrom(); + $messageFromEmail = current(array_keys($messageFrom)); + $from = new TransmissionDTO\ContentDTO\FromDTO($messageFromEmail); + if (!empty($messageFrom[$messageFromEmail])) { + $from->setName($messageFrom[$messageFromEmail]); + } + $content = new TransmissionDTO\ContentDTO($message->getSubject(), $from); + if (!empty($message->getBody())) { + $content->setHtml($message->getBody()); + } + $headers = $message->getHeaders()->getAll(); + /** @var \Swift_Mime_Header $header */ + foreach ($headers as $header) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { + $content->addHeader($header->getFieldName(), $header->getFieldBodyModel()); + } + } + $messageText = PlainTextMassageHelper::getPlainTextFromMessage($message); + if (!empty($messageText)) { + $content->setText($messageText); + } + $transmission = new TransmissionDTO($recipients, $content); + + return $transmission; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageServiceInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageServiceInterface.php new file mode 100644 index 00000000000..04096ab5a69 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageServiceInterface.php @@ -0,0 +1,18 @@ +translator = $translator; + } + + /** + * @param \Swift_Mime_Message $message + * + * @throws SwiftMessageValidationException + */ + public function validate(\Swift_Mime_Message $message) + { + if (empty($message->getSubject())) { + throw new SwiftMessageValidationException($this->translator->trans('mautic.email.subject.notblank', [], 'validators')); + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/SwiftMessageValidator/SwiftMessageValidatorInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/SwiftMessageValidator/SwiftMessageValidatorInterface.php new file mode 100644 index 00000000000..8b0f277282a --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Validator/SwiftMessageValidator/SwiftMessageValidatorInterface.php @@ -0,0 +1,18 @@ +momentumApiFacade = $momentumApiFacade; - $this->sendGridApiCallback = $sendGridApiCallback; + $this->momentumCallback = $momentumCallback; + $this->momentumFacade = $momentumFacade; } /** @@ -95,7 +96,7 @@ public function stop() */ public function send(Swift_Mime_Message $message, &$failedRecipients = null) { - $this->momentumApiFacade->send($message); + $this->momentumFacade->send($message); return count($message->getTo()); } @@ -176,6 +177,6 @@ public function getCallbackPath() */ public function processCallbackRequest(Request $request) { - $this->sendGridApiCallback->processCallbackRequest($request); + $this->momentumCallback->processCallbackRequest($request); } } diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php index b6608de0357..5bbba0324d1 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php @@ -12,7 +12,7 @@ namespace Mautic\EmailBundle\Tests\Swiftmailer\SendGrid\Callback; use Mautic\EmailBundle\Model\TransportCallback; -use Mautic\EmailBundle\Swiftmailer\SendGrid\Callback\SendGridApiCallback; +use Mautic\EmailBundle\Swiftmailer\SendGrid\Callback\MomentumCallback; use Mautic\LeadBundle\Entity\DoNotContact; use Symfony\Component\HttpFoundation\Request; @@ -24,7 +24,7 @@ public function testSupportedEvents() ->disableOriginalConstructor() ->getMock(); - $sendGridApiCallback = new SendGridApiCallback($transportCallback); + $sendGridApiCallback = new MomentumCallback($transportCallback); $payload = [ [ diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index fd846f063ad..822077f7cb5 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -145,6 +145,7 @@ mautic.email.config.mailer_transport.elasticemail="Elastic Email" mautic.email.config.mailer_transport.sendmail="Sendmail" mautic.email.config.mailer_transport.smtp="Other SMTP Server" mautic.email.config.mailer_transport.sparkpost="Sparkpost" +mautic.email.config.mailer_transport.momentum="Momentum" mautic.email.config.monitored_email.bounce_folder.tooltip="Folder to monitor for new bounce messages. Leave blank to disable. NOTE: Gmail will rewrite Return-Path headers when sending through their SMTP servers. Mautic will still attempt to analyze new messages for bounces but it is best to use another sending method or configure a unique mailbox." mautic.email.config.monitored_email.bounce_folder="Bounces" mautic.email.config.monitored_email.general="Default Mailbox" From f3b49e5ce032b55ecc348dbd1082c568df1cfa51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 4 May 2018 14:46:37 +0200 Subject: [PATCH 333/778] Refactor to custom adapter implementation, DTO midifications --- app/bundles/EmailBundle/Config/config.php | 17 +++-- .../Swiftmailer/Momentum/Adapter/Adapter.php | 35 +++++++--- .../Momentum/DTO/MomentumMessage.php | 16 +---- .../Momentum/DTO/TransmissionDTO.php | 30 ++++++--- .../{RecipientsDTO.php => RecipientDTO.php} | 34 +++++----- .../RecipientsDTO/AddressDTO.php | 65 ------------------- .../Momentum/Facade/MomentumFacade.php | 2 + .../Momentum/Service/SwiftMessageService.php | 10 ++- 8 files changed, 79 insertions(+), 130 deletions(-) rename app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/{RecipientsDTO.php => RecipientDTO.php} (77%) delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 7a687239bed..59321970eb3 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -373,7 +373,8 @@ 'mautic.transport.momentum.adapter' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\Adapter::class, 'arguments' => [ - 'mautic.transport.momentum.sparkpost', + '%mautic.momentum_host%', + '%mautic.momentum_api_key%', ], ], 'mautic.transport.momentum.service.swift_message' => [ @@ -410,12 +411,14 @@ ], 'methodCalls' => [ 'setOptions' => [ - 'host' => '%mautic.momentum_host%', - 'protocol' => '%mautic.momentum_protocol%', - 'port' => '%mautic.momentum_port%', - 'key' => '%mautic.momentum_api_key%', - 'version' => '%mautic.momentum_version%', - 'async' => '%mautic.momentum_async%', + [ + 'host' => '%mautic.momentum_host%', + 'protocol' => '%mautic.momentum_protocol%', + 'port' => '%mautic.momentum_port%', + 'key' => '%mautic.momentum_api_key%', + 'version' => '%mautic.momentum_version%', + 'async' => '%mautic.momentum_async%', + ], ], ], ], diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php index 6f3a04ba72f..67c2139fc8e 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php @@ -3,7 +3,6 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Adapter; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; -use SparkPost\SparkPost; use SparkPost\SparkPostPromise; /** @@ -12,18 +11,25 @@ final class Adapter implements AdapterInterface { /** - * @var SparkPost + * @var string */ - private $sparkpost; + private $host; /** - * MomentumAdapter constructor. + * @var string + */ + private $apiKey; + + /** + * Adapter constructor. * - * @param SparkPost $momentumSparkpost + * @param string $host + * @param string $apiKey */ - public function __construct(Sparkpost $momentumSparkpost) + public function __construct($host, $apiKey) { - $this->sparkpost = $momentumSparkpost; + $this->host = $host; + $this->apiKey = $apiKey; } /** @@ -33,9 +39,18 @@ public function __construct(Sparkpost $momentumSparkpost) */ public function createTransmission(TransmissionDTO $transmissionDTO) { - dump($this->sparkpost); + $curl = curl_init(); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($transmissionDTO)); + curl_setopt($curl, CURLOPT_URL, 'http://'.$this->host.'/v1/transmissions'); + $headers = [ + 'Content-Type' => 'application/json', + 'Authorization' => $this->apiKey, + ]; + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + $result = curl_exec($curl); + curl_close($curl); + echo $result; exit; - - return $this->sparkpost->transmissions->post(json_encode($transmissionDTO)); } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php index a078148b68b..d1c2e61bbc4 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php @@ -3,6 +3,7 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\DTO; use Mautic\EmailBundle\Helper\PlainTextMassageHelper; +use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\RecipientDTO; /** * Class MomentumMessage. @@ -105,20 +106,7 @@ private function setHeaders(\Swift_Mime_Message $message) private function setRecipients(\Swift_Mime_Message $message) { foreach ($message->getTo() as $email => $name) { - $recipient = [ - 'address' => $email, - 'substitution_data' => [], - 'metadata' => [], - ]; - - // Apparently Sparkpost (legacy from Sparkpost implementation) doesn't like empty substitution_data or metadata - if (empty($recipient['substitution_data'])) { - unset($recipient['substitution_data']); - } - if (empty($recipient['metadata'])) { - unset($recipient['metadata']); - } - + $recipient = new RecipientDTO($email); $this->recipients[] = $recipient; } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php index 3fea3a4ea8e..7344b48733a 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php @@ -4,7 +4,7 @@ use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\ContentDTO; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\OptionsDTO; -use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\RecipientsDTO; +use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\RecipientDTO; /** * Class Mail. @@ -17,9 +17,9 @@ final class TransmissionDTO implements \JsonSerializable private $options = null; /** - * @var RecipientsDTO + * @var RecipientDTO[] */ - private $recipients; + private $recipients = []; /** * @var string|null @@ -54,17 +54,29 @@ final class TransmissionDTO implements \JsonSerializable /** * TransmissionDTO constructor. * - * @param RecipientsDTO $recipients * @param ContentDTO $content + * @param string $returnPath * @param OptionsDTO|null $options */ - public function __construct(RecipientsDTO $recipients, ContentDTO $content, OptionsDTO $options = null) + public function __construct(ContentDTO $content, $returnPath, OptionsDTO $options = null) { - $this->recipients = $recipients; $this->content = $content; + $this->returnPath = $returnPath; $this->options = $options; } + /** + * @param RecipientDTO $recipientDTO + * + * @return TransmissionDTO + */ + public function addRecipient(RecipientDTO $recipientDTO) + { + $this->recipients[] = $recipientDTO; + + return $this; + } + /** * @return mixed */ @@ -72,11 +84,11 @@ public function jsonSerialize() { $json = [ 'return_path' => $this->returnPath, - 'recipients' => json_encode($this->recipients), - 'content' => json_encode($this->content), + 'recipients' => $this->recipients, + 'content' => $this->content, ]; if ($this->options !== null) { - $json['options'] = json_encode($this->options); + $json['options'] = $this->options; } if ($this->campaignId !== null) { $json['campaign_id'] = $this->campaignId; diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php similarity index 77% rename from app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php rename to app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php index 4d06975e827..c192b364aa8 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php @@ -2,12 +2,10 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; -use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\RecipientsDTO\AddressDTO; - /** - * Class RecipientsDTO. + * Class RecipientDTO. */ -final class RecipientsDTO implements \JsonSerializable +final class RecipientDTO implements \JsonSerializable { /** * @var string|null @@ -15,9 +13,9 @@ final class RecipientsDTO implements \JsonSerializable private $returnPath = null; /** - * @var array + * @var string */ - private $addresses = []; + private $address; /** * @var array @@ -35,25 +33,23 @@ final class RecipientsDTO implements \JsonSerializable private $substitutionData = []; /** - * @param null|string $returnPath + * RecipientsDTO constructor. * - * @return RecipientsDTO + * @param $address */ - public function setReturnPath($returnPath) + public function __construct($address) { - $this->returnPath = $returnPath; - - return $this; + $this->address = $address; } /** - * @param AddressDTO $address + * @param null|string $returnPath * - * @return RecipientsDTO + * @return RecipientDTO */ - public function addAddress(AddressDTO $address) + public function setReturnPath($returnPath) { - $this->addresses[] = $address; + $this->returnPath = $returnPath; return $this; } @@ -62,7 +58,7 @@ public function addAddress(AddressDTO $address) * @param string $key * @param string $value * - * @return RecipientsDTO + * @return RecipientDTO */ public function addTag($key, $value) { @@ -75,7 +71,7 @@ public function addTag($key, $value) * @param string $key * @param mixed $value * - * @return RecipientsDTO + * @return RecipientDTO */ public function addMetadata($key, $value) { @@ -103,7 +99,7 @@ public function addSubstitutionData($key, $value) public function jsonSerialize() { $json = [ - 'address' => $this->addresses, + 'address' => $this->address, ]; if (count($this->tags) !== 0) { $json['tags'] = $this->tags; diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php deleted file mode 100644 index 82d427af596..00000000000 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientsDTO/AddressDTO.php +++ /dev/null @@ -1,65 +0,0 @@ -email = $email; - if ($isBcc === false) { - $this->headerTo = $email; - } - } - - /** - * @param null|string $name - * - * @return AddressDTO - */ - public function setName($name = null) - { - $this->name = $name; - - return $this; - } - - /** - * @return mixed - */ - public function jsonSerialize() - { - $json = [ - 'email' => $this->email, - ]; - if ($this->name !== null) { - $json['name'] = $this->name; - } - - return $json; - } -} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index a59eef1e1bb..51aadedc050 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -66,6 +66,8 @@ public function send(\Swift_Mime_Message $message) } } } catch (\Exception $exception) { + dump($exception); + exit; if ($exception instanceof SwiftMessageValidationException) { throw $exception; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 6ec6af3ce48..75da8d92317 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -34,11 +34,6 @@ public function __construct( */ public function transformToTransmission(\Swift_Mime_Message $message) { - $recipients = new TransmissionDTO\RecipientsDTO(); - foreach ($message->getTo() as $email => $name) { - $address = new TransmissionDTO\RecipientsDTO\AddressDTO($email); - $recipients->addAddress($address); - } $messageFrom = $message->getFrom(); $messageFromEmail = current(array_keys($messageFrom)); $from = new TransmissionDTO\ContentDTO\FromDTO($messageFromEmail); @@ -60,7 +55,10 @@ public function transformToTransmission(\Swift_Mime_Message $message) if (!empty($messageText)) { $content->setText($messageText); } - $transmission = new TransmissionDTO($recipients, $content); + $transmission = new TransmissionDTO($content, 'noreply@mautic.com'); + foreach ($message->getTo() as $email => $name) { + $transmission->addRecipient(new TransmissionDTO\RecipientDTO($email)); + } return $transmission; } From 2f5b11a8165c2652ea3109590039ba1b52b6b6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 4 May 2018 16:51:58 +0200 Subject: [PATCH 334/778] Fix email send and remove some swiftmailer headers --- .../EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php | 7 +++---- .../Momentum/DTO/TransmissionDTO/ContentDTO.php | 7 +++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php index 67c2139fc8e..9846d2274a3 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php @@ -43,10 +43,9 @@ public function createTransmission(TransmissionDTO $transmissionDTO) curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($transmissionDTO)); curl_setopt($curl, CURLOPT_URL, 'http://'.$this->host.'/v1/transmissions'); - $headers = [ - 'Content-Type' => 'application/json', - 'Authorization' => $this->apiKey, - ]; + $headers = []; + $headers[] = 'Content-Type: application/json'; + $headers[] = 'Authorization: '.$this->apiKey; curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); $result = curl_exec($curl); curl_close($curl); diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php index 7a84a868e58..78bfa7a4495 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php @@ -83,6 +83,13 @@ public function setText($text) */ public function addHeader($key, $value) { + if (in_array($key, [ + 'Content-Transfer-Encoding', + 'MIME-Version', + 'Subject', + ])) { + return $this; + } $this->headers[$key] = $value; return $this; From 3b3fd6893a9cbacf508b5707258684ca0933128f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 4 May 2018 16:53:28 +0200 Subject: [PATCH 335/778] Move headers conditions to service from DTO --- .../Momentum/DTO/TransmissionDTO/ContentDTO.php | 7 ------- .../Swiftmailer/Momentum/Service/SwiftMessageService.php | 7 ++++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php index 78bfa7a4495..7a84a868e58 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php @@ -83,13 +83,6 @@ public function setText($text) */ public function addHeader($key, $value) { - if (in_array($key, [ - 'Content-Transfer-Encoding', - 'MIME-Version', - 'Subject', - ])) { - return $this; - } $this->headers[$key] = $value; return $this; diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 75da8d92317..683d138e201 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -47,7 +47,12 @@ public function transformToTransmission(\Swift_Mime_Message $message) $headers = $message->getHeaders()->getAll(); /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && + !in_array($header->getFieldType(), [ + 'Content-Transfer-Encoding', + 'MIME-Version', + 'Subject', + ])) { $content->addHeader($header->getFieldName(), $header->getFieldBodyModel()); } } From b24bfdb1cbe29761581695d032763aced37ff890 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 4 May 2018 16:50:38 -0500 Subject: [PATCH 336/778] Support configuring transports to be injectable into Mautic's configuration --- app/bundles/EmailBundle/Config/config.php | 24 +- .../Compiler/EmailTransportPass.php | 45 +++ .../EmailBundle/Form/Type/ConfigType.php | 59 ++-- app/bundles/EmailBundle/MauticEmailBundle.php | 2 + .../EmailBundle/Model/TransportType.php | 257 ++++++++++++++---- 5 files changed, 293 insertions(+), 94 deletions(-) create mode 100644 app/bundles/EmailBundle/DependencyInjection/Compiler/EmailTransportPass.php diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 59321970eb3..346869f3e60 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -369,12 +369,18 @@ 'mautic.transport.momentum.callback', 'mautic.transport.momentum.facade', ], + 'tag' => 'mautic.email_transport', + 'tagArguments' => [ + \Mautic\EmailBundle\Model\TransportType::TRANSPORT_ALIAS => 'mautic.email.config.mailer_transport.momentum', + \Mautic\EmailBundle\Model\TransportType::FIELD_HOST => true, + \Mautic\EmailBundle\Model\TransportType::FIELD_API_KEY => true, + ], ], 'mautic.transport.momentum.adapter' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\Adapter::class, 'arguments' => [ - '%mautic.momentum_host%', - '%mautic.momentum_api_key%', + '%mautic.mailer_host%', + '%mautic.mailer_api_key%', ], ], 'mautic.transport.momentum.service.swift_message' => [ @@ -407,17 +413,17 @@ 'class' => \SparkPost\SparkPost::class, 'factory' => ['@mautic.sparkpost.factory', 'create'], 'arguments' => [ - '%mautic.momentum_api_key%', + '%mautic.mailer_api_key%', ], 'methodCalls' => [ 'setOptions' => [ [ - 'host' => '%mautic.momentum_host%', - 'protocol' => '%mautic.momentum_protocol%', - 'port' => '%mautic.momentum_port%', - 'key' => '%mautic.momentum_api_key%', - 'version' => '%mautic.momentum_version%', - 'async' => '%mautic.momentum_async%', + 'host' => '%mautic.mailer_host%', + //'protocol' => '%mautic.momentum_protocol%', + //'port' => '%mautic.momentum_port%', + 'key' => '%mautic.mailer_api_key%', + //'version' => '%mautic.momentum_version%', + //'async' => '%mautic.momentum_async%', ], ], ], diff --git a/app/bundles/EmailBundle/DependencyInjection/Compiler/EmailTransportPass.php b/app/bundles/EmailBundle/DependencyInjection/Compiler/EmailTransportPass.php new file mode 100644 index 00000000000..f6042263ef9 --- /dev/null +++ b/app/bundles/EmailBundle/DependencyInjection/Compiler/EmailTransportPass.php @@ -0,0 +1,45 @@ +has('mautic.email.transport_type')) { + return; + } + + $definition = $container->getDefinition('mautic.email.transport_type'); + $taggedServices = $container->findTaggedServiceIds('mautic.email_transport'); + foreach ($taggedServices as $id => $tags) { + $definition->addMethodCall('addTransport', [ + $id, + !empty($tags[0][TransportType::TRANSPORT_ALIAS]) ? $tags[0][TransportType::TRANSPORT_ALIAS] : $id, + !empty($tags[0][TransportType::FIELD_HOST]), + !empty($tags[0][TransportType::FIELD_PORT]), + !empty($tags[0][TransportType::FIELD_USER]), + !empty($tags[0][TransportType::FIELD_PASSWORD]), + !empty($tags[0][TransportType::FIELD_API_KEY]), + ]); + } + } +} diff --git a/app/bundles/EmailBundle/Form/Type/ConfigType.php b/app/bundles/EmailBundle/Form/Type/ConfigType.php index d5bfceb0f96..0b0c72f4597 100644 --- a/app/bundles/EmailBundle/Form/Type/ConfigType.php +++ b/app/bundles/EmailBundle/Form/Type/ConfigType.php @@ -284,9 +284,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $smtpServiceShowConditions = '{"config_emailconfig_mailer_transport":['.$this->transportType->getSmtpService().']}'; - $amazonRegionShowConditions = '{"config_emailconfig_mailer_transport":['.$this->transportType->getAmazonService().']}'; - $builder->add( 'mailer_host', 'text', @@ -295,7 +292,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label_attr' => ['class' => 'control-label'], 'attr' => [ 'class' => 'form-control', - 'data-show-on' => $smtpServiceShowConditions, + 'data-show-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresHost().']}', 'tooltip' => 'mautic.email.config.mailer.host.tooltip', 'onchange' => 'Mautic.disableSendTestEmailButton()', ], @@ -316,7 +313,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, 'attr' => [ 'class' => 'form-control', - 'data-show-on' => $amazonRegionShowConditions, + 'data-show-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getAmazonService().']}', 'tooltip' => 'mautic.email.config.mailer.amazon_host.tooltip', 'onchange' => 'Mautic.disableSendTestEmailButton()', ], @@ -332,7 +329,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label_attr' => ['class' => 'control-label'], 'attr' => [ 'class' => 'form-control', - 'data-show-on' => $smtpServiceShowConditions, + 'data-show-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresPort().']}', 'tooltip' => 'mautic.email.config.mailer.port.tooltip', 'onchange' => 'Mautic.disableSendTestEmailButton()', ], @@ -340,6 +337,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); + $smtpServiceShowConditions = '{"config_emailconfig_mailer_transport":['.$this->transportType->getSmtpService().']}'; $builder->add( 'mailer_auth_mode', 'choice', @@ -362,30 +360,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $mailerLoginUserShowConditions = '{ - "config_emailconfig_mailer_auth_mode":[ - "plain", - "login", - "cram-md5" - ], "config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresLogin().'] - }'; - - $mailerLoginPasswordShowConditions = '{ - "config_emailconfig_mailer_auth_mode":[ - "plain", - "login", - "cram-md5" - ], "config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresPassword().'] - }'; - - $mailerLoginUserHideConditions = '{ - "config_emailconfig_mailer_transport":['.$this->transportType->getServiceDoNotNeedLogin().'] - }'; - - $mailerLoginPasswordHideConditions = '{ - "config_emailconfig_mailer_transport":['.$this->transportType->getServiceDoNotNeedPassword().'] - }'; - $builder->add( 'mailer_user', 'text', @@ -394,8 +368,15 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label_attr' => ['class' => 'control-label'], 'attr' => [ 'class' => 'form-control', - 'data-show-on' => $mailerLoginUserShowConditions, - 'data-hide-on' => $mailerLoginUserHideConditions, + 'data-show-on' => '{ + "config_emailconfig_mailer_auth_mode":[ + "plain", + "login", + "cram-md5" + ], + "config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresUser().'] + }', + 'data-hide-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getServiceDoNotNeedUser().']}', 'tooltip' => 'mautic.email.config.mailer.user.tooltip', 'onchange' => 'Mautic.disableSendTestEmailButton()', 'autocomplete' => 'off', @@ -414,8 +395,15 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => 'form-control', 'placeholder' => 'mautic.user.user.form.passwordplaceholder', 'preaddon' => 'fa fa-lock', - 'data-show-on' => $mailerLoginPasswordShowConditions, - 'data-hide-on' => $mailerLoginPasswordHideConditions, + 'data-show-on' => '{ + "config_emailconfig_mailer_auth_mode":[ + "plain", + "login", + "cram-md5" + ], + "config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresPassword().'] + }', + 'data-hide-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getServiceDoNotNeedPassword().']}', 'tooltip' => 'mautic.email.config.mailer.password.tooltip', 'autocomplete' => 'off', 'onchange' => 'Mautic.disableSendTestEmailButton()', @@ -424,7 +412,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $apiKeyShowConditions = '{"config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresApiKey().']}'; $builder->add( 'mailer_api_key', 'password', @@ -433,7 +420,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label_attr' => ['class' => 'control-label'], 'attr' => [ 'class' => 'form-control', - 'data-show-on' => $apiKeyShowConditions, + 'data-show-on' => '{"config_emailconfig_mailer_transport":['.$this->transportType->getServiceRequiresApiKey().']}', 'tooltip' => 'mautic.email.config.mailer.apikey.tooltop', 'autocomplete' => 'off', 'placeholder' => 'mautic.email.config.mailer.apikey.placeholder', diff --git a/app/bundles/EmailBundle/MauticEmailBundle.php b/app/bundles/EmailBundle/MauticEmailBundle.php index 11badeec775..0029e341ca5 100644 --- a/app/bundles/EmailBundle/MauticEmailBundle.php +++ b/app/bundles/EmailBundle/MauticEmailBundle.php @@ -11,6 +11,7 @@ namespace Mautic\EmailBundle; +use Mautic\EmailBundle\DependencyInjection\Compiler\EmailTransportPass; use Mautic\EmailBundle\DependencyInjection\Compiler\RealTransportCompiler; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -28,5 +29,6 @@ public function build(ContainerBuilder $container) parent::build($container); $container->addCompilerPass(new RealTransportCompiler()); + $container->addCompilerPass(new EmailTransportPass()); } } diff --git a/app/bundles/EmailBundle/Model/TransportType.php b/app/bundles/EmailBundle/Model/TransportType.php index a262e07f45a..0f29c3fddad 100644 --- a/app/bundles/EmailBundle/Model/TransportType.php +++ b/app/bundles/EmailBundle/Model/TransportType.php @@ -4,82 +4,241 @@ class TransportType { + const TRANSPORT_ALIAS = 'transport_alias'; + + const FIELD_HOST = 'field_host'; + const FIELD_PORT = 'field_port'; + const FIELD_USER = 'field_user'; + const FIELD_PASSWORD = 'field_password'; + const FIELD_API_KEY = 'field_api_key'; + + /** + * @var array + */ + private $transportTypes = [ + 'mautic.transport.amazon' => 'mautic.email.config.mailer_transport.amazon', + 'mautic.transport.elasticemail' => 'mautic.email.config.mailer_transport.elasticemail', + 'gmail' => 'mautic.email.config.mailer_transport.gmail', + 'mautic.transport.mandrill' => 'mautic.email.config.mailer_transport.mandrill', + 'mautic.transport.mailjet' => 'mautic.email.config.mailer_transport.mailjet', + 'smtp' => 'mautic.email.config.mailer_transport.smtp', + 'mail' => 'mautic.email.config.mailer_transport.mail', + 'mautic.transport.postmark' => 'mautic.email.config.mailer_transport.postmark', + 'mautic.transport.sendgrid' => 'mautic.email.config.mailer_transport.sendgrid', + 'mautic.transport.sendgrid_api' => 'mautic.email.config.mailer_transport.sendgrid_api', + 'sendmail' => 'mautic.email.config.mailer_transport.sendmail', + 'mautic.transport.sparkpost' => 'mautic.email.config.mailer_transport.sparkpost', + ]; + + /** + * @var array + */ + private $showHost = [ + 'smtp', + ]; + + /** + * @var array + */ + private $showPort = [ + 'smtp', + ]; + + /** + * @var array + */ + private $showUser = [ + 'mautic.transport.mailjet', + 'mautic.transport.sendgrid', + 'mautic.transport.elasticemail', + 'mautic.transport.amazon', + 'mautic.transport.postmark', + 'gmail', + // smtp is left out on purpose as the auth_mode will manage displaying this field + ]; + + /** + * @var array + */ + private $showPassword = [ + 'mautic.transport.mailjet', + 'mautic.transport.sendgrid', + 'mautic.transport.elasticemail', + 'mautic.transport.amazon', + 'mautic.transport.postmark', + 'gmail', + // smtp is left out on purpose as the auth_mode will manage displaying this field + ]; + + /** + * @var array + */ + private $showApiKey = [ + 'mautic.transport.sparkpost', + 'mautic.transport.mandrill', + 'mautic.transport.sendgrid_api', + ]; + + /** + * @param $serviceId + * @param $translatableAlias + * @param $showHost + * @param $showPort + * @param $showUser + * @param $showPassword + * @param $showApiKey + */ + public function addTransport($serviceId, $translatableAlias, $showHost, $showPort, $showUser, $showPassword, $showApiKey) + { + $this->transportTypes[$serviceId] = $translatableAlias; + + if ($showHost) { + $this->showHost[] = $serviceId; + } + + if ($showPort) { + $this->showPort[] = $serviceId; + } + + if ($showUser) { + $this->showUser[] = $serviceId; + } + + if ($showPassword) { + $this->showPassword[] = $serviceId; + } + + if ($showApiKey) { + $this->showApiKey[] = $serviceId; + } + } + + /** + * @return array + */ public function getTransportTypes() { - return [ - 'mautic.transport.amazon' => 'mautic.email.config.mailer_transport.amazon', - 'mautic.transport.elasticemail' => 'mautic.email.config.mailer_transport.elasticemail', - 'gmail' => 'mautic.email.config.mailer_transport.gmail', - 'mautic.transport.mandrill' => 'mautic.email.config.mailer_transport.mandrill', - 'mautic.transport.mailjet' => 'mautic.email.config.mailer_transport.mailjet', - 'smtp' => 'mautic.email.config.mailer_transport.smtp', - 'mail' => 'mautic.email.config.mailer_transport.mail', - 'mautic.transport.postmark' => 'mautic.email.config.mailer_transport.postmark', - 'mautic.transport.sendgrid' => 'mautic.email.config.mailer_transport.sendgrid', - 'mautic.transport.sendgrid_api' => 'mautic.email.config.mailer_transport.sendgrid_api', - 'sendmail' => 'mautic.email.config.mailer_transport.sendmail', - 'mautic.transport.sparkpost' => 'mautic.email.config.mailer_transport.sparkpost', - 'mautic.transport.momentum' => 'mautic.email.config.mailer_transport.momentum', - ]; + return $this->transportTypes; } + /** + * @return string + */ + public function getServiceRequiresHost() + { + return $this->getString($this->showHost); + } + + /** + * @return string + */ + public function getServiceRequiresPort() + { + return $this->getString($this->showPort); + } + + /** + * @return string + */ + public function getServiceRequiresUser() + { + return $this->getString($this->showUser); + } + + /** + * @return string + */ + public function getServiceDoNotNeedUser() + { + // The auth_mode data-show-on will handle smtp + $tempTransports = $this->transportTypes; + unset($tempTransports['smtp']); + + $transports = array_keys($tempTransports); + $doNotRequireUser = array_diff($transports, $this->showUser); + + return $this->getString($doNotRequireUser); + } + + public function getServiceDoNotNeedPassword() + { + // The auth_mode data-show-on will handle smtp + $tempTransports = $this->transportTypes; + unset($tempTransports['smtp']); + + $transports = array_keys($tempTransports); + $doNotRequireUser = array_diff($transports, $this->showPassword); + + return $this->getString($doNotRequireUser); + } + + /** + * @return string + */ + public function getServiceRequiresPassword() + { + return $this->getString($this->showPassword); + } + + /** + * @return string + */ + public function getServiceRequiresApiKey() + { + return $this->getString($this->showApiKey); + } + + /** + * @return string + */ public function getSmtpService() { return '"smtp"'; } + /** + * @return string + */ public function getAmazonService() { return '"mautic.transport.amazon"'; } + /** + * @return string + */ public function getMailjetService() { return '"mautic.transport.mailjet"'; } + /** + * @deprecated 2.14.0 to be removed in 3.0 + * + * @return string + */ public function getServiceRequiresLogin() { - return '"mautic.transport.mandrill", - "mautic.transport.mailjet", - "mautic.transport.sendgrid", - "mautic.transport.elasticemail", - "mautic.transport.amazon", - "mautic.transport.postmark", - "gmail"'; + return $this->getServiceRequiresUser(); } + /** + * @deprecated 2.14.0 to be removed in 3.0 + * + * @return string + */ public function getServiceDoNotNeedLogin() { - return '"mail", - "sendmail", - "mautic.transport.sparkpost", - "mautic.transport.sendgrid_api"'; + return $this->getServiceDoNotNeedUser(); } - public function getServiceRequiresPassword() - { - return '"mautic.transport.elasticemail", - "mautic.transport.sendgrid", - "mautic.transport.amazon", - "mautic.transport.postmark", - "mautic.transport.mailjet", - "gmail"'; - } - - public function getServiceDoNotNeedPassword() - { - return '"mail", - "sendmail", - "mautic.transport.sparkpost", - "mautic.transport.mandrill", - "mautic.transport.sendgrid_api"'; - } - - public function getServiceRequiresApiKey() + /** + * @param array $services + * + * @return string + */ + private function getString(array $services) { - return '"mautic.transport.sparkpost", - "mautic.transport.mandrill", - "mautic.transport.sendgrid_api"'; + return '"'.implode('","', $services).'"'; } } From 023cb68f16d758278a404aaeecc87de915df8bff Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 4 May 2018 16:55:34 -0500 Subject: [PATCH 337/778] Prevent error "Error while validating header Content-Transfer-Encoding: Reserved header name" --- .../Swiftmailer/Momentum/Service/SwiftMessageService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 683d138e201..a10918ca9c2 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -45,10 +45,11 @@ public function transformToTransmission(\Swift_Mime_Message $message) $content->setHtml($message->getBody()); } $headers = $message->getHeaders()->getAll(); + /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && - !in_array($header->getFieldType(), [ + !in_array($header->getFieldName(), [ 'Content-Transfer-Encoding', 'MIME-Version', 'Subject', From 5f04ac46a42564faa86310aff1aa1764de37a981 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 4 May 2018 17:06:48 -0500 Subject: [PATCH 338/778] Ensure email transport tags have required service alias --- .../DependencyInjection/MauticCoreExtension.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/bundles/CoreBundle/DependencyInjection/MauticCoreExtension.php b/app/bundles/CoreBundle/DependencyInjection/MauticCoreExtension.php index 69aae9e5096..2040a632daa 100644 --- a/app/bundles/CoreBundle/DependencyInjection/MauticCoreExtension.php +++ b/app/bundles/CoreBundle/DependencyInjection/MauticCoreExtension.php @@ -147,6 +147,10 @@ public function load(array $configs, ContainerBuilder $container) } $definition->addTag($tag, $tagArguments[$k]); + + if ('mautic.email_transport' === $tag) { + $container->setAlias(sprintf('swiftmailer.mailer.transport.%s', $name), $name); + } } } else { $tag = (!empty($details['tag'])) ? $details['tag'] : $defaultTag; @@ -158,6 +162,10 @@ public function load(array $configs, ContainerBuilder $container) } $definition->addTag($tag, $tagArguments); + + if ('mautic.email_transport' === $tag) { + $container->setAlias(sprintf('swiftmailer.mailer.transport.%s', $name), $name); + } } if ($type == 'events') { From c7a6ed0dfde34cb8417744348418c2b55421839c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 4 May 2018 19:01:29 -0500 Subject: [PATCH 339/778] Added metadata and substitution_data to transmission --- .../Momentum/DTO/TransmissionDTO.php | 16 --- .../DTO/TransmissionDTO/RecipientDTO.php | 12 +- .../Momentum/Metadata/MetadataProcessor.php | 120 ++++++++++++++++++ .../Momentum/Service/SwiftMessageService.php | 27 +++- 4 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php index 7344b48733a..319caae1481 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO.php @@ -31,16 +31,6 @@ final class TransmissionDTO implements \JsonSerializable */ private $description = null; - /** - * @var array - */ - private $metadata = []; - - /** - * @var array - */ - private $substitutionData = []; - /** * @var string */ @@ -96,12 +86,6 @@ public function jsonSerialize() if ($this->description !== null) { $json['description'] = $this->description; } - if (count($this->metadata) !== 0) { - $json['metadata'] = $this->metadata; - } - if (count($this->substitutionData) !== 0) { - $json['substitution_data'] = $this->substitutionData; - } return $json; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php index c192b364aa8..b11bf293fb3 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php @@ -33,13 +33,17 @@ final class RecipientDTO implements \JsonSerializable private $substitutionData = []; /** - * RecipientsDTO constructor. + * RecipientDTO constructor. * - * @param $address + * @param $address + * @param array $metadata + * @param array $substitutionData */ - public function __construct($address) + public function __construct($address, $metadata = [], $substitutionData = []) { - $this->address = $address; + $this->address = $address; + $this->metadata = $metadata; + $this->substitutionData = $substitutionData; } /** diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php new file mode 100644 index 00000000000..be80ffe8e2a --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php @@ -0,0 +1,120 @@ +message = $message; + + $metadata = ($message instanceof MauticMessage) ? $message->getMetadata() : []; + $this->metadata = $metadata; + + // Build the substitution merge vars + $this->buildSubstitutionData(); + + if (count($this->substitutionKeys)) { + // Update the content with the substitution merge vars + MailHelper::searchReplaceTokens($this->substitutionKeys, $this->substitutionMergeVars, $this->message); + } + } + + /** + * @param $email + * + * @return array|mixed + */ + public function getMetadata($email) + { + if (!isset($this->metadata[$email])) { + return []; + } + + $metadata = $this->metadata[$email]; + + // remove the tokens as they'll be part of the substitution data + unset($metadata['tokens']); + + return $metadata; + } + + /** + * @param $email + * + * @return array + */ + public function getSubstitutionData($email) + { + if (!isset($this->metadata[$email])) { + return []; + } + + $substitutionData = []; + foreach ($this->metadata[$email]['tokens'] as $token => $value) { + $substitutionData[$this->substitutionMergeVars[$token]] = $value; + } + + return $substitutionData; + } + + private function buildSubstitutionData() + { + // Sparkpost uses {{ name }} for tokens so Mautic's need to be converted; although using their {{{ }}} syntax to prevent HTML escaping + $metadataSet = reset($this->metadata); + $tokens = (!empty($metadataSet['tokens'])) ? $metadataSet['tokens'] : []; + $mauticTokens = array_keys($tokens); + + foreach ($mauticTokens as $token) { + $this->substitutionKeys[$token] = strtoupper(preg_replace('/[^a-z0-9]+/i', '', $token)); + $this->substitutionMergeVars[$token] = '{{{ '.$this->substitutionKeys[$token].' }}}'; + } + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index a10918ca9c2..1f594f46b92 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -4,6 +4,7 @@ use Mautic\EmailBundle\Helper\PlainTextMassageHelper; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; +use Mautic\EmailBundle\Swiftmailer\Momentum\Metadata\MetadataProcessor; use Symfony\Component\Translation\TranslatorInterface; /** @@ -41,11 +42,14 @@ public function transformToTransmission(\Swift_Mime_Message $message) $from->setName($messageFrom[$messageFromEmail]); } $content = new TransmissionDTO\ContentDTO($message->getSubject(), $from); - if (!empty($message->getBody())) { - $content->setHtml($message->getBody()); + + $metadataProcessor = new MetadataProcessor($message); + + if ($body = $message->getBody()) { + $content->setHtml($body); } - $headers = $message->getHeaders()->getAll(); + $headers = $message->getHeaders()->getAll(); /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && @@ -57,13 +61,22 @@ public function transformToTransmission(\Swift_Mime_Message $message) $content->addHeader($header->getFieldName(), $header->getFieldBodyModel()); } } - $messageText = PlainTextMassageHelper::getPlainTextFromMessage($message); - if (!empty($messageText)) { + + if ($messageText = PlainTextMassageHelper::getPlainTextFromMessage($message)) { $content->setText($messageText); } - $transmission = new TransmissionDTO($content, 'noreply@mautic.com'); + + $returnPath = $message->getReplyTo() ? $message->getReplyTo() : $messageFromEmail; + $transmission = new TransmissionDTO($content, $returnPath); + foreach ($message->getTo() as $email => $name) { - $transmission->addRecipient(new TransmissionDTO\RecipientDTO($email)); + $recipientDTO = new TransmissionDTO\RecipientDTO( + $email, + $metadataProcessor->getMetadata($email), + $metadataProcessor->getSubstitutionData($email) + ); + + $transmission->addRecipient($recipientDTO); } return $transmission; From 2644aa2f9e6324779f901f454cea01a1175f1c59 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 4 May 2018 19:22:59 -0500 Subject: [PATCH 340/778] Fixed return path --- .../Swiftmailer/Momentum/Service/SwiftMessageService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 1f594f46b92..420033cefdb 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -66,7 +66,7 @@ public function transformToTransmission(\Swift_Mime_Message $message) $content->setText($messageText); } - $returnPath = $message->getReplyTo() ? $message->getReplyTo() : $messageFromEmail; + $returnPath = $message->getReturnPath() ? $message->getReturnPath() : $messageFromEmail; $transmission = new TransmissionDTO($content, $returnPath); foreach ($message->getTo() as $email => $name) { From ba69757a8de8c7b98171c3b6590b3c6dc81d91f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Mon, 7 May 2018 16:43:13 +0200 Subject: [PATCH 341/778] Sparkpost library back in use and error handling --- app/bundles/EmailBundle/Config/config.php | 4 +- .../Swiftmailer/Momentum/Adapter/Adapter.php | 23 ++++----- .../Momentum/Facade/MomentumFacade.php | 48 ++++++++----------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 346869f3e60..4b3f3b60af8 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -379,8 +379,7 @@ 'mautic.transport.momentum.adapter' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\Adapter::class, 'arguments' => [ - '%mautic.mailer_host%', - '%mautic.mailer_api_key%', + 'mautic.transport.momentum.sparkpost', ], ], 'mautic.transport.momentum.service.swift_message' => [ @@ -407,6 +406,7 @@ 'mautic.transport.momentum.adapter', 'mautic.transport.momentum.service.swift_message', 'mautic.transport.momentum.validator.swift_message', + 'monolog.logger.mautic', ], ], 'mautic.transport.momentum.sparkpost' => [ diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php index 9846d2274a3..fe3818dbe17 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php @@ -3,6 +3,7 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Adapter; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; +use SparkPost\SparkPost; use SparkPost\SparkPostPromise; /** @@ -11,25 +12,18 @@ final class Adapter implements AdapterInterface { /** - * @var string + * @var SparkPost */ - private $host; - - /** - * @var string - */ - private $apiKey; + private $momentumSparkpost; /** * Adapter constructor. * - * @param string $host - * @param string $apiKey + * @param SparkPost $momentumSparkpost */ - public function __construct($host, $apiKey) + public function __construct(SparkPost $momentumSparkpost) { - $this->host = $host; - $this->apiKey = $apiKey; + $this->momentumSparkpost = $momentumSparkpost; } /** @@ -39,7 +33,8 @@ public function __construct($host, $apiKey) */ public function createTransmission(TransmissionDTO $transmissionDTO) { - $curl = curl_init(); + return $this->momentumSparkpost->transmissions->post(json_decode(json_encode($transmissionDTO), true)); + /*$curl = curl_init(); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($transmissionDTO)); curl_setopt($curl, CURLOPT_URL, 'http://'.$this->host.'/v1/transmissions'); @@ -50,6 +45,6 @@ public function createTransmission(TransmissionDTO $transmissionDTO) $result = curl_exec($curl); curl_close($curl); echo $result; - exit; + exit;*/ } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index 51aadedc050..541710b4008 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -3,11 +3,11 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Facade; use Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\AdapterInterface; -use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; use Mautic\EmailBundle\Swiftmailer\Momentum\Exception\Facade\MomentumSendException; use Mautic\EmailBundle\Swiftmailer\Momentum\Exception\Validator\SwiftMessageValidator\SwiftMessageValidationException; use Mautic\EmailBundle\Swiftmailer\Momentum\Service\SwiftMessageServiceInterface; use Mautic\EmailBundle\Swiftmailer\Momentum\Validator\SwiftMessageValidator\SwiftMessageValidatorInterface; +use Monolog\Logger; /** * Class MomentumApiFacade. @@ -29,21 +29,27 @@ final class MomentumFacade implements MomentumFacadeInterface */ private $swiftMessageValidator; + /** @var Logger */ + private $logger; + /** * MomentumFacade constructor. * * @param AdapterInterface $adapter * @param SwiftMessageServiceInterface $swiftMessageService * @param SwiftMessageValidatorInterface $swiftMessageValidator + * @param Logger $logger */ public function __construct( AdapterInterface $adapter, SwiftMessageServiceInterface $swiftMessageService, - SwiftMessageValidatorInterface $swiftMessageValidator + SwiftMessageValidatorInterface $swiftMessageValidator, + Logger $logger ) { $this->adapter = $adapter; $this->swiftMessageService = $swiftMessageService; $this->swiftMessageValidator = $swiftMessageValidator; + $this->logger = $logger; } /** @@ -59,38 +65,24 @@ public function send(\Swift_Mime_Message $message) $transmission = $this->swiftMessageService->transformToTransmission($message); $response = $this->adapter->createTransmission($transmission); $response = $response->wait(); - if (200 == (int) $response->getStatusCode()) { - $results = $response->getBody(); - if (!$sendCount = $results['results']['total_accepted_recipients']) { - $this->processResponseErrors($transmission, $results); - } + if (200 !== (int) $response->getStatusCode()) { + $this->logger->addError( + 'Momentum send: '.$response->getStatusCode(), + [ + 'response' => $response->getBody(), + ] + ); } } catch (\Exception $exception) { - dump($exception); - exit; + $this->logger->addError( + 'Momentum send exception', + [ + 'message' => $exception->getMessage(), + ]); if ($exception instanceof SwiftMessageValidationException) { throw $exception; } throw new MomentumSendException(); } } - - /** - * @param TransmissionDTO $transmissionDTO - * @param array $results - */ - private function processResponseErrors(TransmissionDTO $transmissionDTO, array $results) - { - /* - if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { - $comments = $response['errors'][0]['description']; - $emailAddress = $momentumMessage['recipients']['to'][0]['email']; - $metadata = $this->getMetadata(); - - if (isset($metadata[$emailAddress]) && isset($metadata[$emailAddress]['leadId'])) { - $emailId = (!empty($metadata[$emailAddress]['emailId'])) ? $metadata[$emailAddress]['emailId'] : null; - $this->transportCallback->addFailureByContactId($metadata[$emailAddress]['leadId'], $comments, DoNotContact::BOUNCED, $emailId); - } - }*/ - } } From 11000557ffc180f570e731b9062eb623af39013c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Mon, 7 May 2018 21:55:58 +0200 Subject: [PATCH 342/778] AddressDTO, AttachmentDTO, Sparkpost no-fix --- app/bundles/EmailBundle/Config/config.php | 13 +- .../Swiftmailer/Momentum/Adapter/Adapter.php | 4 +- .../Momentum/DTO/MomentumMessage.php | 113 ------------------ .../DTO/TransmissionDTO/ContentDTO.php | 21 ++++ .../ContentDTO/AttachementDTO.php | 50 ++++++++ .../DTO/TransmissionDTO/RecipientDTO.php | 14 ++- .../RecipientDTO/AddressDTO.php | 56 +++++++++ .../Momentum/Service/SwiftMessageService.php | 66 +++++++++- .../Sparkpost/SparkpostFactory.php | 18 ++- .../Sparkpost/SparkpostFactoryInterface.php | 3 +- 10 files changed, 217 insertions(+), 141 deletions(-) delete mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO/AddressDTO.php diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 4b3f3b60af8..f393d6e6734 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -413,20 +413,9 @@ 'class' => \SparkPost\SparkPost::class, 'factory' => ['@mautic.sparkpost.factory', 'create'], 'arguments' => [ + '%mautic.mailer_host%', '%mautic.mailer_api_key%', ], - 'methodCalls' => [ - 'setOptions' => [ - [ - 'host' => '%mautic.mailer_host%', - //'protocol' => '%mautic.momentum_protocol%', - //'port' => '%mautic.momentum_port%', - 'key' => '%mautic.mailer_api_key%', - //'version' => '%mautic.momentum_version%', - //'async' => '%mautic.momentum_async%', - ], - ], - ], ], 'mautic.transport.sendgrid' => [ 'class' => 'Mautic\EmailBundle\Swiftmailer\Transport\SendgridTransport', diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php index fe3818dbe17..0f761cf3eeb 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php @@ -33,7 +33,9 @@ public function __construct(SparkPost $momentumSparkpost) */ public function createTransmission(TransmissionDTO $transmissionDTO) { - return $this->momentumSparkpost->transmissions->post(json_decode(json_encode($transmissionDTO), true)); + $payload = json_decode(json_encode($transmissionDTO), true); + + return $this->momentumSparkpost->transmissions->post($payload); /*$curl = curl_init(); curl_setopt($curl, CURLOPT_POST, 1); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($transmissionDTO)); diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php deleted file mode 100644 index d1c2e61bbc4..00000000000 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/MomentumMessage.php +++ /dev/null @@ -1,113 +0,0 @@ -setContent($swiftMessage); - $this->setHeaders($swiftMessage); - $this->setRecipients($swiftMessage); - } - - /** - * @return mixed - */ - public function jsonSerialize() - { - return [ - 'content' => $this->content, - 'headers' => $this->headers, - 'recipients' => $this->recipients, - 'tags' => $this->tags, - ]; - } - - /** - * @param \Swift_Mime_Message $message - */ - private function setContent(\Swift_Mime_Message $message) - { - $from = $message->getFrom(); - $fromEmail = current(array_keys($from)); - $fromName = $from[$fromEmail]; - $this->content = [ - 'from' => (!empty($fromName) ? ($fromName.' <'.$fromEmail.'>') : $fromEmail), - 'subject' => $message->getSubject(), - ]; - if (!empty($message->getBody())) { - $this->content['html'] = $message->getBody(); - } - $messageText = PlainTextMassageHelper::getPlainTextFromMessage($message); - if (!empty($messageText)) { - $this->content['text'] = $messageText; - } - $encoder = new \Swift_Mime_ContentEncoder_Base64ContentEncoder(); - foreach ($message->getChildren() as $child) { - if ($child instanceof \Swift_Image) { - $this->content['inline_images'][] = [ - 'type' => $child->getContentType(), - 'name' => $child->getId(), - 'data' => $encoder->encodeString($child->getBody()), - ]; - } - } - } - - /** - * @param \Swift_Mime_Message $message - */ - private function setHeaders(\Swift_Mime_Message $message) - { - $headers = $message->getHeaders()->getAll(); - /** @var \Swift_Mime_Header $header */ - foreach ($headers as $header) { - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT) { - $this->headers[$header->getFieldName()] = $header->getFieldBodyModel(); - } - } - } - - /** - * @param \Swift_Mime_Message $message - */ - private function setRecipients(\Swift_Mime_Message $message) - { - foreach ($message->getTo() as $email => $name) { - $recipient = new RecipientDTO($email); - $this->recipients[] = $recipient; - } - } -} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php index 7a84a868e58..5916e955df2 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php @@ -2,6 +2,7 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; +use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\ContentDTO\AttachementDTO; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\ContentDTO\FromDTO; /** @@ -39,6 +40,11 @@ final class ContentDTO implements \JsonSerializable */ private $headers = []; + /** + * @var array + */ + private $attachments = []; + /** * ContentDTO constructor. * @@ -88,6 +94,18 @@ public function addHeader($key, $value) return $this; } + /** + * @param AttachementDTO $attachementDTO + * + * @return $this + */ + public function addAttachment(AttachementDTO $attachementDTO) + { + $this->attachments[] = $attachementDTO; + + return $this; + } + /** * @return array|mixed */ @@ -109,6 +127,9 @@ public function jsonSerialize() if (count($this->headers) !== 0) { $json['headers'] = $this->headers; } + if (count($this->attachments) !== 0) { + $json['attachments'] = $this->attachments; + } return $json; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php new file mode 100644 index 00000000000..054e605eb6a --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php @@ -0,0 +1,50 @@ +type = $type; + $this->name = $name; + $this->content = $content; + } + + /** + * @return mixed + */ + public function jsonSerialize() + { + return [ + 'type' => $this->type, + 'name' => $this->name, + 'content' => $this->content, + ]; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php index b11bf293fb3..33d630e0742 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO.php @@ -2,6 +2,8 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; +use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO\RecipientDTO\AddressDTO; + /** * Class RecipientDTO. */ @@ -13,7 +15,7 @@ final class RecipientDTO implements \JsonSerializable private $returnPath = null; /** - * @var string + * @var AddressDTO */ private $address; @@ -35,13 +37,13 @@ final class RecipientDTO implements \JsonSerializable /** * RecipientDTO constructor. * - * @param $address - * @param array $metadata - * @param array $substitutionData + * @param AddressDTO $addressDTO + * @param array $metadata + * @param array $substitutionData */ - public function __construct($address, $metadata = [], $substitutionData = []) + public function __construct(AddressDTO $addressDTO, $metadata = [], $substitutionData = []) { - $this->address = $address; + $this->address = $addressDTO; $this->metadata = $metadata; $this->substitutionData = $substitutionData; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO/AddressDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO/AddressDTO.php new file mode 100644 index 00000000000..fd3f95fbc96 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/RecipientDTO/AddressDTO.php @@ -0,0 +1,56 @@ +email = $email; + $this->name = $name; + if ($bcc === false) { + $this->headerTo = $email; + } + } + + /** + * @return array + */ + public function jsonSerialize() + { + $json = [ + 'email' => $this->email, + 'name' => $this->name, + ]; + if ($this->headerTo !== null) { + $json['header_to'] = $this->headerTo; + } + + return $json; + } +} diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 420033cefdb..145cd526eee 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -3,6 +3,7 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Service; use Mautic\EmailBundle\Helper\PlainTextMassageHelper; +use Mautic\EmailBundle\Swiftmailer\Message\MauticMessage; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; use Mautic\EmailBundle\Swiftmailer\Momentum\Metadata\MetadataProcessor; use Symfony\Component\Translation\TranslatorInterface; @@ -66,17 +67,70 @@ public function transformToTransmission(\Swift_Mime_Message $message) $content->setText($messageText); } + if ($message instanceof MauticMessage) { + foreach ($message->getAttachments() as $attachment) { + if (file_exists($attachment['filePath']) && is_readable($attachment['filePath'])) { + try { + $swiftAttachment = \Swift_Attachment::fromPath($attachment['filePath']); + + if (!empty($attachment['fileName'])) { + $swiftAttachment->setFilename($attachment['fileName']); + } + + if (!empty($attachment['contentType'])) { + $swiftAttachment->setContentType($attachment['contentType']); + } + + if (!empty($attachment['inline'])) { + $swiftAttachment->setDisposition('inline'); + } + $attachmentContent = $swiftAttachment->getEncoder()->encodeString($swiftAttachment->getBody()); + $attachment = new TransmissionDTO\ContentDTO\AttachementDTO( + $swiftAttachment->getContentType(), + $swiftAttachment->getFilename(), + $attachmentContent + ); + $content->addAttachment($attachment); + } catch (\Exception $e) { + error_log($e); + } + } + } + } + $returnPath = $message->getReturnPath() ? $message->getReturnPath() : $messageFromEmail; $transmission = new TransmissionDTO($content, $returnPath); + $ccRecipients = []; foreach ($message->getTo() as $email => $name) { - $recipientDTO = new TransmissionDTO\RecipientDTO( - $email, - $metadataProcessor->getMetadata($email), - $metadataProcessor->getSubstitutionData($email) - ); + $ccRecipients[$email] = $name; + } + if ($message->getCc() !== null) { + foreach ($message->getCc() as $email => $name) { + $ccRecipients[$email] = $name; + } + } + $bccRecipients = []; + if ($message->getBcc() !== null) { + $bccRecipients = $message->getBcc(); + } + + $recipientsGrouped = [ + 'cc' => $ccRecipients, + 'bcc' => $bccRecipients, + ]; + foreach ($recipientsGrouped as $group => $recipients) { + $isBcc = ($group === 'bcc'); + foreach ($recipients as $email => $name) { + $addressDTO = new TransmissionDTO\RecipientDTO\AddressDTO($email, $name, $isBcc); + $recipientDTO = new TransmissionDTO\RecipientDTO( + $addressDTO, + $metadataProcessor->getMetadata($email), + $metadataProcessor->getSubstitutionData($email) + ); - $transmission->addRecipient($recipientDTO); + $transmission->addRecipient($recipientDTO); + } } return $transmission; diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php index 37948d19ff1..211b9afd94b 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php @@ -26,12 +26,26 @@ public function __construct(GuzzleAdapter $client) } /** + * @param string $host * @param string $apiKey * * @return SparkPost */ - public function create($apiKey) + public function create($host, $apiKey) { - return new SparkPost($this->client, ['key' => $apiKey]); + if ((strpos($host, '://') === false && substr($host, 0, 1) != '/')) { + $host = 'https://'.$host; + } + $hostInfo = parse_url($host); + if ($hostInfo) { + return new SparkPost($this->client, [ + 'host' => $hostInfo['host'].$hostInfo['path'], + 'protocol' => $hostInfo['scheme'], + 'port' => $hostInfo['scheme'] === 'https' ? 443 : 80, + 'key' => $apiKey, + ]); + } else { + // problem :/ + } } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php index 127a880944d..2acf14e0417 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php @@ -10,9 +10,10 @@ interface SparkpostFactoryInterface { /** + * @param string $host * @param string $apiKey * * @return SparkPost */ - public function create($apiKey); + public function create($host, $apiKey); } From fb1e458d65f7428fa8dee11f11b9c375260a3cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Tue, 8 May 2018 14:51:32 +0200 Subject: [PATCH 343/778] Add inline css --- .../DTO/TransmissionDTO/ContentDTO.php | 20 +++++++++++++++++++ .../Momentum/Service/SwiftMessageService.php | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php index 5916e955df2..09ef8738bb3 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO.php @@ -25,6 +25,11 @@ final class ContentDTO implements \JsonSerializable */ private $html = null; + /** + * @var string|null + */ + private $inlineCss = null; + /** * @var string|null */ @@ -69,6 +74,18 @@ public function setHtml($html) return $this; } + /** + * @param string|null $inlineCss + * + * @return ContentDTO\ + */ + public function setInlineCss($inlineCss = null) + { + $this->inlineCss = $inlineCss; + + return $this; + } + /** * @param null|string $text * @@ -130,6 +147,9 @@ public function jsonSerialize() if (count($this->attachments) !== 0) { $json['attachments'] = $this->attachments; } + if ($this->inlineCss !== null) { + $json['inline_css'] = $this->inlineCss; + } return $json; } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 145cd526eee..cae4a011e72 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -97,6 +97,10 @@ public function transformToTransmission(\Swift_Mime_Message $message) } } } + $cssHeader = $message->getHeaders()->get('X-MC-InlineCSS'); + if ($cssHeader !== null) { + $content->setInlineCss($cssHeader); + } $returnPath = $message->getReturnPath() ? $message->getReturnPath() : $messageFromEmail; $transmission = new TransmissionDTO($content, $returnPath); From f628b001aba34e1730b486fc8e47e6950935f4a5 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 08:44:27 -0500 Subject: [PATCH 344/778] Allow custom port --- app/bundles/EmailBundle/Config/config.php | 2 ++ .../Sparkpost/SparkpostFactoryInterface.php | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index f393d6e6734..3f8caa7eb40 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -373,6 +373,7 @@ 'tagArguments' => [ \Mautic\EmailBundle\Model\TransportType::TRANSPORT_ALIAS => 'mautic.email.config.mailer_transport.momentum', \Mautic\EmailBundle\Model\TransportType::FIELD_HOST => true, + \Mautic\EmailBundle\Model\TransportType::FIELD_PORT => true, \Mautic\EmailBundle\Model\TransportType::FIELD_API_KEY => true, ], ], @@ -415,6 +416,7 @@ 'arguments' => [ '%mautic.mailer_host%', '%mautic.mailer_api_key%', + '%mautic.mailer_port%', ], ], 'mautic.transport.sendgrid' => [ diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php index 2acf14e0417..ecf8fd4c5f0 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactoryInterface.php @@ -2,18 +2,17 @@ namespace Mautic\EmailBundle\Swiftmailer\Sparkpost; -use SparkPost\SparkPost; - /** * Interface SparkpostFactoryInterface. */ interface SparkpostFactoryInterface { /** - * @param string $host - * @param string $apiKey + * @param $host + * @param $apiKey + * @param null $port * - * @return SparkPost + * @return mixed */ - public function create($host, $apiKey); + public function create($host, $apiKey, $port = null); } From 13c481b9d8abc4a754d1f921b19655c16a00163a Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 08:45:26 -0500 Subject: [PATCH 345/778] OCD move; alphabetize transports in the config --- .../EmailBundle/Form/Type/ConfigType.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Form/Type/ConfigType.php b/app/bundles/EmailBundle/Form/Type/ConfigType.php index 0b0c72f4597..9da0d30c993 100644 --- a/app/bundles/EmailBundle/Form/Type/ConfigType.php +++ b/app/bundles/EmailBundle/Form/Type/ConfigType.php @@ -227,7 +227,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mailer_transport', ChoiceType::class, [ - 'choices' => $this->transportType->getTransportTypes(), + 'choices' => $this->getTransportChoices(), 'label' => 'mautic.email.config.mailer.transport', 'required' => false, 'attr' => [ @@ -777,4 +777,20 @@ public function getName() { return 'emailconfig'; } + + /** + * @return array + */ + private function getTransportChoices() + { + $choices = $this->transportType->getTransportTypes(); + + foreach ($choices as $value => $label) { + $choices[$value] = $this->translator->trans($label); + } + + asort($choices, SORT_NATURAL); + + return $choices; + } } From 791f24bce16a829110629b42c6db26d448f0df05 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 08:47:09 -0500 Subject: [PATCH 346/778] Parse /api out of path as Sparkpost auto-appends that but use the path to set version if that is ever needed --- .../Sparkpost/SparkpostFactory.php | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php index 211b9afd94b..46c47cac13e 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php @@ -26,24 +26,47 @@ public function __construct(GuzzleAdapter $client) } /** - * @param string $host - * @param string $apiKey + * @param $host + * @param $apiKey + * @param null $port * - * @return SparkPost + * @return mixed|SparkPost */ - public function create($host, $apiKey) + public function create($host, $apiKey, $port = null) { if ((strpos($host, '://') === false && substr($host, 0, 1) != '/')) { $host = 'https://'.$host; } + $hostInfo = parse_url($host); + if ($hostInfo) { - return new SparkPost($this->client, [ - 'host' => $hostInfo['host'].$hostInfo['path'], + if (empty($port)) { + $port = $hostInfo['scheme'] === 'https' ? 443 : 80; + } + + $options = [ 'protocol' => $hostInfo['scheme'], - 'port' => $hostInfo['scheme'] === 'https' ? 443 : 80, + 'port' => $port, 'key' => $apiKey, - ]); + ]; + + $host = $hostInfo['host']; + if (isset($hostInfo['path'])) { + $path = $hostInfo['path']; + if (preg_match('~/api/(v\d+)$~i', $path, $matches)) { + // Remove /api from the path and extract the version in case differnt than the Sparkpost SDK default + $path = str_replace($matches[0], '', $path); + $options['version'] = $matches[1]; + } + + // Append whatever is left over to the host (assuming Momentum can be in a subfolder?) + $host .= $path; + } + + $options['host'] = $host; + + return new SparkPost($this->client, $options); } else { // problem :/ } From 33838f19c39555030fdae202ab219c3b1da0510f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 21:30:34 -0500 Subject: [PATCH 347/778] Force errors to feedback to UI --- app/bundles/EmailBundle/Helper/MailHelper.php | 1 + .../Facade/MomentumSendException.php | 2 +- .../SwiftMessageValidationException.php | 2 +- .../Momentum/Facade/MomentumFacade.php | 48 +++++++++++++++---- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index afa2cfba1f9..2a1560ac5d0 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -445,6 +445,7 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne if (!$this->transport->isStarted()) { $this->transportStartTime = time(); } + $this->mailer->send($this->message, $failures); if (!empty($failures)) { diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php index db4eca8f403..c39d8321376 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Facade/MomentumSendException.php @@ -5,6 +5,6 @@ /** * Class MomentumSendException. */ -class MomentumSendException extends \Exception +class MomentumSendException extends \Swift_TransportException { } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Validator/SwiftMessageValidator/SwiftMessageValidationException.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Validator/SwiftMessageValidator/SwiftMessageValidationException.php index 2448f617861..9a5818bf582 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Validator/SwiftMessageValidator/SwiftMessageValidationException.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Exception/Validator/SwiftMessageValidator/SwiftMessageValidationException.php @@ -5,6 +5,6 @@ /** * Class SwiftMessageValidationException. */ -final class SwiftMessageValidationException extends \RuntimeException +final class SwiftMessageValidationException extends \Swift_TransportException { } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index 541710b4008..9afa3cff9e6 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -65,24 +65,52 @@ public function send(\Swift_Mime_Message $message) $transmission = $this->swiftMessageService->transformToTransmission($message); $response = $this->adapter->createTransmission($transmission); $response = $response->wait(); - if (200 !== (int) $response->getStatusCode()) { - $this->logger->addError( - 'Momentum send: '.$response->getStatusCode(), - [ - 'response' => $response->getBody(), - ] - ); + + if (200 === (int) $response->getStatusCode()) { + return; } + + $message = $this->getErrors($response->getBody()); + + $this->logger->addError( + 'Momentum send: '.$response->getStatusCode(), + [ + 'response' => $response->getBody(), + ] + ); + + throw new MomentumSendException($message); } catch (\Exception $exception) { $this->logger->addError( 'Momentum send exception', [ 'message' => $exception->getMessage(), ]); - if ($exception instanceof SwiftMessageValidationException) { - throw $exception; + + throw $exception; + } + } + + /** + * @param $body + * + * @return string + */ + private function getErrors($body) + { + if (!is_array($body)) { + return (string) $body; + } + + if (isset($body['errors'])) { + $errors = []; + foreach ($body['errors'] as $error) { + $errors[] = $error['message'].' : '.$error['description']; } - throw new MomentumSendException(); + + return implode('; ', $errors); } + + return var_export($body, true); } } From 77f5f13d92a7ec6ca7efe6696e1d08762f084251 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 21:42:07 -0500 Subject: [PATCH 348/778] Return send count and process immediate feedback as SparkpostTransport does --- app/bundles/EmailBundle/Config/config.php | 1 + .../Momentum/Callback/MomentumCallback.php | 27 +++++++++++++++++++ .../Momentum/Facade/MomentumFacade.php | 25 +++++++++++++---- .../Transport/MomentumTransport.php | 7 ++--- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 3f8caa7eb40..e270a5854cc 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -407,6 +407,7 @@ 'mautic.transport.momentum.adapter', 'mautic.transport.momentum.service.swift_message', 'mautic.transport.momentum.validator.swift_message', + 'mautic.transport.momentum.callback', 'monolog.logger.mautic', ], ], diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php index 07184405fc0..1345ba729dd 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php @@ -12,6 +12,7 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Callback; use Mautic\EmailBundle\Model\TransportCallback; +use Mautic\LeadBundle\Entity\DoNotContact; use Symfony\Component\HttpFoundation\Request; class MomentumCallback @@ -21,11 +22,19 @@ class MomentumCallback */ private $transportCallback; + /** + * MomentumCallback constructor. + * + * @param TransportCallback $transportCallback + */ public function __construct(TransportCallback $transportCallback) { $this->transportCallback = $transportCallback; } + /** + * @param Request $request + */ public function processCallbackRequest(Request $request) { $responseItems = new ResponseItems($request); @@ -33,4 +42,22 @@ public function processCallbackRequest(Request $request) $this->transportCallback->addFailureByAddress($item->getEmail(), $item->getReason(), $item->getDncReason()); } } + + /** + * @param $transmission + * @param $response + */ + public function processImmediateFeedback($transmission, $response) + { + if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { + $comments = $response['errors'][0]['description']; + $emailAddress = $transmission['recipients']['to'][0]['email']; + $metadata = $this->getMetadata(); + + if (isset($metadata[$emailAddress]) && isset($metadata[$emailAddress]['leadId'])) { + $emailId = (!empty($metadata[$emailAddress]['emailId'])) ? $metadata[$emailAddress]['emailId'] : null; + $this->transportCallback->addFailureByContactId($metadata[$emailAddress]['leadId'], $comments, DoNotContact::BOUNCED, $emailId); + } + } + } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index 9afa3cff9e6..9a8dd578e17 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -3,8 +3,8 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Facade; use Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\AdapterInterface; +use Mautic\EmailBundle\Swiftmailer\Momentum\Callback\MomentumCallback; use Mautic\EmailBundle\Swiftmailer\Momentum\Exception\Facade\MomentumSendException; -use Mautic\EmailBundle\Swiftmailer\Momentum\Exception\Validator\SwiftMessageValidator\SwiftMessageValidationException; use Mautic\EmailBundle\Swiftmailer\Momentum\Service\SwiftMessageServiceInterface; use Mautic\EmailBundle\Swiftmailer\Momentum\Validator\SwiftMessageValidator\SwiftMessageValidatorInterface; use Monolog\Logger; @@ -29,21 +29,30 @@ final class MomentumFacade implements MomentumFacadeInterface */ private $swiftMessageValidator; - /** @var Logger */ + /** + * @var Logger + */ private $logger; + /** + * @var MomentumCallback + */ + private $momentumCallback; + /** * MomentumFacade constructor. * * @param AdapterInterface $adapter * @param SwiftMessageServiceInterface $swiftMessageService * @param SwiftMessageValidatorInterface $swiftMessageValidator + * @param MomentumCallback $momentumCallback * @param Logger $logger */ public function __construct( AdapterInterface $adapter, SwiftMessageServiceInterface $swiftMessageService, SwiftMessageValidatorInterface $swiftMessageValidator, + MomentumCallback $momentumCallback, Logger $logger ) { $this->adapter = $adapter; @@ -55,8 +64,9 @@ public function __construct( /** * @param \Swift_Mime_Message $message * - * @throws SwiftMessageValidationException - * @throws MomentumSendException + * @return mixed + * + * @throws \Exception */ public function send(\Swift_Mime_Message $message) { @@ -67,7 +77,12 @@ public function send(\Swift_Mime_Message $message) $response = $response->wait(); if (200 === (int) $response->getStatusCode()) { - return; + $results = $response->getBody(); + if (!$sendCount = $results['results']['total_accepted_recipients']) { + $this->momentumCallback->processImmediateFeedback($transmission, $results); + } + + return $sendCount; } $message = $this->getErrors($response->getBody()); diff --git a/app/bundles/EmailBundle/Swiftmailer/Transport/MomentumTransport.php b/app/bundles/EmailBundle/Swiftmailer/Transport/MomentumTransport.php index 0bef4615615..d2f30e7c903 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Transport/MomentumTransport.php +++ b/app/bundles/EmailBundle/Swiftmailer/Transport/MomentumTransport.php @@ -48,8 +48,7 @@ class MomentumTransport implements \Swift_Transport, TokenTransportInterface, Ca public function __construct( MomentumCallback $momentumCallback, MomentumFacadeInterface $momentumFacade - ) - { + ) { $this->momentumCallback = $momentumCallback; $this->momentumFacade = $momentumFacade; } @@ -96,9 +95,7 @@ public function stop() */ public function send(Swift_Mime_Message $message, &$failedRecipients = null) { - $this->momentumFacade->send($message); - - return count($message->getTo()); + return $this->momentumFacade->send($message); } /** From 4cd29f1936896ef9f1eeac9a8a0f76fddd3c6850 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:01:55 -0500 Subject: [PATCH 349/778] Fix jsonSerialize object use --- .../Swiftmailer/Momentum/Callback/MomentumCallback.php | 5 ++--- .../Swiftmailer/Momentum/Facade/MomentumFacade.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php index 1345ba729dd..c6f20c541fb 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php @@ -44,14 +44,13 @@ public function processCallbackRequest(Request $request) } /** - * @param $transmission + * @param $emailAddress * @param $response */ - public function processImmediateFeedback($transmission, $response) + public function processImmediateFeedback($emailAddress, array $response) { if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { $comments = $response['errors'][0]['description']; - $emailAddress = $transmission['recipients']['to'][0]['email']; $metadata = $this->getMetadata(); if (isset($metadata[$emailAddress]) && isset($metadata[$emailAddress]['leadId'])) { diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index 9a8dd578e17..d3e6e59eb60 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -79,7 +79,7 @@ public function send(\Swift_Mime_Message $message) if (200 === (int) $response->getStatusCode()) { $results = $response->getBody(); if (!$sendCount = $results['results']['total_accepted_recipients']) { - $this->momentumCallback->processImmediateFeedback($transmission, $results); + $this->momentumCallback->processImmediateFeedback(key($message->getTo()), $results); } return $sendCount; From 63e49a6803c24a3071da19e538c69c3e4d320faa Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:02:12 -0500 Subject: [PATCH 350/778] Fixed subsitution data format --- .../Momentum/Metadata/MetadataProcessor.php | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php index be80ffe8e2a..5759f540b64 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Metadata/MetadataProcessor.php @@ -32,19 +32,14 @@ class MetadataProcessor private $substitutionMergeVars = []; /** - * @var \Swift_Message - */ - private $message; - - /** - * @var string + * @var array */ - private $body; + private $mauticTokens = []; /** - * @var string + * @var \Swift_Message */ - private $text; + private $message; /** * MetadataProcessor constructor. @@ -61,9 +56,9 @@ public function __construct(\Swift_Message $message) // Build the substitution merge vars $this->buildSubstitutionData(); - if (count($this->substitutionKeys)) { + if (count($this->mauticTokens)) { // Update the content with the substitution merge vars - MailHelper::searchReplaceTokens($this->substitutionKeys, $this->substitutionMergeVars, $this->message); + MailHelper::searchReplaceTokens($this->mauticTokens, $this->substitutionMergeVars, $this->message); } } @@ -99,7 +94,7 @@ public function getSubstitutionData($email) $substitutionData = []; foreach ($this->metadata[$email]['tokens'] as $token => $value) { - $substitutionData[$this->substitutionMergeVars[$token]] = $value; + $substitutionData[$this->substitutionKeys[$token]] = $value; } return $substitutionData; @@ -108,11 +103,11 @@ public function getSubstitutionData($email) private function buildSubstitutionData() { // Sparkpost uses {{ name }} for tokens so Mautic's need to be converted; although using their {{{ }}} syntax to prevent HTML escaping - $metadataSet = reset($this->metadata); - $tokens = (!empty($metadataSet['tokens'])) ? $metadataSet['tokens'] : []; - $mauticTokens = array_keys($tokens); + $metadataSet = reset($this->metadata); + $tokens = (!empty($metadataSet['tokens'])) ? $metadataSet['tokens'] : []; + $this->mauticTokens = array_keys($tokens); - foreach ($mauticTokens as $token) { + foreach ($this->mauticTokens as $token) { $this->substitutionKeys[$token] = strtoupper(preg_replace('/[^a-z0-9]+/i', '', $token)); $this->substitutionMergeVars[$token] = '{{{ '.$this->substitutionKeys[$token].' }}}'; } From b20f9bfb6949a5f0148771e01c004753e0aa19eb Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:25:16 -0500 Subject: [PATCH 351/778] Fixed attachment payload --- .../DTO/TransmissionDTO/ContentDTO/AttachementDTO.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php index 054e605eb6a..071d36cf0b4 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/DTO/TransmissionDTO/ContentDTO/AttachementDTO.php @@ -42,9 +42,9 @@ public function __construct($type, $name, $content) public function jsonSerialize() { return [ - 'type' => $this->type, - 'name' => $this->name, - 'content' => $this->content, + 'type' => $this->type, + 'name' => $this->name, + 'data' => $this->content, ]; } } From ba74b45b60f4896d4946c920d3a2943328cd8bf0 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:25:40 -0500 Subject: [PATCH 352/778] Prevented notice if description is not set in an error message --- .../Swiftmailer/Momentum/Facade/MomentumFacade.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index d3e6e59eb60..19bca6099be 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -120,7 +120,13 @@ private function getErrors($body) if (isset($body['errors'])) { $errors = []; foreach ($body['errors'] as $error) { - $errors[] = $error['message'].' : '.$error['description']; + $error = $error['message']; + + if (isset($error['description'])) { + $error .= ' : '.$error['description']; + } + + $errors[] = $error; } return implode('; ', $errors); From 9f67b83a05b72638098cd450b52fdbef8e39611c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:26:18 -0500 Subject: [PATCH 353/778] Fixed to addresses from getting mixed up in cc addresses and add a CC header --- .../Momentum/Service/SwiftMessageService.php | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index cae4a011e72..84965e0b263 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -105,24 +105,12 @@ public function transformToTransmission(\Swift_Mime_Message $message) $returnPath = $message->getReturnPath() ? $message->getReturnPath() : $messageFromEmail; $transmission = new TransmissionDTO($content, $returnPath); - $ccRecipients = []; - foreach ($message->getTo() as $email => $name) { - $ccRecipients[$email] = $name; - } - if ($message->getCc() !== null) { - foreach ($message->getCc() as $email => $name) { - $ccRecipients[$email] = $name; - } - } - $bccRecipients = []; - if ($message->getBcc() !== null) { - $bccRecipients = $message->getBcc(); - } - $recipientsGrouped = [ - 'cc' => $ccRecipients, - 'bcc' => $bccRecipients, + 'to' => (array) $message->getTo(), + 'cc' => (array) $message->getCc(), + 'bcc' => (array) $message->getBcc(), ]; + foreach ($recipientsGrouped as $group => $recipients) { $isBcc = ($group === 'bcc'); foreach ($recipients as $email => $name) { @@ -137,6 +125,10 @@ public function transformToTransmission(\Swift_Mime_Message $message) } } + if (count($recipientsGrouped['cc'])) { + $content->addHeader('CC', implode(',', array_keys($recipientsGrouped['cc']))); + } + return $transmission; } } From 2e79c963c15d03ae13f18c1eb96fdbf25b12e1ce Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:44:35 -0500 Subject: [PATCH 354/778] Fixed issue where not all failed emails from a "tokenized mailer" were communicated to the send UI --- app/bundles/EmailBundle/Helper/MailHelper.php | 15 +++++++++------ .../EmailBundle/Model/SendEmailToContact.php | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 2a1560ac5d0..bdd2d4ddc5e 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -440,8 +440,6 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne $this->setMessageHeaders(); try { - $failures = []; - if (!$this->transport->isStarted()) { $this->transportStartTime = time(); } @@ -456,11 +454,16 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne // Clear the log so that previous output is not associated with new errors $this->logger->clear(); } catch (\Exception $e) { + $failures = $this->tokenizationEnabled ? array_keys($this->message->getMetadata()) : []; + // Exception encountered when sending so all recipients are considered failures - $this->errors['failures'] = array_merge( - array_keys((array) $this->message->getTo()), - array_keys((array) $this->message->getCc()), - array_keys((array) $this->message->getBcc()) + $this->errors['failures'] = array_unique( + array_merge( + $failures, + array_keys((array) $this->message->getTo()), + array_keys((array) $this->message->getCc()), + array_keys((array) $this->message->getBcc()) + ) ); $this->logError($e, 'send'); diff --git a/app/bundles/EmailBundle/Model/SendEmailToContact.php b/app/bundles/EmailBundle/Model/SendEmailToContact.php index d275070fe73..459b5ab2cf1 100644 --- a/app/bundles/EmailBundle/Model/SendEmailToContact.php +++ b/app/bundles/EmailBundle/Model/SendEmailToContact.php @@ -362,7 +362,7 @@ protected function processSendFailures($sendFailures) } // Add lead ID to list of failures - $this->failedContacts[$stat->getEmailId()] = $failedEmail; + $this->failedContacts[$stat->getLeadId()] = $failedEmail; $this->errorMessages[$stat->getLeadId()] = $error; $this->statHelper->markForDeletion($stat); From 3471bcbfde84e9119cefa00530558cfbda4bea4f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 22:47:43 -0500 Subject: [PATCH 355/778] Fixed appending trailing slash onto the host --- .../EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php index 46c47cac13e..10594e3868d 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php @@ -61,7 +61,9 @@ public function create($host, $apiKey, $port = null) } // Append whatever is left over to the host (assuming Momentum can be in a subfolder?) - $host .= $path; + if ('/' !== $path) { + $host .= $path; + } } $options['host'] = $host; From cd75334e81e7784d9e499c7b78a9e1ee95cf3a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 10 May 2018 17:56:56 +0200 Subject: [PATCH 356/778] WIP --- .../Momentum/Facade/MomentumFacadeTest.php | 113 ++++++++++++++++++ .../Service/SwiftMessageServiceTest.php | 40 +++++++ 2 files changed, 153 insertions(+) create mode 100644 app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php create mode 100644 app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php new file mode 100644 index 00000000000..5cf1888d9e8 --- /dev/null +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php @@ -0,0 +1,113 @@ +adapterMock = $this->createMock(AdapterInterface::class); + $this->swiftMessageServiceMock = $this->createMock(SwiftMessageServiceInterface::class); + $this->swiftMessageValidatorMock = $this->createMock(SwiftMessageValidatorInterface::class); + $this->loggerMock = $this->createMock(Logger::class); + } + + public function testSendOk() + { + $swiftMessageMock = $this->createMock(\Swift_Mime_Message::class); + $this->swiftMessageValidatorMock->expects($this->at(0)) + ->method('validate') + ->with($swiftMessageMock); + $transmissionDTOMock = $this->createMock(TransmissionDTO::class); + $this->swiftMessageServiceMock->expects($this->at(0)) + ->method('transformToTransmission') + ->with($swiftMessageMock) + ->willReturn($transmissionDTOMock); + $sparkPostPromiseMock = $this->createMock(SparkPostPromise::class); + $this->adapterMock->expects($this->at(0)) + ->method('createTransmission') + ->with($transmissionDTOMock) + ->willReturn($sparkPostPromiseMock); + $sparkPostResponseMock = $this->createMock(SparkPostResponse::class); + $sparkPostPromiseMock->expects($this->at(0)) + ->method('wait') + ->willReturn($sparkPostResponseMock); + $sparkPostResponseMock->expects($this->at(0)) + ->method('getStatusCode') + ->willReturn('200'); + $facade = $this->getMomentumFacade(); + $facade->send($swiftMessageMock); + } + + /** + * Test for SwiftMessageValidationException exception. + */ + public function testSendValidatorError() + { + $swiftMessageMock = $this->createMock(\Swift_Mime_Message::class); + $swiftMessageValidationExceptionMock = $this->createMock(SwiftMessageValidationException::class); + $this->swiftMessageValidatorMock->expects($this->at(0)) + ->method('validate') + ->with($swiftMessageMock) + ->willThrowException($swiftMessageValidationExceptionMock); + $exceptionMessage = 'Example exception message'; + $swiftMessageValidationExceptionMock->expects($this->at(0)) + ->method('getMessage') + ->willReturn($exceptionMessage); + $this->loggerMock->expects($this->at(0)) + ->method('addError') + ->with('Momentum send exception', [ + 'message' => $exceptionMessage, + ]); + $facade = $this->getMomentumFacade(); + $this->expectException(SwiftMessageValidationException::class); + $facade->send($swiftMessageMock); + } + + /** + * @return MomentumFacade + */ + private function getMomentumFacade() + { + return new MomentumFacade( + $this->adapterMock, + $this->swiftMessageServiceMock, + $this->swiftMessageValidatorMock, + $this->loggerMock + ); + } +} diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php new file mode 100644 index 00000000000..b585775a532 --- /dev/null +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php @@ -0,0 +1,40 @@ +translatorInterfaceMock = $this->createMock(TranslatorInterface::class); + } + + public function testTransformToTransmission() + { + $mauticMessage = new MauticMessage(); + $service = $this->getSwiftMessageService(); + $transmissionDTO = $service->transformToTransmission($mauticMessage); + } + + /** + * @return SwiftMessageService + */ + private function getSwiftMessageService() + { + return new SwiftMessageService( + $this->translatorInterfaceMock + ); + } +} From 6b24a7755568ce754066f3a19ed57104ccd9221c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 11 May 2018 14:50:14 +0200 Subject: [PATCH 357/778] Transform to transmission DTO --- .../Service/SwiftMessageServiceTest.php | 102 +++++++++++++++++- .../Service/data/attachments/sample.txt | 1 + 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/data/attachments/sample.txt diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php index b585775a532..87ab3fd98a3 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php @@ -21,11 +21,109 @@ protected function setUp() $this->translatorInterfaceMock = $this->createMock(TranslatorInterface::class); } - public function testTransformToTransmission() + /** + * @param MauticMessage $mauticMessage + * @param string $expectedTransmissionJson + * + * @dataProvider dataTransformToTransmission + */ + public function testTransformToTransmission(MauticMessage $mauticMessage, $expectedTransmissionJson) { - $mauticMessage = new MauticMessage(); $service = $this->getSwiftMessageService(); $transmissionDTO = $service->transformToTransmission($mauticMessage); + + $this->assertJsonStringEqualsJsonString($expectedTransmissionJson, json_encode($transmissionDTO)); + } + + public function dataTransformToTransmission() + { + return [ + $this->geTransformToTransmissionComplexData(), + ]; + } + + /** + * @return array + */ + private function geTransformToTransmissionComplexData() + { + $mauticMessage = new MauticMessage(); + $mauticMessage->setSubject('Test subject') + ->setReturnPath('return-path@test.local') + ->setSender('sender@test.local', 'Sender test') + ->setFrom('from@test.local', 'From test') + ->setBody('') + ->addTo('to1@test.local', 'To1 test') + ->addTo('to2@test.local', 'To2 test') + ->addCc('cc1@test.local', 'CC1 test') + ->addCc('cc2@test.local', 'CC2 test') + ->addBcc('bcc1@test.local', 'BCC1 test') + ->addBcc('bcc2@test.local', 'BCC2 test') + ->addAttachment(__DIR__.'/data/attachments/sample.txt'); + $json = ' + { + "return_path":"return-path@test.local", + "recipients": [ + { + "address": { + "email": "to1@test.local", + "name": "To1 test", + "header_to": "to1@test.local" + } + }, + { + "address": { + "email": "to2@test.local", + "name": "To2 test", + "header_to": "to2@test.local" + } + }, + { + "address": { + "email": "cc1@test.local", + "name": "CC1 test", + "header_to": "cc1@test.local" + } + }, + { + "address": { + "email": "cc2@test.local", + "name": "CC2 test", + "header_to": "cc2@test.local" + } + }, + { + "address": { + "email": "bcc1@test.local", + "name": "BCC1 test" + } + }, + { + "address": { + "email": "bcc2@test.local", + "name": "BCC2 test" + } + } + ], + "content": { + "subject": "Test subject", + "from": { + "email": "from@test.local", + "name": "From test" + }, + "html": "<\/html>", + "attachments": [ + { + "type": "text\/plain", + "name": "sample.txt", + "content": "VGhpcyBpcyBzYW1wbGUgYXR0YWNobWVudAo=" + } + ] + } + } + '; + + return [$mauticMessage, $json]; } /** diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/data/attachments/sample.txt b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/data/attachments/sample.txt new file mode 100644 index 00000000000..8d0fe507e06 --- /dev/null +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/data/attachments/sample.txt @@ -0,0 +1 @@ +This is sample attachment From ed6cf890981f7e8d4a62ff6a33c0652562bdf00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 11 May 2018 15:18:09 +0200 Subject: [PATCH 358/778] WIP --- .../Momentum/Callback/MomentumCallback.php | 2 +- .../Callback/MomentumCallbackInterface.php | 22 +++++++++++++++++++ .../Momentum/DTO/TransmissionDTO.php | 2 +- .../SwiftMessageValidationException.php | 2 +- .../Momentum/Facade/MomentumFacade.php | 9 ++++---- .../Momentum/Facade/MomentumFacadeTest.php | 15 +++++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php index c6f20c541fb..e604238b60f 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php @@ -15,7 +15,7 @@ use Mautic\LeadBundle\Entity\DoNotContact; use Symfony\Component\HttpFoundation\Request; -class MomentumCallback +final class MomentumCallback implements MomentumCallbackInterface { /** * @var TransportCallback diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php new file mode 100644 index 00000000000..6756e64dc31 --- /dev/null +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php @@ -0,0 +1,22 @@ +adapter = $adapter; $this->swiftMessageService = $swiftMessageService; $this->swiftMessageValidator = $swiftMessageValidator; + $this->momentumCallback = $momentumCallback; $this->logger = $logger; } diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php index 5cf1888d9e8..298c6c2a96d 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php @@ -3,6 +3,7 @@ namespace Mautic\EmailBundle\Tests\Swiftmailer\Momentum\Facade; use Mautic\EmailBundle\Swiftmailer\Momentum\Adapter\AdapterInterface; +use Mautic\EmailBundle\Swiftmailer\Momentum\Callback\MomentumCallbackInterface; use Mautic\EmailBundle\Swiftmailer\Momentum\DTO\TransmissionDTO; use Mautic\EmailBundle\Swiftmailer\Momentum\Exception\Validator\SwiftMessageValidator\SwiftMessageValidationException; use Mautic\EmailBundle\Swiftmailer\Momentum\Facade\MomentumFacade; @@ -32,6 +33,11 @@ class MomentumFacadeTest extends \PHPUnit_Framework_TestCase */ private $swiftMessageValidatorMock; + /** + * @var MomentumCallbackInterface + */ + private $momentumCallback; + /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -43,6 +49,7 @@ protected function setUp() $this->adapterMock = $this->createMock(AdapterInterface::class); $this->swiftMessageServiceMock = $this->createMock(SwiftMessageServiceInterface::class); $this->swiftMessageValidatorMock = $this->createMock(SwiftMessageValidatorInterface::class); + $this->momentumCallback = $this->createMock(MomentumCallbackInterface::class); $this->loggerMock = $this->createMock(Logger::class); } @@ -69,6 +76,13 @@ public function testSendOk() $sparkPostResponseMock->expects($this->at(0)) ->method('getStatusCode') ->willReturn('200'); + $sparkPostResponseMock->expects($this->at(1)) + ->method('getBody') + ->willReturn([ + 'results' => [ + + ] + ]) $facade = $this->getMomentumFacade(); $facade->send($swiftMessageMock); } @@ -107,6 +121,7 @@ private function getMomentumFacade() $this->adapterMock, $this->swiftMessageServiceMock, $this->swiftMessageValidatorMock, + $this->momentumCallbackMock, $this->loggerMock ); } From 9e5dc7d046fab5682fe08d560921a773a4f184a1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 08:22:17 -0500 Subject: [PATCH 359/778] Fixed callback processImmediateFeedback --- .../Momentum/Callback/MomentumCallback.php | 12 ++++++++---- .../Momentum/Callback/MomentumCallbackInterface.php | 8 +++++--- .../Swiftmailer/Momentum/Facade/MomentumFacade.php | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php index e604238b60f..ef3e8262b8d 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallback.php @@ -12,6 +12,7 @@ namespace Mautic\EmailBundle\Swiftmailer\Momentum\Callback; use Mautic\EmailBundle\Model\TransportCallback; +use Mautic\EmailBundle\Swiftmailer\Message\MauticMessage; use Mautic\LeadBundle\Entity\DoNotContact; use Symfony\Component\HttpFoundation\Request; @@ -44,14 +45,17 @@ public function processCallbackRequest(Request $request) } /** - * @param $emailAddress - * @param $response + * @param \Swift_Mime_Message $message + * @param array $response + * + * @return mixed|void */ - public function processImmediateFeedback($emailAddress, array $response) + public function processImmediateFeedback(\Swift_Mime_Message $message, array $response) { if (!empty($response['errors'][0]['code']) && 1902 == (int) $response['errors'][0]['code']) { $comments = $response['errors'][0]['description']; - $metadata = $this->getMetadata(); + $metadata = ($message instanceof MauticMessage) ? $message->getMetadata() : []; + $emailAddress = key($message->getTo()); if (isset($metadata[$emailAddress]) && isset($metadata[$emailAddress]['leadId'])) { $emailId = (!empty($metadata[$emailAddress]['emailId'])) ? $metadata[$emailAddress]['emailId'] : null; diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php index 6756e64dc31..15bf4683be2 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Callback/MomentumCallbackInterface.php @@ -15,8 +15,10 @@ interface MomentumCallbackInterface public function processCallbackRequest(Request $request); /** - * @param string $emailAddress - * @param array $response + * @param \Swift_Mime_Message $message + * @param array $response + * + * @return mixed */ - public function processImmediateFeedback($emailAddress, array $response); + public function processImmediateFeedback(\Swift_Mime_Message $message, array $response); } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php index 0f2e96b83bd..9e38979acee 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Facade/MomentumFacade.php @@ -80,7 +80,7 @@ public function send(\Swift_Mime_Message $message) if (200 === (int) $response->getStatusCode()) { $results = $response->getBody(); if (!$sendCount = $results['results']['total_accepted_recipients']) { - $this->momentumCallback->processImmediateFeedback(key($message->getTo()), $results); + $this->momentumCallback->processImmediateFeedback($message, $results); } return $sendCount; From 9518873314384d3c389c756e9c3e3efd827253fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 11 May 2018 15:41:13 +0200 Subject: [PATCH 360/778] Fix one Momentum Facade test --- .../Momentum/Facade/MomentumFacadeTest.php | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php index 298c6c2a96d..e281f4be7e0 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php @@ -34,9 +34,9 @@ class MomentumFacadeTest extends \PHPUnit_Framework_TestCase private $swiftMessageValidatorMock; /** - * @var MomentumCallbackInterface + * @var \PHPUnit_Framework_MockObject_MockObject */ - private $momentumCallback; + private $momentumCallbackMock; /** * @var \PHPUnit_Framework_MockObject_MockObject @@ -49,7 +49,7 @@ protected function setUp() $this->adapterMock = $this->createMock(AdapterInterface::class); $this->swiftMessageServiceMock = $this->createMock(SwiftMessageServiceInterface::class); $this->swiftMessageValidatorMock = $this->createMock(SwiftMessageValidatorInterface::class); - $this->momentumCallback = $this->createMock(MomentumCallbackInterface::class); + $this->momentumCallbackMock = $this->createMock(MomentumCallbackInterface::class); $this->loggerMock = $this->createMock(Logger::class); } @@ -76,15 +76,20 @@ public function testSendOk() $sparkPostResponseMock->expects($this->at(0)) ->method('getStatusCode') ->willReturn('200'); + $totalRecipients = 0; + $bodyResults = [ + 'results' => [ + 'total_accepted_recipients' => $totalRecipients, + ], + ]; $sparkPostResponseMock->expects($this->at(1)) ->method('getBody') - ->willReturn([ - 'results' => [ - - ] - ]) + ->willReturn($bodyResults); + $this->momentumCallbackMock->expects($this->at(0)) + ->method('processImmediateFeedback') + ->with($swiftMessageMock, $bodyResults); $facade = $this->getMomentumFacade(); - $facade->send($swiftMessageMock); + $this->assertSame($totalRecipients, $facade->send($swiftMessageMock)); } /** @@ -93,15 +98,15 @@ public function testSendOk() public function testSendValidatorError() { $swiftMessageMock = $this->createMock(\Swift_Mime_Message::class); + $exceptionMessage = 'Example exception message'; $swiftMessageValidationExceptionMock = $this->createMock(SwiftMessageValidationException::class); + $swiftMessageValidationExceptionMock->expects($this->at(0)) + ->method('getMessage') + ->willReturn($exceptionMessage); $this->swiftMessageValidatorMock->expects($this->at(0)) ->method('validate') ->with($swiftMessageMock) ->willThrowException($swiftMessageValidationExceptionMock); - $exceptionMessage = 'Example exception message'; - $swiftMessageValidationExceptionMock->expects($this->at(0)) - ->method('getMessage') - ->willReturn($exceptionMessage); $this->loggerMock->expects($this->at(0)) ->method('addError') ->with('Momentum send exception', [ From 317748d4065be336d6ac83640aed0322e494ecd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 11 May 2018 15:53:59 +0200 Subject: [PATCH 361/778] Fix second Momentum facade test --- .../Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php index e281f4be7e0..194f68e158b 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Facade/MomentumFacadeTest.php @@ -99,10 +99,7 @@ public function testSendValidatorError() { $swiftMessageMock = $this->createMock(\Swift_Mime_Message::class); $exceptionMessage = 'Example exception message'; - $swiftMessageValidationExceptionMock = $this->createMock(SwiftMessageValidationException::class); - $swiftMessageValidationExceptionMock->expects($this->at(0)) - ->method('getMessage') - ->willReturn($exceptionMessage); + $swiftMessageValidationExceptionMock = new SwiftMessageValidationException($exceptionMessage); $this->swiftMessageValidatorMock->expects($this->at(0)) ->method('validate') ->with($swiftMessageMock) From 75f2aad69645e07a0428264af85de95dfbc83eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 11 May 2018 17:00:08 +0200 Subject: [PATCH 362/778] Fix wrong test refactoring --- .../Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php index 5bbba0324d1..b6608de0357 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Callback/SendGridApiCallbackTest.php @@ -12,7 +12,7 @@ namespace Mautic\EmailBundle\Tests\Swiftmailer\SendGrid\Callback; use Mautic\EmailBundle\Model\TransportCallback; -use Mautic\EmailBundle\Swiftmailer\SendGrid\Callback\MomentumCallback; +use Mautic\EmailBundle\Swiftmailer\SendGrid\Callback\SendGridApiCallback; use Mautic\LeadBundle\Entity\DoNotContact; use Symfony\Component\HttpFoundation\Request; @@ -24,7 +24,7 @@ public function testSupportedEvents() ->disableOriginalConstructor() ->getMock(); - $sendGridApiCallback = new MomentumCallback($transportCallback); + $sendGridApiCallback = new SendGridApiCallback($transportCallback); $payload = [ [ From 00b4fb7aba575545a39bb98a2558e443a35fe44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 11 May 2018 17:16:43 +0200 Subject: [PATCH 363/778] PHPDocs fixs --- .../Swiftmailer/Momentum/Adapter/Adapter.php | 21 ++++++++----------- .../Momentum/Adapter/AdapterInterface.php | 9 ++++++++ .../Momentum/Callback/CallbackEnum.php | 2 +- .../Momentum/Callback/MomentumCallback.php | 2 +- .../Callback/MomentumCallbackInterface.php | 9 ++++++++ .../Momentum/Callback/ResponseItem.php | 2 +- .../Momentum/Callback/ResponseItems.php | 2 +- .../Momentum/DTO/TransmissionDTO.php | 9 ++++++++ .../DTO/TransmissionDTO/ContentDTO.php | 11 +++++++++- .../ContentDTO/AttachementDTO.php | 9 ++++++++ .../TransmissionDTO/ContentDTO/FromDTO.php | 9 ++++++++ .../DTO/TransmissionDTO/OptionsDTO.php | 9 ++++++++ .../DTO/TransmissionDTO/RecipientDTO.php | 9 ++++++++ .../RecipientDTO/AddressDTO.php | 9 ++++++++ .../Facade/MomentumSendException.php | 9 ++++++++ .../SwiftMessageValidationException.php | 9 ++++++++ .../Momentum/Facade/MomentumFacade.php | 9 ++++++++ .../Facade/MomentumFacadeInterface.php | 9 ++++++++ .../Momentum/Service/SwiftMessageService.php | 9 ++++++++ .../Service/SwiftMessageServiceInterface.php | 9 ++++++++ .../SwiftMessageValidator.php | 9 ++++++++ .../SwiftMessageValidatorInterface.php | 9 ++++++++ .../Transport/MomentumTransport.php | 2 +- 23 files changed, 168 insertions(+), 18 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php index 0f761cf3eeb..310f9c8e972 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/Adapter.php @@ -1,5 +1,14 @@ momentumSparkpost->transmissions->post($payload); - /*$curl = curl_init(); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($transmissionDTO)); - curl_setopt($curl, CURLOPT_URL, 'http://'.$this->host.'/v1/transmissions'); - $headers = []; - $headers[] = 'Content-Type: application/json'; - $headers[] = 'Authorization: '.$this->apiKey; - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - $result = curl_exec($curl); - curl_close($curl); - echo $result; - exit;*/ } } diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php index 2cdac6f4bc7..6a13a0af505 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Adapter/AdapterInterface.php @@ -1,5 +1,14 @@ Date: Fri, 11 May 2018 12:33:48 -0500 Subject: [PATCH 364/778] Revert "Add sendgrid support for custom headers" This reverts commit 81ba2896c089f13f278eb46b659b5c3565185f15. --- app/bundles/EmailBundle/Config/config.php | 4 -- .../SendGrid/Mail/SendGridMailHeader.php | 53 ------------------- .../SendGrid/SendGridApiMessage.php | 20 +------ .../SendGrid/Mail/SendGridMailHeaderTest.php | 52 ------------------ .../SendGrid/SendGridApiMessageTest.php | 11 +--- 5 files changed, 2 insertions(+), 138 deletions(-) delete mode 100644 app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php delete mode 100644 app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index e270a5854cc..de342c9163d 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -459,9 +459,6 @@ 'mautic.transport.sendgrid_api.mail.attachment' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailAttachment::class, ], - 'mautic.transport.sendgrid_api.mail.header' => [ - 'class' => \Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailHeader::class, - ], 'mautic.transport.sendgrid_api.message' => [ 'class' => \Mautic\EmailBundle\Swiftmailer\SendGrid\SendGridApiMessage::class, 'arguments' => [ @@ -469,7 +466,6 @@ 'mautic.transport.sendgrid_api.mail.personalization', 'mautic.transport.sendgrid_api.mail.metadata', 'mautic.transport.sendgrid_api.mail.attachment', - 'mautic.transport.sendgrid_api.mail.header', ], ], 'mautic.transport.sendgrid_api.response' => [ diff --git a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php b/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php deleted file mode 100644 index 4f25b64724a..00000000000 --- a/app/bundles/EmailBundle/Swiftmailer/SendGrid/Mail/SendGridMailHeader.php +++ /dev/null @@ -1,53 +0,0 @@ -getHeaders()->getAll(); - /** @var \Swift_Mime_Header $header */ - foreach ($headers as $header) { - $headerName = $header->getFieldName(); - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && !in_array($headerName, $this->reservedKeys)) { - $mail->addHeader($headerName, $header->getFieldBodyModel()); - } - } - } -} diff --git a/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php b/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php index 5c055a7971f..dddd3bac928 100644 --- a/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php +++ b/app/bundles/EmailBundle/Swiftmailer/SendGrid/SendGridApiMessage.php @@ -13,7 +13,6 @@ use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailAttachment; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailBase; -use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailHeader; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailMetadata; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailPersonalization; use SendGrid\Mail; @@ -40,32 +39,16 @@ class SendGridApiMessage */ private $sendGridMailAttachment; - /** - * @var SendGridMailHeader - */ - private $sendGridMailHeader; - - /** - * SendGridApiMessage constructor. - * - * @param SendGridMailBase $sendGridMailBase - * @param SendGridMailPersonalization $sendGridMailPersonalization - * @param SendGridMailMetadata $sendGridMailMetadata - * @param SendGridMailAttachment $sendGridMailAttachment - * @param SendGridMailHeader $sendGridMailHeader - */ public function __construct( SendGridMailBase $sendGridMailBase, SendGridMailPersonalization $sendGridMailPersonalization, SendGridMailMetadata $sendGridMailMetadata, - SendGridMailAttachment $sendGridMailAttachment, - SendGridMailHeader $sendGridMailHeader + SendGridMailAttachment $sendGridMailAttachment ) { $this->sendGridMailBase = $sendGridMailBase; $this->sendGridMailPersonalization = $sendGridMailPersonalization; $this->sendGridMailMetadata = $sendGridMailMetadata; $this->sendGridMailAttachment = $sendGridMailAttachment; - $this->sendGridMailHeader = $sendGridMailHeader; } /** @@ -80,7 +63,6 @@ public function getMessage(\Swift_Mime_Message $message) $this->sendGridMailPersonalization->addPersonalizedDataToMail($mail, $message); $this->sendGridMailMetadata->addMetadataToMail($mail, $message); $this->sendGridMailAttachment->addAttachmentsToMail($mail, $message); - $this->sendGridMailHeader->addHeadersToMail($mail, $message); return $mail; } diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php deleted file mode 100644 index 190fd66bfab..00000000000 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/Mail/SendGridMailHeaderTest.php +++ /dev/null @@ -1,52 +0,0 @@ -getMockBuilder(\Swift_Mime_Message::class) - ->getMock(); - - $header = $this->createMock(\Swift_Mime_Header::class); - $header->expects($this->once()) - ->method('getFieldName') - ->willReturn('name'); - $header->expects($this->once()) - ->method('getFieldBodyModel') - ->willReturn('body'); - $header->expects($this->once()) - ->method('getFieldType') - ->willReturn(\Swift_Mime_Header::TYPE_TEXT); - - $headerSet = $this->createMock(\Swift_Mime_HeaderSet::class); - $headerSet->expects($this->once()) - ->method('getAll') - ->willReturn([$header]); - - $message->expects($this->once()) - ->method('getHeaders') - ->willReturn($headerSet); - - $mail = new Mail('from', 'subject', 'to', 'content'); - - $sendGridMailHeader->addHeadersToMail($mail, $message); - - $this->assertEquals(['name' => 'body'], $mail->getHeaders()); - } -} diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php index b3e84e90978..dd73c7b86a9 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/SendGrid/SendGridApiMessageTest.php @@ -13,7 +13,6 @@ use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailAttachment; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailBase; -use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailHeader; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailMetadata; use Mautic\EmailBundle\Swiftmailer\SendGrid\Mail\SendGridMailPersonalization; use Mautic\EmailBundle\Swiftmailer\SendGrid\SendGridApiMessage; @@ -39,10 +38,6 @@ public function testGetMail() ->disableOriginalConstructor() ->getMock(); - $sendGridMailHeader = $this->getMockBuilder(SendGridMailHeader::class) - ->disableOriginalConstructor() - ->getMock(); - $mail = $this->getMockBuilder(Mail::class) ->disableOriginalConstructor() ->getMock(); @@ -51,7 +46,7 @@ public function testGetMail() ->disableOriginalConstructor() ->getMock(); - $sendGridApiMessage = new SendGridApiMessage($sendGridMailBase, $sendGridMailPersonalization, $sendGridMailMetadata, $sendGridMailAttachment, $sendGridMailHeader); + $sendGridApiMessage = new SendGridApiMessage($sendGridMailBase, $sendGridMailPersonalization, $sendGridMailMetadata, $sendGridMailAttachment); $sendGridMailBase->expects($this->once()) ->method('getSendGridMail') @@ -70,10 +65,6 @@ public function testGetMail() ->method('addAttachmentsToMail') ->with($mail, $message); - $sendGridMailHeader->expects($this->once()) - ->method('addHeadersToMail') - ->with($mail, $message); - $result = $sendGridApiMessage->getMessage($message); $this->assertSame($mail, $result); From d1d1f6c2bb76ac1b283b0b2613bcee0042a965fa Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 15:05:01 -0500 Subject: [PATCH 365/778] Remove headers not allowed for momentum --- .../Momentum/Service/SwiftMessageService.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php index 02ef644768e..86170de9787 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php +++ b/app/bundles/EmailBundle/Swiftmailer/Momentum/Service/SwiftMessageService.php @@ -22,6 +22,17 @@ */ final class SwiftMessageService implements SwiftMessageServiceInterface { + private $reservedKeys = [ + 'MIME-Version', + 'Content-Type', + 'Content-Transfer-Encoding', + 'To', + 'From', + 'Subject', + 'Reply-To', + 'BCC', + ]; + /** * @var TranslatorInterface */ @@ -62,12 +73,7 @@ public function transformToTransmission(\Swift_Mime_Message $message) $headers = $message->getHeaders()->getAll(); /** @var \Swift_Mime_Header $header */ foreach ($headers as $header) { - if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && - !in_array($header->getFieldName(), [ - 'Content-Transfer-Encoding', - 'MIME-Version', - 'Subject', - ])) { + if ($header->getFieldType() == \Swift_Mime_Header::TYPE_TEXT && !in_array($header->getFieldName(), $this->reservedKeys)) { $content->addHeader($header->getFieldName(), $header->getFieldBodyModel()); } } From 4d023639da42dd32394c926e3212956edf7b5ee1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 15:05:12 -0500 Subject: [PATCH 366/778] Fixed tests --- .../Tests/Model/TransportTypeTest.php | 36 +++++-------------- .../Service/SwiftMessageServiceTest.php | 7 ++-- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/app/bundles/EmailBundle/Tests/Model/TransportTypeTest.php b/app/bundles/EmailBundle/Tests/Model/TransportTypeTest.php index 4e38c88d698..9b4d9d801aa 100644 --- a/app/bundles/EmailBundle/Tests/Model/TransportTypeTest.php +++ b/app/bundles/EmailBundle/Tests/Model/TransportTypeTest.php @@ -68,39 +68,25 @@ public function testRequiresLogin() { $transportType = new TransportType(); - $expected = '"mautic.transport.mandrill", - "mautic.transport.mailjet", - "mautic.transport.sendgrid", - "mautic.transport.elasticemail", - "mautic.transport.amazon", - "mautic.transport.postmark", - "gmail"'; - - $this->assertSame($expected, $transportType->getServiceRequiresLogin()); + $expected = '"mautic.transport.mailjet","mautic.transport.sendgrid","mautic.transport.elasticemail","mautic.transport.amazon","mautic.transport.postmark","gmail"'; + + $this->assertSame($expected, $transportType->getServiceRequiresUser()); } public function testDoNotNeedLogin() { $transportType = new TransportType(); - $expected = '"mail", - "sendmail", - "mautic.transport.sparkpost", - "mautic.transport.sendgrid_api"'; + $expected = '"mautic.transport.mandrill","mail","mautic.transport.sendgrid_api","sendmail","mautic.transport.sparkpost"'; - $this->assertSame($expected, $transportType->getServiceDoNotNeedLogin()); + $this->assertSame($expected, $transportType->getServiceDoNotNeedUser()); } public function testRequiresPassword() { $transportType = new TransportType(); - $expected = '"mautic.transport.elasticemail", - "mautic.transport.sendgrid", - "mautic.transport.amazon", - "mautic.transport.postmark", - "mautic.transport.mailjet", - "gmail"'; + $expected = '"mautic.transport.mailjet","mautic.transport.sendgrid","mautic.transport.elasticemail","mautic.transport.amazon","mautic.transport.postmark","gmail"'; $this->assertSame($expected, $transportType->getServiceRequiresPassword()); } @@ -109,11 +95,7 @@ public function testDoNotNeedPassword() { $transportType = new TransportType(); - $expected = '"mail", - "sendmail", - "mautic.transport.sparkpost", - "mautic.transport.mandrill", - "mautic.transport.sendgrid_api"'; + $expected = '"mautic.transport.mandrill","mail","mautic.transport.sendgrid_api","sendmail","mautic.transport.sparkpost"'; $this->assertSame($expected, $transportType->getServiceDoNotNeedPassword()); } @@ -122,9 +104,7 @@ public function testRequiresApiKey() { $transportType = new TransportType(); - $expected = '"mautic.transport.sparkpost", - "mautic.transport.mandrill", - "mautic.transport.sendgrid_api"'; + $expected = '"mautic.transport.sparkpost","mautic.transport.mandrill","mautic.transport.sendgrid_api"'; $this->assertSame($expected, $transportType->getServiceRequiresApiKey()); } diff --git a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php index 87ab3fd98a3..3ecb837ef94 100644 --- a/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php +++ b/app/bundles/EmailBundle/Tests/Swiftmailer/Momentum/Service/SwiftMessageServiceTest.php @@ -116,9 +116,12 @@ private function geTransformToTransmissionComplexData() { "type": "text\/plain", "name": "sample.txt", - "content": "VGhpcyBpcyBzYW1wbGUgYXR0YWNobWVudAo=" + "data": "VGhpcyBpcyBzYW1wbGUgYXR0YWNobWVudAo=" } - ] + ], + "headers": { + "CC": "cc1@test.local,cc2@test.local" + } } } '; From b87112d99aeca1b74d06fb3fc0e4a5e34d39cf14 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 15:51:05 -0600 Subject: [PATCH 367/778] Updated labels since not all mailers that leverage host and port are now SMTP --- app/bundles/EmailBundle/Translations/en_US/messages.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index 822077f7cb5..37c8738de64 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -94,14 +94,14 @@ mautic.email.config.mailer.from.email.tooltip="Set the from email for email sent mautic.email.config.mailer.from.email="E-mail address to send mail from" mautic.email.config.mailer.from.name.tooltip="Set the from name for email sent by Mautic" mautic.email.config.mailer.from.name="Name to send mail as" -mautic.email.config.mailer.host.tooltip="Set the host for SMTP server" -mautic.email.config.mailer.host="SMTP host" +mautic.email.config.mailer.host.tooltip="Set the host for mail server" +mautic.email.config.mailer.host="Host" mautic.email.config.mailer.is.owner.tooltip="If a contact owner is known, force his/her email and name as sender email and name" mautic.email.config.mailer.is.owner="Mailer is owner" mautic.email.config.mailer.password.tooltip="Set the password required to authenticate the selected mail service" mautic.email.config.mailer.password="Password for the selected mail service" -mautic.email.config.mailer.port.tooltip="Set the port for the SMTP server" -mautic.email.config.mailer.port="SMTP port" +mautic.email.config.mailer.port.tooltip="Set the port for the mail server" +mautic.email.config.mailer.port="Port" mautic.email.config.mailer.return.path.tooltip="Set a custom return path/bounce email for emails sent from the system. Note that some mail transports, such as Gmail, will not support this." mautic.email.config.mailer.return.path="Custom return path (bounce) address" mautic.email.config.mailer.spool.clear.timeout.tooltip="Sets the amount of time in seconds before deleting failed messages" From cd7e0b18a11f06665543548898e20a3d1bc83f87 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 15:56:03 -0600 Subject: [PATCH 368/778] Prevent killing Mautic if host is empty for Momentum because a SparkPost object is required as a dependency for the momentum service --- .../Sparkpost/SparkpostFactory.php | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php index 10594e3868d..cf916ef29cd 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php @@ -38,19 +38,19 @@ public function create($host, $apiKey, $port = null) $host = 'https://'.$host; } - $hostInfo = parse_url($host); + $options = [ + 'host' => '', + 'protocol' => 'https', + 'port' => $port, + 'key' => $apiKey, + ]; + $hostInfo = parse_url($host); if ($hostInfo) { if (empty($port)) { - $port = $hostInfo['scheme'] === 'https' ? 443 : 80; + $options['port'] = $hostInfo['scheme'] === 'https' ? 443 : 80; } - $options = [ - 'protocol' => $hostInfo['scheme'], - 'port' => $port, - 'key' => $apiKey, - ]; - $host = $hostInfo['host']; if (isset($hostInfo['path'])) { $path = $hostInfo['path']; @@ -67,10 +67,9 @@ public function create($host, $apiKey, $port = null) } $options['host'] = $host; - - return new SparkPost($this->client, $options); - } else { - // problem :/ } + + // Must always return a SparkPost host or else Symfony will fail to build the container if host is empty + return new SparkPost($this->client, $options); } } From 1b902582f5953e64b36c7d64845d24e0eb31c7b6 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 16:04:35 -0600 Subject: [PATCH 369/778] Fixed tests --- .../ConfigBundle/Tests/Form/Helper/RestrictionHelperTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/ConfigBundle/Tests/Form/Helper/RestrictionHelperTest.php b/app/bundles/ConfigBundle/Tests/Form/Helper/RestrictionHelperTest.php index 3b0b7d20b00..75bf0e8dae2 100644 --- a/app/bundles/ConfigBundle/Tests/Form/Helper/RestrictionHelperTest.php +++ b/app/bundles/ConfigBundle/Tests/Form/Helper/RestrictionHelperTest.php @@ -270,6 +270,8 @@ function ($key) { $transportType = $this->getMockBuilder(TransportType::class) ->disableOriginalConstructor() ->getMock(); + $transportType->method('getTransportTypes') + ->willReturn([]); // This is what we're really testing here $restrictionHelper = new RestrictionHelper($translator, $this->restrictedFields, $this->displayMode); From 2d14c4fd1377de60365864b6d63c2b2d1bb32701 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 17:08:48 -0600 Subject: [PATCH 370/778] Display headers in the details --- .../Templating/Helper/FormatterHelper.php | 16 ++++++++++++++++ .../EmailBundle/Views/Email/details.html.php | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php b/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php index ad4fd9d3674..a50711e525a 100644 --- a/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php +++ b/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php @@ -151,6 +151,22 @@ public function arrayToString($array, $delimiter = ', ') return $array; } + /** + * @param array $array + * @param string $delimeter + * + * @return string + */ + public function simpleArrayToHtml(array $array, $delimeter = '
') + { + $pairs = []; + foreach ($array as $key => $value) { + $pairs[] = "$key: $value"; + } + + return implode($delimeter, $pairs); + } + /** * @return string */ diff --git a/app/bundles/EmailBundle/Views/Email/details.html.php b/app/bundles/EmailBundle/Views/Email/details.html.php index 7e0860d4f6e..83f0396c508 100644 --- a/app/bundles/EmailBundle/Views/Email/details.html.php +++ b/app/bundles/EmailBundle/Views/Email/details.html.php @@ -193,6 +193,14 @@ + getHeaders()): ?> + + + trans('mautic.email.custom_headers'); ?> + + simpleArrayToHtml($headers); ?> + +
From 34b55765bc6fe46499d0c48ebe4253b759f09b1f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 17:10:19 -0600 Subject: [PATCH 371/778] Set headers prior to search/replace for non-tokenized mailers. Decode headers for things like . Auto append unsubscription URL to List-Unsubscribe header. --- app/bundles/EmailBundle/Helper/MailHelper.php | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index afa2cfba1f9..31023256507 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -369,9 +369,6 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne if (empty($this->fatal)) { if (!$isQueueFlush) { - // Only add unsubscribe header to one-off sends as tokenized sends are built by the transport - $this->addUnsubscribeHeader(); - // Search/replace tokens if this is not a queue flush // Generate tokens from listeners @@ -391,6 +388,8 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne } $this->setMessagePlainText(); + $this->setMessageHeaders(); + if (!$isQueueFlush) { // Replace token content $tokens = $this->getTokens(); @@ -437,8 +436,6 @@ public function send($dispatchSendEvent = false, $isQueueFlush = false, $useOwne } } - $this->setMessageHeaders(); - try { $failures = []; @@ -678,6 +675,7 @@ public function reset($cleanSlate = true) $this->queueEnabled = false; $this->from = $this->systemFrom; $this->headers = []; + $this->systemHeaders = []; $this->source = []; $this->assets = []; $this->globalTokens = []; @@ -1434,6 +1432,9 @@ public function setEmail(Email $email, $allowBcc = true, $slots = [], $assetAtta // Set custom headers if ($headers = $email->getHeaders()) { + // HTML decode headers + $headers = array_map('html_entity_decode', $headers); + foreach ($headers as $name => $value) { $this->addCustomHeader($name, $value); } @@ -1476,18 +1477,18 @@ public function getCustomHeaders() $headers = $this->headers; $systemHeaders = $this->getSystemHeaders(); - return array_merge($headers, $systemHeaders); + return array_merge($this->getUnsubscribeHeader(), $headers, $systemHeaders); } /** - * Generate and insert List-Unsubscribe header. + * @return array */ - private function addUnsubscribeHeader() + private function getUnsubscribeHeader() { - if (isset($this->idHash)) { - $unsubscribeLink = $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], true); - $this->headers['List-Unsubscribe'] = "<$unsubscribeLink>"; - } + $unsubscribeLink = ($this->idHash) ? $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], true) : + '<{unsubscribe_url}>'; + + return ['List-Unsubscribe' => "<$unsubscribeLink>"]; } /** @@ -2082,6 +2083,9 @@ private function getSystemHeaders() return []; } + // HTML decode headers + $systemHeaders = array_map('html_entity_decode', $systemHeaders); + return $systemHeaders; } From aa5109bddc6897faa53f7b57471f2c088304692d Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 17:10:56 -0600 Subject: [PATCH 372/778] Encode header values to support custom List-Unsubscribes and the like --- app/bundles/EmailBundle/Form/Type/ConfigType.php | 3 +++ app/bundles/EmailBundle/Form/Type/EmailType.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Form/Type/ConfigType.php b/app/bundles/EmailBundle/Form/Type/ConfigType.php index d5bfceb0f96..b8853cc5bd7 100644 --- a/app/bundles/EmailBundle/Form/Type/ConfigType.php +++ b/app/bundles/EmailBundle/Form/Type/ConfigType.php @@ -11,6 +11,7 @@ namespace Mautic\EmailBundle\Form\Type; +use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber; use Mautic\CoreBundle\Form\Type\SortableListType; use Mautic\EmailBundle\Model\TransportType; use Symfony\Component\Form\AbstractType; @@ -53,6 +54,8 @@ public function __construct(TranslatorInterface $translator, TransportType $tran */ public function buildForm(FormBuilderInterface $builder, array $options) { + $builder->addEventSubscriber(new CleanFormSubscriber(['mailer_custom_headers' => 'clean'])); + $builder->add( 'unsubscribe_text', 'textarea', diff --git a/app/bundles/EmailBundle/Form/Type/EmailType.php b/app/bundles/EmailBundle/Form/Type/EmailType.php index 97668415939..67f8cbd3aac 100644 --- a/app/bundles/EmailBundle/Form/Type/EmailType.php +++ b/app/bundles/EmailBundle/Form/Type/EmailType.php @@ -73,7 +73,7 @@ public function __construct(MauticFactory $factory) */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html', 'customHtml' => 'html'])); + $builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html', 'customHtml' => 'html', 'headers' => 'clean'])); $builder->addEventSubscriber(new FormExitSubscriber('email.email', $options)); $builder->add( From ff6c3f8f1dafd4b4fea26f5b018ef485564c345a Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 17:24:20 -0600 Subject: [PATCH 373/778] Generate List-Unsubscribe header only when appropriate --- app/bundles/EmailBundle/Helper/MailHelper.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 31023256507..498e2e23579 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -534,6 +534,9 @@ public function queue($dispatchSendEvent = false, $returnMode = self::QUEUE_RESE // Reset recipients $this->queuedRecipients = []; + // Reset hash which is unique to the recipient + $this->idHash = null; + // Assume success return (self::QUEUE_RETURN_ERRORS) ? [true, []] : true; } else { @@ -1485,10 +1488,17 @@ public function getCustomHeaders() */ private function getUnsubscribeHeader() { - $unsubscribeLink = ($this->idHash) ? $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], true) : - '<{unsubscribe_url}>'; + if ($this->idHash) { + $url = $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], true); + + return ['List-Unsubscribe' => "<$url>"]; + } + + if (!empty($this->queuedRecipients) || !empty($this->lead)) { + return ['List-Unsubscribe' => '<{unsubscribe_url}>']; + } - return ['List-Unsubscribe' => "<$unsubscribeLink>"]; + return []; } /** From f8ed9c73aa077e7016024b2681ff4e6cbe580cac Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 17:31:18 -0600 Subject: [PATCH 374/778] Ensure Mautic's List-Unsubscribe header is always part of the email if appropriate --- app/bundles/EmailBundle/Helper/MailHelper.php | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 498e2e23579..1bddb2ba2ad 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -1477,28 +1477,37 @@ public function addCustomHeader($name, $value) */ public function getCustomHeaders() { - $headers = $this->headers; - $systemHeaders = $this->getSystemHeaders(); + $headers = array_merge($this->headers, $this->getSystemHeaders()); - return array_merge($this->getUnsubscribeHeader(), $headers, $systemHeaders); + $listUnsubscribeHeader = $this->getUnsubscribeHeader(); + if ($listUnsubscribeHeader) { + if (!empty($headers['List-Unsubscribe'])) { + // Ensure Mautic's is always part of this header + $headers['List-Unsubscribe'] .= ','.$listUnsubscribeHeader; + } else { + $headers['List-Unsubscribe'] = $listUnsubscribeHeader; + } + } + + return $headers; } /** - * @return array + * @return bool|string */ private function getUnsubscribeHeader() { if ($this->idHash) { $url = $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], true); - return ['List-Unsubscribe' => "<$url>"]; + return "<$url>"; } if (!empty($this->queuedRecipients) || !empty($this->lead)) { - return ['List-Unsubscribe' => '<{unsubscribe_url}>']; + return '<{unsubscribe_url}>'; } - return []; + return false; } /** From d97ffb8e4a47ccf0d3666ec6b88fc1395d77716b Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 14 May 2018 17:52:32 -0600 Subject: [PATCH 375/778] Fixed setting the protocol based on host info --- .../EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php index cf916ef29cd..9cb44fe6fd1 100644 --- a/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php +++ b/app/bundles/EmailBundle/Swiftmailer/Sparkpost/SparkpostFactory.php @@ -47,6 +47,8 @@ public function create($host, $apiKey, $port = null) $hostInfo = parse_url($host); if ($hostInfo) { + $options['protocol'] = $hostInfo['scheme']; + if (empty($port)) { $options['port'] = $hostInfo['scheme'] === 'https' ? 443 : 80; } From a80ff2eeaa3b77adf2e3fdf15a586dfabb884d2b Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 15 May 2018 10:43:49 +0200 Subject: [PATCH 376/778] A migration for instances created on 2.13.1 added --- app/migrations/Version20180515082957.php | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/migrations/Version20180515082957.php diff --git a/app/migrations/Version20180515082957.php b/app/migrations/Version20180515082957.php new file mode 100644 index 00000000000..463fdb80c2a --- /dev/null +++ b/app/migrations/Version20180515082957.php @@ -0,0 +1,40 @@ +getTable($this->prefix.'webhook_queue')->getColumn('payload')->getType() instanceof TextType) { + throw new SkipMigrationException('Schema includes this migration'); + } + } + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->addSql("ALTER TABLE {$this->prefix}webhook_queue CHANGE payload payload LONGTEXT NOT NULL"); + } +} From ebcb60c693f18e5c6c3966679c7dc53718e08d46 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Tue, 15 May 2018 12:56:52 +0200 Subject: [PATCH 377/778] Add column for contact ID, escape echoed values --- .../Entity/SubmissionRepository.php | 2 +- .../Translations/en_US/messages.ini | 1 + .../FormBundle/Views/Result/list.html.php | 29 ++++++++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/bundles/FormBundle/Entity/SubmissionRepository.php b/app/bundles/FormBundle/Entity/SubmissionRepository.php index 207ef2931a4..53530d2a46d 100644 --- a/app/bundles/FormBundle/Entity/SubmissionRepository.php +++ b/app/bundles/FormBundle/Entity/SubmissionRepository.php @@ -98,7 +98,7 @@ function ($value) { $dq->resetQueryPart('select'); $fieldAliasSql = (!empty($fieldAliases)) ? ', '.implode(',r.', $fieldAliases) : ''; - $dq->select('r.submission_id, s.date_submitted as dateSubmitted,s.referer,i.ip_address as ipAddress'.$fieldAliasSql); + $dq->select('r.submission_id, s.date_submitted as dateSubmitted, s.lead_id as leadId, s.referer, i.ip_address as ipAddress'.$fieldAliasSql); $results = $dq->execute()->fetchAll(); //loop over results to put form submission results in something that can be assigned to the entities diff --git a/app/bundles/FormBundle/Translations/en_US/messages.ini b/app/bundles/FormBundle/Translations/en_US/messages.ini index 1e1c5dd62e7..db76fa5be7b 100644 --- a/app/bundles/FormBundle/Translations/en_US/messages.ini +++ b/app/bundles/FormBundle/Translations/en_US/messages.ini @@ -165,6 +165,7 @@ mautic.form.report.form_id="Form ID" mautic.form.report.page_id="Page ID" mautic.form.report.page_name="Page name" mautic.form.report.submission.table="Form Submissions" +mautic.form.report.submission.id="Submission ID" mautic.form.report.submit.date_submitted="Date submitted" mautic.form.result.export.csv="Export to CSV" mautic.form.result.export.html="Export to HTML" diff --git a/app/bundles/FormBundle/Views/Result/list.html.php b/app/bundles/FormBundle/Views/Result/list.html.php index 99874dd0674..82e54a61760 100644 --- a/app/bundles/FormBundle/Views/Result/list.html.php +++ b/app/bundles/FormBundle/Views/Result/list.html.php @@ -35,11 +35,19 @@ echo $view->render('MauticCoreBundle:Helper:tableheader.html.php', [ 'sessionVar' => 'formresult.'.$formId, 'orderBy' => 's.id', - 'text' => 'mautic.core.id', + 'text' => 'mautic.form.report.submission.id', 'class' => 'col-formresult-id', 'filterBy' => 's.id', ]); + echo $view->render('MauticCoreBundle:Helper:tableheader.html.php', [ + 'sessionVar' => 'formresult.'.$formId, + 'orderBy' => 's.lead_id', + 'text' => 'mautic.lead.report.contact_id', + 'class' => 'col-formresult-lead-id', + 'filterBy' => 's.lead_id', + ]); + echo $view->render('MauticCoreBundle:Helper:tableheader.html.php', [ 'sessionVar' => 'formresult.'.$formId, 'orderBy' => 's.date_submitted', @@ -100,28 +108,27 @@ - + escape($item['id']); ?> - - - toFull($item['dateSubmitted']); ?> + + + escape($item['leadId']); ?> - - toFull($item['dateSubmitted']); ?> - + toFull($item['dateSubmitted']); ?> + escape($item['ipAddress']); ?> $r): ?> > - + escape(nl2br($r['value'])); ?> - + escape($r['value']); ?> - + escape($r['value']); ?> From 3ea98f3075d2c611841989b8d54e462421f08c1f Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 15 May 2018 15:04:02 +0200 Subject: [PATCH 378/778] Test for negative condition to include NULL values (name != xxx should include NULL) --- .../Tests/DataFixtures/ORM/LoadSegmentsData.php | 17 +++++++++++++++++ .../ContactSegmentServiceFunctionalTest.php | 8 ++++++++ 2 files changed, 25 insertions(+) diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index 04d727c8fd1..d8487f64823 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -824,6 +824,23 @@ public function load(ObjectManager $manager) ], 'populate' => false, ], + [ // ID 39 + 'name' => 'Name is not equal (not null test)', + 'alias' => 'name-is-not-equal-not-null-test', + 'public' => true, + 'filters' => [ + [ + 'glue' => 'and', + 'type' => 'text', + 'object' => 'lead', + 'field' => 'firstname', + 'operator' => '!=', + 'filter' => 'xxxxx', + 'display' => null, + ], + ], + 'populate' => false, + ], ]; foreach ($segments as $segmentConfig) { diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php index f8e89fb8bbf..32c87486005 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentServiceFunctionalTest.php @@ -191,6 +191,14 @@ public function testSegmentCountIsCorrect() $segmentContacts[$segmentMembershipCompanyOnlyFields->getId()]['count'], 'There should be 14 in this segment.' ); + + $segmentMembershipCompanyOnlyFields = $this->fixtures->getReference('name-is-not-equal-not-null-test'); + $segmentContacts = $contactSegmentService->getTotalLeadListLeadsCount($segmentMembershipCompanyOnlyFields); + $this->assertEquals( + 54, + $segmentContacts[$segmentMembershipCompanyOnlyFields->getId()]['count'], + 'There should be 54 in this segment. Check that contact with NULL firstname were added if error here' + ); } public function testSegmentRebuildCommand() From 4b85ecc323c0653f279c0c732d29114c03e1ac90 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 15 May 2018 08:42:05 -0600 Subject: [PATCH 379/778] Fixed issue with saving hash to stat --- app/bundles/EmailBundle/Helper/MailHelper.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 1bddb2ba2ad..8640934ad85 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -534,9 +534,6 @@ public function queue($dispatchSendEvent = false, $returnMode = self::QUEUE_RESE // Reset recipients $this->queuedRecipients = []; - // Reset hash which is unique to the recipient - $this->idHash = null; - // Assume success return (self::QUEUE_RETURN_ERRORS) ? [true, []] : true; } else { From 83c5ad9e619ffcda40e82b5c313baca8c21c2852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=20=E2=98=95?= Date: Tue, 15 May 2018 12:28:35 -0400 Subject: [PATCH 380/778] Add an index for manually_removed. This is used in queries for pulling segments which get very slow with 1+million leads. Adding this index speeds those up by ~20%. --- app/bundles/LeadBundle/Entity/ListLead.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/LeadBundle/Entity/ListLead.php b/app/bundles/LeadBundle/Entity/ListLead.php index 7cec12182eb..c4f86049347 100644 --- a/app/bundles/LeadBundle/Entity/ListLead.php +++ b/app/bundles/LeadBundle/Entity/ListLead.php @@ -71,6 +71,8 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) $builder->createField('manuallyAdded', 'boolean') ->columnName('manually_added') ->build(); + + $builder->addIndex(['manually_removed'], 'manually_removed'); } /** From 857dd610eb0576c5e6338f45d0a990b9c75599f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=20=E2=98=95?= Date: Tue, 15 May 2018 13:15:47 -0400 Subject: [PATCH 381/778] Remove extra whitespace. --- app/bundles/LeadBundle/Entity/ListLead.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/ListLead.php b/app/bundles/LeadBundle/Entity/ListLead.php index c4f86049347..49b39222794 100644 --- a/app/bundles/LeadBundle/Entity/ListLead.php +++ b/app/bundles/LeadBundle/Entity/ListLead.php @@ -71,7 +71,7 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) $builder->createField('manuallyAdded', 'boolean') ->columnName('manually_added') ->build(); - + $builder->addIndex(['manually_removed'], 'manually_removed'); } From fe0ba6ea8f41c448c6d19645b05f50fb3a962095 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 16 May 2018 09:54:03 +0200 Subject: [PATCH 382/778] bugfix: 264,265,266. add null values as accepted to noEqual and simillar --- .../Query/Filter/BaseFilterQueryBuilder.php | 80 ++++++++++--------- .../Filter/ForeignValueFilterQueryBuilder.php | 16 ++-- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 36dd97c877a..83101e46601 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -65,7 +65,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil foreach ($filterParameters as $filterParameter) { $parameters[] = $this->generateRandomParameterName(); } - } else { + } + else { $parameters = $this->generateRandomParameterName(); } @@ -81,77 +82,80 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); if ($filterAggr) { - if ($filter->getTable() != MAUTIC_TABLE_PREFIX.'leads') { + if ($filter->getTable() != MAUTIC_TABLE_PREFIX . 'leads') { throw new InvalidUseException('You should use ForeignFuncFilterQueryBuilder instead.'); } $queryBuilder->leftJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) - ); - } else { - if ($filter->getTable() == MAUTIC_TABLE_PREFIX.'companies') { + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), $tableAlias) + ); + } + else { + if ($filter->getTable() == MAUTIC_TABLE_PREFIX . 'companies') { $relTable = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); - $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); - } else { + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX . 'companies_leads', $relTable, $relTable . '.lead_id = l.id'); + $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias . '.id = ' . $relTable . '.company_id'); + } + else { $queryBuilder->leftJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), - $filter->getTable(), - $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) - ); + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), + $filter->getTable(), + $tableAlias, + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), $tableAlias) + ); } } } switch ($filterOperator) { case 'empty': - $expression = $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()); - + $expression = $queryBuilder->expr()->isNull($tableAlias . '.' . $filter->getField()); break; case 'notEmpty': - $expression = $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()); + $expression = $queryBuilder->expr()->isNotNull($tableAlias . '.' . $filter->getField()); break; case 'neq': - if ($filter->isColumnTypeBoolean() && $filter->getParameterValue() == 1) { - $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->$filterOperator( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ) - ); - break; // Break will be performed only if the condition above matches - } + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias . '.' . $filter->getField()), + $queryBuilder->expr()->$filterOperator( + $tableAlias . '.' . $filter->getField(), + $filterParametersHolder + ) + ); + break; case 'startsWith': case 'endsWith': case 'gt': case 'eq': case 'gte': case 'like': - case 'notLike': case 'lt': case 'lte': - case 'notIn': case 'in': case 'regexp': + $expression = $queryBuilder->expr()->$filterOperator( + $tableAlias . '.' . $filter->getField(), + $filterParametersHolder + ); + break; + case 'notLike': + case 'notIn': case 'between': case 'notBetween': case 'notRegexp': - $expression = $queryBuilder->expr()->$filterOperator( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ); + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->$filterOperator($tableAlias . '.' . $filter->getField(),$filterParametersHolder) + , $queryBuilder->expr()->isNull($tableAlias . '.' . $filter->getField())); break; default: - throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); + throw new \Exception('Dunno how to handle operator "' . $filterOperator . '"'); } if ($queryBuilder->isJoinTable($filter->getTable())) { - $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); + $queryBuilder->addJoinCondition($tableAlias, ' (' . $expression . ')'); } $queryBuilder->addLogic($expression, $filterGlue); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 0db60037584..1a0363e972d 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -107,19 +107,21 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->addLogic($expression, 'and'); break; case 'neq': - $expression = $queryBuilder->expr()->eq( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(),$filterParametersHolder), + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()) ); + $queryBuilder->addJoinCondition($tableAlias, $expression); $queryBuilder->setParametersPairs($parameters, $filterParameters); break; case 'notLike': - $expression = $queryBuilder->expr()->like( - $tableAlias.'.'.$filter->getField(), - $filterParametersHolder - ); + $expression = $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->like($tableAlias.'.'.$filter->getField(),$filterParametersHolder) + ) ; + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); $queryBuilder->setParametersPairs($parameters, $filterParameters); break; From 8f1d8413277ab8c023c9a88a3e8cad5448c32025 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 16 May 2018 10:26:44 +0200 Subject: [PATCH 383/778] Fix CS Fixer issues --- .../Query/Filter/BaseFilterQueryBuilder.php | 42 +++++++++---------- .../Filter/ForeignValueFilterQueryBuilder.php | 7 ++-- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 83101e46601..e39beb3b208 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -65,8 +65,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil foreach ($filterParameters as $filterParameter) { $parameters[] = $this->generateRandomParameterName(); } - } - else { + } else { $parameters = $this->generateRandomParameterName(); } @@ -82,28 +81,26 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil if (!$tableAlias) { $tableAlias = $this->generateRandomParameterName(); if ($filterAggr) { - if ($filter->getTable() != MAUTIC_TABLE_PREFIX . 'leads') { + if ($filter->getTable() != MAUTIC_TABLE_PREFIX.'leads') { throw new InvalidUseException('You should use ForeignFuncFilterQueryBuilder instead.'); } $queryBuilder->leftJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); - } - else { - if ($filter->getTable() == MAUTIC_TABLE_PREFIX . 'companies') { + } else { + if ($filter->getTable() == MAUTIC_TABLE_PREFIX.'companies') { $relTable = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX . 'companies_leads', $relTable, $relTable . '.lead_id = l.id'); - $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias . '.id = ' . $relTable . '.company_id'); - } - else { + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'companies_leads', $relTable, $relTable.'.lead_id = l.id'); + $queryBuilder->leftJoin($relTable, $filter->getTable(), $tableAlias, $tableAlias.'.id = '.$relTable.'.company_id'); + } else { $queryBuilder->leftJoin( - $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), + $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, - sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX . 'leads'), $tableAlias) + sprintf('%s.id = %s.lead_id', $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $tableAlias) ); } } @@ -111,16 +108,16 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil switch ($filterOperator) { case 'empty': - $expression = $queryBuilder->expr()->isNull($tableAlias . '.' . $filter->getField()); + $expression = $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()); break; case 'notEmpty': - $expression = $queryBuilder->expr()->isNotNull($tableAlias . '.' . $filter->getField()); + $expression = $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()); break; case 'neq': $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($tableAlias . '.' . $filter->getField()), + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), $queryBuilder->expr()->$filterOperator( - $tableAlias . '.' . $filter->getField(), + $tableAlias.'.'.$filter->getField(), $filterParametersHolder ) ); @@ -136,7 +133,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'in': case 'regexp': $expression = $queryBuilder->expr()->$filterOperator( - $tableAlias . '.' . $filter->getField(), + $tableAlias.'.'.$filter->getField(), $filterParametersHolder ); break; @@ -146,16 +143,15 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'notBetween': case 'notRegexp': $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->$filterOperator($tableAlias . '.' . $filter->getField(),$filterParametersHolder) - , $queryBuilder->expr()->isNull($tableAlias . '.' . $filter->getField())); + $queryBuilder->expr()->$filterOperator($tableAlias.'.'.$filter->getField(), $filterParametersHolder), $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField())); break; default: - throw new \Exception('Dunno how to handle operator "' . $filterOperator . '"'); + throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); } if ($queryBuilder->isJoinTable($filter->getTable())) { - $queryBuilder->addJoinCondition($tableAlias, ' (' . $expression . ')'); + $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); } $queryBuilder->addLogic($expression, $filterGlue); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 1a0363e972d..96b3ce5eddc 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -108,19 +108,18 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil break; case 'neq': $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(),$filterParametersHolder), + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), $filterParametersHolder), $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()) ); - $queryBuilder->addJoinCondition($tableAlias, $expression); $queryBuilder->setParametersPairs($parameters, $filterParameters); break; case 'notLike': $expression = $queryBuilder->expr()->orX( $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->like($tableAlias.'.'.$filter->getField(),$filterParametersHolder) - ) ; + $queryBuilder->expr()->like($tableAlias.'.'.$filter->getField(), $filterParametersHolder) + ); $queryBuilder->addJoinCondition($tableAlias, ' ('.$expression.')'); $queryBuilder->setParametersPairs($parameters, $filterParameters); From f447eb6587da192bfa640b2270941afb11ed6a7b Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Wed, 16 May 2018 10:44:30 +0200 Subject: [PATCH 384/778] put back select for minId to countWrapper, gone somehow :-( --- .../LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index d56c4cff408..ca43e066918 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -151,7 +151,7 @@ public function wrapInCount(QueryBuilder $qb) $qb->addSelect($select); } - $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId') + $queryBuilder->select('count(leadIdPrimary) count, max(leadIdPrimary) maxId, min(leadIdPrimary) minId') ->from('('.$qb->getSQL().')', 'sss'); $queryBuilder->setParameters($qb->getParameters()); From c76bdc398787024caf71991dad4c112c93428a48 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 16 May 2018 12:05:15 +0200 Subject: [PATCH 385/778] Fix between operator for date filter - Between should not contains OR NOT NULL condition --- .../LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index e39beb3b208..9d03de111f7 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -131,6 +131,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'lt': case 'lte': case 'in': + case 'between': case 'regexp': $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), @@ -139,7 +140,6 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil break; case 'notLike': case 'notIn': - case 'between': case 'notBetween': case 'notRegexp': $expression = $queryBuilder->expr()->orX( From 15e281fd85f68a40b85c17e39fa4a8c7390e6b47 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 16 May 2018 09:52:19 -0600 Subject: [PATCH 386/778] Prevent stripping HTML from token content --- app/bundles/EmailBundle/Form/Type/ConfigType.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Form/Type/ConfigType.php b/app/bundles/EmailBundle/Form/Type/ConfigType.php index b8853cc5bd7..2c1d62be80c 100644 --- a/app/bundles/EmailBundle/Form/Type/ConfigType.php +++ b/app/bundles/EmailBundle/Form/Type/ConfigType.php @@ -54,7 +54,20 @@ public function __construct(TranslatorInterface $translator, TransportType $tran */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addEventSubscriber(new CleanFormSubscriber(['mailer_custom_headers' => 'clean'])); + $builder->addEventSubscriber( + new CleanFormSubscriber( + [ + 'mailer_from_email' => 'email', + 'mailer_return_path' => 'email', + 'default_signature_text' => 'html', + 'unsubscribe_text' => 'html', + 'unsubscribe_message' => 'html', + 'resubscribe_message' => 'html', + 'webview_text' => 'html', + 'mailer_custom_headers' => 'clean', + ] + ) + ); $builder->add( 'unsubscribe_text', From 4cf9680fc72e739ad87b1458130cb00ab30a9f72 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 16 May 2018 09:53:28 -0600 Subject: [PATCH 387/778] Added a note --- app/bundles/EmailBundle/Form/Type/ConfigType.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/bundles/EmailBundle/Form/Type/ConfigType.php b/app/bundles/EmailBundle/Form/Type/ConfigType.php index 2c1d62be80c..44cca7c6990 100644 --- a/app/bundles/EmailBundle/Form/Type/ConfigType.php +++ b/app/bundles/EmailBundle/Form/Type/ConfigType.php @@ -64,6 +64,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'unsubscribe_message' => 'html', 'resubscribe_message' => 'html', 'webview_text' => 'html', + // Encode special chars to keep congruent with Email entity custom headers 'mailer_custom_headers' => 'clean', ] ) From ae787c308724f4550b7f7a6d1814663a4f3cbf18 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 16 May 2018 18:21:11 +0200 Subject: [PATCH 388/778] Fix for NOT regex filter - added missing NOT --- .../LeadBundle/Segment/Query/Expression/ExpressionBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index d4d42f09f4c..79fc797b89d 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -193,7 +193,7 @@ public function regexp($x, $y) */ public function notRegexp($x, $y) { - return $this->comparison($x, self::REGEXP, $y); + return 'NOT '.$this->comparison($x, self::REGEXP, $y); } /** From f995db09c3d617fd1f657b6bdc183acf03e1df7b Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 16 May 2018 18:40:10 +0200 Subject: [PATCH 389/778] Remove unnecessary check for arguments - done in another function --- .../LeadBundle/Segment/Query/Expression/ExpressionBuilder.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index 79fc797b89d..8013c79d054 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -149,10 +149,6 @@ public function between($x, $arr) */ public function notBetween($x, $arr) { - if (!is_array($arr) || count($arr) != 2) { - throw new SegmentQueryException('Not between expression expects second argument to be an array with exactly two elements'); - } - return 'NOT '.$this->between($x, $arr); } From 290d4880678a8160df87e8be3c6972664474daee Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 16 May 2018 18:49:03 +0200 Subject: [PATCH 390/778] Fix NULL conditions for NOT operators --- .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 9d03de111f7..70c6a59de17 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -131,19 +131,21 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'lt': case 'lte': case 'in': - case 'between': + case 'between': //Used only for date with week combination (EQUAL [this week, next week, last week]) case 'regexp': + case 'notIn': //Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3. + case 'notRegexp': //Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3. $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), $filterParametersHolder ); break; case 'notLike': - case 'notIn': - case 'notBetween': - case 'notRegexp': + case 'notBetween': //Used only for date with week combination (NOT EQUAL [this week, next week, last week]) $expression = $queryBuilder->expr()->orX( - $queryBuilder->expr()->$filterOperator($tableAlias.'.'.$filter->getField(), $filterParametersHolder), $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField())); + $queryBuilder->expr()->$filterOperator($tableAlias.'.'.$filter->getField(), $filterParametersHolder), + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()) + ); break; default: From 23b29d71d9ccc03c6ea2af413486e76c252a3059 Mon Sep 17 00:00:00 2001 From: heathdutton Date: Wed, 16 May 2018 16:20:59 -0400 Subject: [PATCH 391/778] Add migration for manually_removed on lead_lists_leads. --- app/migrations/Version20180516201301.php | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/migrations/Version20180516201301.php diff --git a/app/migrations/Version20180516201301.php b/app/migrations/Version20180516201301.php new file mode 100644 index 00000000000..b96ffd7b55a --- /dev/null +++ b/app/migrations/Version20180516201301.php @@ -0,0 +1,43 @@ +getTable("{$this->prefix}lead_lists_leads"); + if ($table->hasIndex("{$this->prefix}manually_removed")) { + throw new SkipMigrationException('Schema includes this migration'); + } + } + + /** + * @param Schema $schema + */ + public function up(Schema $schema) + { + $this->addSql("CREATE INDEX {$this->prefix}manually_removed ON {$this->prefix}lead_lists_leads (manually_removed)"); + } +} From 4815c512b44158323f96f464599fb6fcbfa769ed Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 17 May 2018 09:50:18 +0200 Subject: [PATCH 392/778] remove BC breaks, revert function name and remove deprecated error --- app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php | 4 ++-- app/bundles/LeadBundle/Controller/ListController.php | 2 +- .../LeadBundle/DataFixtures/ORM/LoadLeadListData.php | 2 +- app/bundles/LeadBundle/Model/ListModel.php | 7 +------ .../LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php | 2 +- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php index 3b12321c479..39d5497b5ac 100644 --- a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php +++ b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php @@ -59,7 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($list !== null) { $output->writeln(''.$translator->trans('mautic.lead.list.rebuild.rebuilding', ['%id%' => $id]).''); try { - $processed = $listModel->updateLeadList($list, $batch, $max, $output); + $processed = $listModel->rebuildListLeads($list, $batch, $max, $output); } catch (QueryException $e) { $this->getContainer()->get('mautic.logger')->error('Query Builder Exception: '.$e->getMessage()); } @@ -83,7 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln(''.$translator->trans('mautic.lead.list.rebuild.rebuilding', ['%id%' => $l->getId()]).''); - $processed = $listModel->updateLeadList($l, $batch, $max, $output); + $processed = $listModel->rebuildListLeads($l, $batch, $max, $output); $output->writeln( ''.$translator->trans('mautic.lead.list.rebuild.leads_affected', ['%leads%' => $processed]).''."\n" ); diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index fc9938ad06d..50bdd88a9aa 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -686,7 +686,7 @@ public function viewAction($objectId) $listModel = $this->get('mautic.lead.model.list'); $list = $listModel->getEntity($objectId); - $processed = $listModel->updateLeadList($list); + $processed = $listModel->rebuildListLeads($list); /** @var \Mautic\LeadBundle\Model\ListModel $model */ $model = $this->getModel('lead.list'); diff --git a/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php b/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php index 73d8d32d995..51ede36cb7f 100644 --- a/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php +++ b/app/bundles/LeadBundle/DataFixtures/ORM/LoadLeadListData.php @@ -63,7 +63,7 @@ public function load(ObjectManager $manager) $manager->persist($list); $manager->flush(); - $this->container->get('mautic.lead.model.list')->updateLeadList($list); + $this->container->get('mautic.lead.model.list')->rebuildListLeads($list); } /** diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 1702782694b..bdb7d3ad1a3 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -872,7 +872,7 @@ public function getVersionOld(LeadList $entity) * @throws \Doctrine\ORM\ORMException * @throws \Exception */ - public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = false, OutputInterface $output = null) + public function rebuildListLeads(LeadList $leadList, $limit = 100, $maxLeads = false, OutputInterface $output = null) { defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1); @@ -1060,11 +1060,6 @@ public function updateLeadList(LeadList $leadList, $limit = 100, $maxLeads = fal return $leadsProcessed; } - public function rebuildListLeads() - { - throw new \Exception('Deprecated function, use updateLeadList instead'); - } - /** * Add lead to lists. * diff --git a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php index d8487f64823..3298a89600b 100644 --- a/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php +++ b/app/bundles/LeadBundle/Tests/DataFixtures/ORM/LoadSegmentsData.php @@ -865,7 +865,7 @@ protected function createSegment($listConfig, ObjectManager $manager) $manager->flush(); if ($listConfig['populate']) { - $this->container->get('mautic.lead.model.list')->updateLeadList($list); + $this->container->get('mautic.lead.model.list')->rebuildListLeads($list); } if (!empty($listConfig['manually_add'])) { From 8aaca31fc02f8c4120e22b0d510ae37926422811 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 17 May 2018 10:33:20 +0200 Subject: [PATCH 393/778] Remove building segments in detail action of Controller --- app/bundles/LeadBundle/Controller/ListController.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/bundles/LeadBundle/Controller/ListController.php b/app/bundles/LeadBundle/Controller/ListController.php index 50bdd88a9aa..cecbdee4724 100644 --- a/app/bundles/LeadBundle/Controller/ListController.php +++ b/app/bundles/LeadBundle/Controller/ListController.php @@ -682,12 +682,6 @@ protected function changeList($listId, $action) */ public function viewAction($objectId) { - /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ - $listModel = $this->get('mautic.lead.model.list'); - - $list = $listModel->getEntity($objectId); - $processed = $listModel->rebuildListLeads($list); - /** @var \Mautic\LeadBundle\Model\ListModel $model */ $model = $this->getModel('lead.list'); $security = $this->get('mautic.security'); From a25e888ad8fbb62aaab36c936554074b820224be Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 17 May 2018 10:46:41 +0200 Subject: [PATCH 394/778] Remove introduced BC - Logger is optional for getLeadsByList method --- app/bundles/LeadBundle/Entity/LeadListRepository.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index c235ac332a4..89d9e7f03db 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -326,7 +326,7 @@ private function format_period($inputSeconds) * * @return array */ - public function getLeadsByList($lists, $args = [], Logger $logger) + public function getLeadsByList($lists, $args = [], Logger $logger = null) { // Return only IDs $idOnly = (!array_key_exists('idOnly', $args)) ? false : $args['idOnly']; //Always TRUE @@ -528,11 +528,15 @@ public function getLeadsByList($lists, $args = [], Logger $logger) $sqlT = str_replace(":{$key}", $val, $sqlT); } - $logger->debug(sprintf('Old version SQL: %s', $sqlT)); + if (null !== $logger) { + $logger->debug(sprintf('Old version SQL: %s', $sqlT)); + } $timer = microtime(true); $results = $q->execute()->fetchAll(); $timer = microtime(true) - $timer; - $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); + if (null !== $logger) { + $logger->debug(sprintf('Old version SQL took: %s', $this->format_period($timer))); + } foreach ($results as $r) { if ($countOnly) { From dd59512ffc2f75b5848aa62edb3b04dc2b260fd3 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 17 May 2018 13:29:03 +0200 Subject: [PATCH 395/778] Remove BC from ListModel + mark old methods as deprecated --- .../LeadBundle/Entity/LeadListRepository.php | 2 ++ app/bundles/LeadBundle/Model/ListModel.php | 17 ++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 89d9e7f03db..3b4352522f0 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -320,6 +320,8 @@ private function format_period($inputSeconds) } /** + * @deprecated in 2.14, to be removed in Mautic 3 - Use methods in the ContactSegmentService class + * * @param $lists * @param array $args * @param Logger $logger diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index bdb7d3ad1a3..20c7e02751d 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -30,7 +30,6 @@ use Mautic\LeadBundle\Helper\FormFieldHelper; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\ContactSegmentService; -use Monolog\Logger; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -852,8 +851,7 @@ public function getVersionOld(LeadList $entity) 'countOnly' => true, 'newOnly' => true, 'batchLimiters' => $batchLimiters, - ], - $this->logger + ] ); $return = array_shift($newLeadsCount); @@ -1323,18 +1321,19 @@ public function removeLead($lead, $lists, $manuallyRemoved = false, $batchProces } /** - * @param $lists - * @param bool $idOnly - * @param array $args - * @param Logger $logger + * @deprecated in 2.14, to be removed in Mautic 3 - Use methods in the ContactSegmentService class + * + * @param $lists + * @param bool $idOnly + * @param array $args * * @return array */ - public function getLeadsByList($lists, $idOnly = false, array $args = [], Logger $logger) + public function getLeadsByList($lists, $idOnly = false, array $args = []) { $args['idOnly'] = $idOnly; - return $this->getRepository()->getLeadsByList($lists, $args, $logger); + return $this->getRepository()->getLeadsByList($lists, $args, $this->logger); } /** From 99af4668fdda176dd8372bd620b48d788fb2ab08 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 17 May 2018 13:32:22 +0200 Subject: [PATCH 396/778] Remove unnused debug method from ContactSegmentFilter --- .../LeadBundle/Segment/ContactSegmentFilter.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 77f21b53a20..7e11ab69c00 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -196,21 +196,4 @@ public function getDoNotContactParts() { return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); } - - /** - * @return mixed - * - * @throws QueryException - */ - public function __toString() - { - $data = [ - 'column' => $this->getTable().'.'.$this->getField(), - 'operator' => $this->getOperator(), - 'glue' => $this->getGlue(), - 'queryBuilder' => get_class($this->getFilterQueryBuilder()), - ]; - - return print_r($data, true); - } } From 112b01f1e1e2172e8bad7c3693c906fa1320d278 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 17 May 2018 14:00:33 +0200 Subject: [PATCH 397/778] Excluding a list uses NULL condition too --- .../LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 70c6a59de17..d6d3cc16e6d 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -133,7 +133,6 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'in': case 'between': //Used only for date with week combination (EQUAL [this week, next week, last week]) case 'regexp': - case 'notIn': //Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3. case 'notRegexp': //Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3. $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), @@ -142,6 +141,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil break; case 'notLike': case 'notBetween': //Used only for date with week combination (NOT EQUAL [this week, next week, last week]) + case 'notIn': $expression = $queryBuilder->expr()->orX( $queryBuilder->expr()->$filterOperator($tableAlias.'.'.$filter->getField(), $filterParametersHolder), $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()) From 15fb8093344059f6c41897eb492fedb631a8f7f7 Mon Sep 17 00:00:00 2001 From: hammad-tfg <20450902+hammad-tfg@users.noreply.github.com> Date: Fri, 18 May 2018 09:56:16 +1000 Subject: [PATCH 398/778] Update MailHelper.php --- app/bundles/EmailBundle/Helper/MailHelper.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index afa2cfba1f9..b7ad625f263 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -1264,7 +1264,13 @@ public function getIdHash() public function setIdHash($idHash = null, $statToBeGenerated = true) { if ($idHash === null) { - $idHash = uniqid(); + /* + 18.05.2018 - hammad-tfg: + fixed issue # 6093 by using more_entropy parameter to PHP uniqid. + Read more here: http://php.net/manual/en/function.uniqid.php + Issue link: https://github.com/mautic/mautic/issues/6093 + */ + $idHash = str_replace('.','',uniqid('',true));//uniqid(); } $this->idHash = $idHash; From 33ee437ab07ec7a9fa57e9ff76e388f446c79798 Mon Sep 17 00:00:00 2001 From: heathdutton Date: Fri, 18 May 2018 14:02:00 -0400 Subject: [PATCH 399/778] PHPCS fix. --- app/migrations/Version20180516201301.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/migrations/Version20180516201301.php b/app/migrations/Version20180516201301.php index b96ffd7b55a..4bd558f40f9 100644 --- a/app/migrations/Version20180516201301.php +++ b/app/migrations/Version20180516201301.php @@ -10,9 +10,9 @@ namespace Mautic\Migrations; -use Mautic\CoreBundle\Doctrine\AbstractMauticMigration; -use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Migrations\SkipMigrationException; +use Doctrine\DBAL\Schema\Schema; +use Mautic\CoreBundle\Doctrine\AbstractMauticMigration; /** * Adds an index which speeds up the display of very large segments. From 023319623b1cf52cb23437becd6e98231918e995 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 21 May 2018 14:55:15 +0200 Subject: [PATCH 400/778] Add option to exclude visitors from segments --- .../Segment/ContactSegmentService.php | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index fd92f1ab706..e394ee6aeea 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -72,8 +72,6 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); - // I really the following line should be enabled; but it doesn't match with the old results - //$queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId()); return $queryBuilder; } @@ -125,12 +123,10 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters $qb = $this->getNewSegmentContactsQuery($segment, $batchLimiters); - if (isset($batchLimiters['minId'])) { - $qb->andWhere($qb->expr()->gte('l.id', $qb->expr()->literal((int) $batchLimiters['minId']))); - } + $this->addMinMaxLimiters($qb, $batchLimiters); - if (isset($batchLimiters['maxId'])) { - $qb->andWhere($qb->expr()->lte('l.id', $qb->expr()->literal((int) $batchLimiters['maxId']))); + if (!empty($batchLimiters['excludeVisitors'])) { + $this->excludeVisitors($qb); } $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); @@ -192,15 +188,7 @@ public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $li $queryBuilder->setMaxResults($limit); - if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { - $queryBuilder->andWhere( - $queryBuilder->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") - ); - } elseif (!empty($batchLimiters['maxId'])) { - $queryBuilder->andWhere( - $queryBuilder->expr()->lte('l.id', $batchLimiters['maxId']) - ); - } + $this->addMinMaxLimiters($queryBuilder, $batchLimiters); if (!empty($batchLimiters['dateTime'])) { // Only leads in the list at the time of count @@ -209,6 +197,10 @@ public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $li ); } + if (!empty($batchLimiters['excludeVisitors'])) { + $this->excludeVisitors($queryBuilder); + } + $result = $this->timedFetchAll($queryBuilder, $segment->getId()); return [$segment->getId() => $result]; @@ -348,4 +340,33 @@ private function timedFetchAll(QueryBuilder $qb, $segmentId) return $result; } + + /** + * @param QueryBuilder $queryBuilder + * @param array $batchLimiters + */ + private function addMinMaxLimiters(QueryBuilder $queryBuilder, array $batchLimiters) + { + if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { + $queryBuilder->andWhere( + $queryBuilder->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") + ); + } elseif (!empty($batchLimiters['maxId'])) { + $queryBuilder->andWhere( + $queryBuilder->expr()->lte('l.id', $batchLimiters['maxId']) + ); + } elseif (!empty($batchLimiters['minId'])) { + $queryBuilder->andWhere( + $queryBuilder->expr()->gte('l.id', $queryBuilder->expr()->literal((int) $batchLimiters['minId'])) + ); + } + } + + /** + * @param QueryBuilder $queryBuilder + */ + private function excludeVisitors(QueryBuilder $queryBuilder) + { + $queryBuilder->where($queryBuilder->expr()->isNotNull('l.date_identified')); + } } From 9b5daa3ee93c3340a002e4de9d2f18b33109eb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Mon, 21 May 2018 17:35:20 +0200 Subject: [PATCH 401/778] Emails in time company filter --- app/bundles/EmailBundle/Config/config.php | 7 +++- .../EventListener/DashboardSubscriber.php | 3 ++ .../Type/DashboardEmailsInTimeWidgetType.php | 29 ++++++++++++++ app/bundles/EmailBundle/Model/EmailModel.php | 39 +++++++++++++++++-- .../Translations/en_US/messages.ini | 1 + app/bundles/LeadBundle/Config/config.php | 7 ++++ 6 files changed, 80 insertions(+), 6 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 8563a5540bb..986b6234cdd 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -313,8 +313,11 @@ 'alias' => 'monitored_email', ], 'mautic.form.type.email_dashboard_emails_in_time_widget' => [ - 'class' => 'Mautic\EmailBundle\Form\Type\DashboardEmailsInTimeWidgetType', - 'alias' => 'email_dashboard_emails_in_time_widget', + 'class' => 'Mautic\EmailBundle\Form\Type\DashboardEmailsInTimeWidgetType', + 'alias' => 'email_dashboard_emails_in_time_widget', + 'arguments' => [ + 'mautic.lead.repository.company', + ], ], 'mautic.form.type.email_to_user' => [ 'class' => Mautic\EmailBundle\Form\Type\EmailToUserType::class, diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index 57606040c8b..cd29cdfd211 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -86,6 +86,9 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['flag'])) { $params['filter']['flag'] = $params['flag']; } + if (isset($params['companyId'])) { + $params['filter']['companyId'] = $params['companyId']; + } if (!$event->isCached()) { $event->setTemplateData([ diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index f95d9f174d6..6c49a8148a9 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -11,6 +11,7 @@ namespace Mautic\EmailBundle\Form\Type; +use Mautic\LeadBundle\Entity\CompanyRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -19,6 +20,21 @@ */ class DashboardEmailsInTimeWidgetType extends AbstractType { + /** + * @var CompanyRepository + */ + private $companyRepository; + + /** + * DashboardEmailsInTimeWidgetType constructor. + * + * @param CompanyRepository $companyRepository + */ + public function __construct(CompanyRepository $companyRepository) + { + $this->companyRepository = $companyRepository; + } + /** * @param FormBuilderInterface $builder * @param array $options @@ -40,6 +56,19 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ] ); + $companies = $this->companyRepository->getCompanies(); + $companiesChoises = []; + foreach ($companies as $company) { + $companiesChoises[$company['id']] = $company['companyname']; + } + $builder->add('companyId', 'choice', [ + 'label' => 'mautic.email.companyId.filter', + 'choices' => $companiesChoises, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ]); } /** diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index d1c3bf74c1f..4486613e110 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1713,12 +1713,17 @@ public function limitQueryToCreator(QueryBuilder &$q) */ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true) { - $flag = null; + $flag = null; + $companyId = null; if (isset($filter['flag'])) { $flag = $filter['flag']; unset($filter['flag']); } + if (isset($filter['companyId'])) { + $companyId = $filter['companyId']; + unset($filter['companyId']); + } $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); @@ -1728,6 +1733,11 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da if (!$canViewOthers) { $this->limitQueryToCreator($q); } + if ($companyId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.sent.emails'), $data); } @@ -1737,6 +1747,11 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da if (!$canViewOthers) { $this->limitQueryToCreator($q); } + if ($companyId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data); } @@ -1748,6 +1763,11 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da } $q->andWhere($q->expr()->eq('t.is_failed', ':true')) ->setParameter('true', true, 'boolean'); + if ($companyId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.failed.emails'), $data); } @@ -1770,18 +1790,23 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da if (!$canViewOthers) { $this->limitQueryToCreator($q); } + if ($companyId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.clicked'), $data); } if ($flag == 'all' || $flag == 'unsubscribed') { - $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers); + $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers, $companyId); $chart->setDataset($this->translator->trans('mautic.email.unsubscribed'), $data); } if ($flag == 'all' || $flag == 'bounced') { - $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers); + $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers, $companyId); $chart->setDataset($this->translator->trans('mautic.email.bounced'), $data); } @@ -1795,10 +1820,11 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da * @param array $filter * @param $reason * @param $canViewOthers + * @param int|null $companyId * * @return array */ - public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reason, $canViewOthers) + public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reason, $canViewOthers, $companyId = null) { $dncFilter = isset($filter['email_id']) ? ['channel_id' => $filter['email_id']] : []; $q = $query->prepareTimeDataQuery('lead_donotcontact', 'date_added', $dncFilter); @@ -1810,6 +1836,11 @@ public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reaso if (!$canViewOthers) { $this->limitQueryToCreator($q); } + if ($companyId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } return $data = $query->loadAndBuildTimeData($q); } diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index ab93166edd8..d086b2da2c5 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -69,6 +69,7 @@ mautic.email.campaign.event.validate_address="Has valid email address" mautic.email.campaign.event.validate_address_descr="Attempt to validate contact's email address. This may not be 100% accurate." mautic.form.action.send.email.to.owner="Send email to contact's owner" mautic.email.choose.emails_descr="Choose the email to be sent." +mautic.email.companyId.filter="Company filter" mautic.email.config.header.mail="Mail Send Settings" mautic.email.config.header.message="Message Settings" mautic.email.config.header.monitored_email="Monitored Inbox Settings" diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1d092ca201b..df26cd25e2c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -725,6 +725,13 @@ ], ], 'repositories' => [ + 'mautic.lead.repository.company' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\LeadBundle\Entity\Company::class, + ], + ], 'mautic.lead.repository.dnc' => [ 'class' => Doctrine\ORM\EntityRepository::class, 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], From 566e36e30ce2f946506b72d1fa767e29d97e60e5 Mon Sep 17 00:00:00 2001 From: hammad-tfg <20450902+hammad-tfg@users.noreply.github.com> Date: Tue, 22 May 2018 08:26:50 +1000 Subject: [PATCH 402/778] Update MailHelper.php fixed issue # 6093 by using more_entropy parameter to PHP uniqid. Read more here: http://php.net/manual/en/function.uniqid.php Issue link: https://github.com/mautic/mautic/issues/6093 --- app/bundles/EmailBundle/Helper/MailHelper.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index b7ad625f263..5417de173a7 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -1264,13 +1264,7 @@ public function getIdHash() public function setIdHash($idHash = null, $statToBeGenerated = true) { if ($idHash === null) { - /* - 18.05.2018 - hammad-tfg: - fixed issue # 6093 by using more_entropy parameter to PHP uniqid. - Read more here: http://php.net/manual/en/function.uniqid.php - Issue link: https://github.com/mautic/mautic/issues/6093 - */ - $idHash = str_replace('.','',uniqid('',true));//uniqid(); + $idHash = str_replace('.','',uniqid('',true)); } $this->idHash = $idHash; From 39a9e990305c69f535f6e89147eda4fffe0a6da6 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Tue, 22 May 2018 10:05:48 +0200 Subject: [PATCH 403/778] Fix condition for filter visitors from segments --- app/bundles/LeadBundle/Segment/ContactSegmentService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index e394ee6aeea..bd645778501 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -367,6 +367,6 @@ private function addMinMaxLimiters(QueryBuilder $queryBuilder, array $batchLimit */ private function excludeVisitors(QueryBuilder $queryBuilder) { - $queryBuilder->where($queryBuilder->expr()->isNotNull('l.date_identified')); + $queryBuilder->andWhere($queryBuilder->expr()->isNotNull('l.date_identified')); } } From aadfb5d1590bf9205cf397ea18ac9bed016c0094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Tue, 22 May 2018 20:28:34 +0200 Subject: [PATCH 404/778] New widget, missing translations --- .../Event/WidgetDetailEvent.php | 2 +- app/bundles/EmailBundle/Config/config.php | 8 +++ .../EmailBundle/Entity/StatRepository.php | 40 ++++++++++++ .../EventListener/DashboardSubscriber.php | 46 ++++++++++++++ ...DashboardSentEmailToContactsWidgetType.php | 58 ++++++++++++++++++ app/bundles/EmailBundle/Model/EmailModel.php | 61 ++++++++++++++++++- app/bundles/LeadBundle/Config/config.php | 7 +++ 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php diff --git a/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php b/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php index 0c0509ec9f7..7e9c03e860d 100644 --- a/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php +++ b/app/bundles/DashboardBundle/Event/WidgetDetailEvent.php @@ -124,7 +124,7 @@ public function setWidget(Widget $widget) /** * Returns the widget entity. * - * @param Widget $widget + * @return Widget $widget */ public function getWidget() { diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 8563a5540bb..c1b80f27f0c 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -316,6 +316,13 @@ 'class' => 'Mautic\EmailBundle\Form\Type\DashboardEmailsInTimeWidgetType', 'alias' => 'email_dashboard_emails_in_time_widget', ], + 'mautic.form.type.email_dashboard_sent_email_to_contacts_widget' => [ + 'class' => \Mautic\EmailBundle\Form\Type\DashboardSentEmailToContactsWidgetType::class, + 'alias' => 'email_dashboard_sent_email_to_contacts_widget', + 'arguments' => [ + 'mautic.lead.repository.company', + ], + ], 'mautic.form.type.email_to_user' => [ 'class' => Mautic\EmailBundle\Form\Type\EmailToUserType::class, 'alias' => 'email_to_user', @@ -589,6 +596,7 @@ 'mautic.channel.model.queue', 'mautic.email.model.send_email_to_contacts', 'mautic.tracker.device', + 'mautic.lead.repository.company', ], ], 'mautic.email.model.send_email_to_user' => [ diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index 70266f0dd6f..6827245468a 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -46,6 +46,46 @@ public function getEmailStatus($trackingHash) return (!empty($result)) ? $result[0] : null; } + /** + * @param $limit + * @param \DateTime $dateFrom + * @param \DateTime $dateTo + * @param int|null $createdByUserId + * @param int|null $companyId + * + * @return array + */ + public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime $dateTo, $createdByUserId = null, $companyId = null) + { + $q = $this->_em->getConnection()->createQueryBuilder(); + $q->select('s.*') + ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's') + ->innerJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id') + ->addSelect('e.name AS email_name') + ->leftJoin('s', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 's.email_id = ph.email_id AND s.lead_id = ph.lead_id') + ->leftJoin('ph', MAUTIC_TABLE_PREFIX.'page_redirects', 'pr', 'ph.redirect_id = pr.redirect_id') + ->addSelect('pr.url AS link_url') + ->addSelect('pr.hits AS link_hits'); + if ($createdByUserId !== null) { + $q->andWhere('e.created_by = :userId') + ->setParameter('userId', $createdByUserId); + } + $q->andWhere('s.date_sent BETWEEN :dateFrom AND :dateTo') + ->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s')) + ->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s')); + if ($companyId !== null) { + $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 's.lead_id = cl.lead_id') + ->andWhere('cl.company_id = :companyId') + ->setParameter('companyId', $companyId) + ->innerJoin('s', MAUTIC_TABLE_PREFIX.'companies', 'c', 'cl.company_id = c.id') + ->addSelect('c.id AS company_id') + ->addSelect('c.companyname AS company_name'); + } + $q->setMaxResults($limit); + + return $q->execute()->fetchAll(); + } + /** * @param $emailId * @param null $listId diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index 57606040c8b..aef26b897bf 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -36,6 +36,9 @@ class DashboardSubscriber extends MainDashboardSubscriber 'emails.in.time' => [ 'formAlias' => 'email_dashboard_emails_in_time_widget', ], + 'sent.email.to.contacts' => [ + 'formAlias' => 'email_dashboard_sent_email_to_contacts_widget', + ], 'ignored.vs.read.emails' => [], 'upcoming.emails' => [], 'most.sent.emails' => [], @@ -106,6 +109,49 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $event->stopPropagation(); } + if ($event->getType() == 'sent.email.to.contacts') { + $widget = $event->getWidget(); + $params = $widget->getParams(); + + if (!$event->isCached()) { + if (empty($params['limit'])) { + // Count the emails limit from the widget height + $limit = round((($event->getWidget()->getHeight() - 80) / 35) - 1); + } else { + $limit = $params['limit']; + } + $companyId = null; + if (isset($params['companyId'])) { + $companyId = $params['companyId']; + } + $event->setTemplateData([ + 'headItems' => [ + 'mautic.dashboard.label.contact.id', + 'mautic.dashboard.label.contact.email.address', + 'mautic.dashboard.label.contact.open', + 'mautic.dashboard.label.contact.click', + 'mautic.dashboard.label.contact.links.clicked', + 'mautic.dashboard.label.email.id', + 'mautic.dashboard.label.email.name', + 'mautic.dashboard.label.segment.id', + 'mautic.dashboard.label.segment.name', + 'mautic.dashboard.label.contact.company.id', + 'mautic.dashboard.label.contact.company.name', + ], + 'bodyItems' => $this->emailModel->getSentEmailToContactData( + $limit, + $params['dateFrom'], + $params['dateTo'], + ['groupBy' => 'sends', 'canViewOthers' => $canViewOthers], + $companyId + ), + ]); + } + + $event->setTemplate('MauticCoreBundle:Helper:table.html.php'); + $event->stopPropagation(); + } + if ($event->getType() == 'ignored.vs.read.emails') { $widget = $event->getWidget(); $params = $widget->getParams(); diff --git a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php new file mode 100644 index 00000000000..6cc219b89bd --- /dev/null +++ b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php @@ -0,0 +1,58 @@ +companyRepository = $companyRepository; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $companies = $this->companyRepository->getCompanies(); + $companiesChoises = []; + foreach ($companies as $company) { + $companiesChoises[$company['id']] = $company['companyname']; + } + $builder->add('companyId', 'choice', [ + 'label' => 'mautic.email.companyId.filter', + 'choices' => $companiesChoises, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); + } + + /** + * @return string + */ + public function getName() + { + return 'email_dashboard_sent_email_to_contacts_widget'; + } +} diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index d1c3bf74c1f..2f007c0057a 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -37,6 +37,7 @@ use Mautic\EmailBundle\Exception\FailedToSendToContactException; use Mautic\EmailBundle\Helper\MailHelper; use Mautic\EmailBundle\MonitoredEmail\Mailbox; +use Mautic\LeadBundle\Entity\CompanyRepository; use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\CompanyModel; @@ -124,6 +125,11 @@ class EmailModel extends FormModel implements AjaxLookupModelInterface */ private $deviceTracker; + /** + * @var CompanyRepository + */ + private $companyRepository; + /** * EmailModel constructor. * @@ -138,6 +144,7 @@ class EmailModel extends FormModel implements AjaxLookupModelInterface * @param MessageQueueModel $messageQueueModel * @param SendEmailToContact $sendModel * @param DeviceTracker $deviceTracker + * @param CompanyRepository $companyRepository */ public function __construct( IpLookupHelper $ipLookupHelper, @@ -150,7 +157,8 @@ public function __construct( UserModel $userModel, MessageQueueModel $messageQueueModel, SendEmailToContact $sendModel, - DeviceTracker $deviceTracker + DeviceTracker $deviceTracker, + CompanyRepository $companyRepository ) { $this->ipLookupHelper = $ipLookupHelper; $this->themeHelper = $themeHelper; @@ -163,6 +171,7 @@ public function __construct( $this->messageQueueModel = $messageQueueModel; $this->sendModel = $sendModel; $this->deviceTracker = $deviceTracker; + $this->companyRepository = $companyRepository; } /** @@ -533,6 +542,56 @@ public function getBuilderComponents(Email $email = null, $requestedComponents = return $this->getCommonBuilderComponents($requestedComponents, $event); } + /** + * @param $limit + * @param \DateTime $dateFrom + * @param \DateTime $dateTo + * @param array $options + * @param int|null $companyId + * + * @return array + */ + public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null) + { + $createdByUserId = null; + if (!empty($options['canViewOthers'])) { + $createdByUserId = $this->userHelper->getUser()->getId(); + } + $stats = $this->getStatRepository()->getSentEmailToContactData($limit, $dateFrom, $dateTo, $createdByUserId, $companyId); + $data = []; + foreach ($stats as $stat) { + $statId = $stat['id']; + if (!array_key_exists($statId, $data)) { + $item = [ + 'contact_id' => $stat['lead_id'], + 'contact_email' => $stat['email_address'], + 'open' => $stat['is_read'], + 'email_id' => $stat['email_id'], + 'email_name' => $stat['email_name'], + ]; + $item['click'] = ($stat['link_hits'] !== null) ? $stat['link_hits'] : 0; + $item['links_clicked'] = []; + if ($stat['link_url'] !== null) { + $item['links_clicked'][] = $stat['link_url']; + } + if (isset($stat['companyId'])) { + $item['company_id'] = $stat['company_id']; + $item['company_name'] = $stat['company_name']; + } + $data[$statId] = $item; + } else { + if ($stat['link_hits'] !== null) { + $data[$statId]['click'] += $stat['link_hits']; + } + if ($stat['link_url'] !== null && !in_array($stat['link_url'], $data[$statId]['links_clicked'])) { + $data[$statId]['links_clicked'][] = $stat['link_url']; + } + } + } + + return $data; + } + /** * @param $idHash * diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1d092ca201b..df26cd25e2c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -725,6 +725,13 @@ ], ], 'repositories' => [ + 'mautic.lead.repository.company' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\LeadBundle\Entity\Company::class, + ], + ], 'mautic.lead.repository.dnc' => [ 'class' => Doctrine\ORM\EntityRepository::class, 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], From 128dd4819bb0750ccbda8bb8ef661d8d10bb995a Mon Sep 17 00:00:00 2001 From: hammad-tfg <20450902+hammad-tfg@users.noreply.github.com> Date: Wed, 23 May 2018 08:29:05 +1000 Subject: [PATCH 405/778] Update MailHelper.php --- app/bundles/EmailBundle/Helper/MailHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 5417de173a7..5b89257a3f6 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -1264,7 +1264,7 @@ public function getIdHash() public function setIdHash($idHash = null, $statToBeGenerated = true) { if ($idHash === null) { - $idHash = str_replace('.','',uniqid('',true)); + $idHash = str_replace('.', '', uniqid('', true)); } $this->idHash = $idHash; From 8165621bd2b2e5dbd87293a7e671cd3d32a52ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Wed, 23 May 2018 01:13:59 +0200 Subject: [PATCH 406/778] Last commit --- app/bundles/EmailBundle/Config/config.php | 8 +++ .../EmailBundle/Entity/StatRepository.php | 2 +- .../EventListener/DashboardSubscriber.php | 62 ++++++++++++++++--- ...shboardMostHitEmailRedirectsWidgetType.php | 58 +++++++++++++++++ app/bundles/EmailBundle/Model/EmailModel.php | 30 ++++++++- app/bundles/PageBundle/Config/config.php | 9 +++ .../PageBundle/Entity/RedirectRepository.php | 38 ++++++++++++ 7 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index c1b80f27f0c..7b491983a4f 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -323,6 +323,13 @@ 'mautic.lead.repository.company', ], ], + 'mautic.form.type.email_dashboard_most_hit_email_redirects_widget' => [ + 'class' => \Mautic\EmailBundle\Form\Type\DashboardMostHitEmailRedirectsWidgetType::class, + 'alias' => 'email_dashboard_most_hit_email_redirects_widget', + 'arguments' => [ + 'mautic.lead.repository.company', + ], + ], 'mautic.form.type.email_to_user' => [ 'class' => Mautic\EmailBundle\Form\Type\EmailToUserType::class, 'alias' => 'email_to_user', @@ -597,6 +604,7 @@ 'mautic.email.model.send_email_to_contacts', 'mautic.tracker.device', 'mautic.lead.repository.company', + 'mautic.page.repository.redirect', ], ], 'mautic.email.model.send_email_to_user' => [ diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index 6827245468a..08c21ce427e 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -47,7 +47,7 @@ public function getEmailStatus($trackingHash) } /** - * @param $limit + * @param int $limit * @param \DateTime $dateFrom * @param \DateTime $dateTo * @param int|null $createdByUserId diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index aef26b897bf..94c68901533 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -39,6 +39,9 @@ class DashboardSubscriber extends MainDashboardSubscriber 'sent.email.to.contacts' => [ 'formAlias' => 'email_dashboard_sent_email_to_contacts_widget', ], + 'most.hit.email.redirects' => [ + 'formAlias' => 'email_dashboard_most_hit_email_redirects_widget', + ], 'ignored.vs.read.emails' => [], 'upcoming.emails' => [], 'most.sent.emails' => [], @@ -113,6 +116,51 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $widget = $event->getWidget(); $params = $widget->getParams(); + if (!$event->isCached()) { + if (empty($params['limit'])) { + // Count the emails limit from the widget height + $limit = round((($event->getWidget()->getHeight() - 80) / 35) - 1); + } else { + $limit = $params['limit']; + } + $companyId = null; + if (isset($params['companyId'])) { + $companyId = $params['companyId']; + } + $event->setTemplateData( + [ + 'headItems' => [ + 'mautic.dashboard.label.contact.id', + 'mautic.dashboard.label.contact.email.address', + 'mautic.dashboard.label.contact.open', + 'mautic.dashboard.label.contact.click', + 'mautic.dashboard.label.contact.links.clicked', + 'mautic.dashboard.label.email.id', + 'mautic.dashboard.label.email.name', + 'mautic.dashboard.label.segment.id', + 'mautic.dashboard.label.segment.name', + 'mautic.dashboard.label.contact.company.id', + 'mautic.dashboard.label.contact.company.name', + ], + 'bodyItems' => $this->emailModel->getSentEmailToContactData( + $limit, + $params['dateFrom'], + $params['dateTo'], + ['groupBy' => 'sends', 'canViewOthers' => $canViewOthers], + $companyId + ), + ] + ); + } + + $event->setTemplate('MauticCoreBundle:Helper:table.html.php'); + $event->stopPropagation(); + } + + if ($event->getType() == 'most.hit.email.redirects') { + $widget = $event->getWidget(); + $params = $widget->getParams(); + if (!$event->isCached()) { if (empty($params['limit'])) { // Count the emails limit from the widget height @@ -126,19 +174,13 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) } $event->setTemplateData([ 'headItems' => [ - 'mautic.dashboard.label.contact.id', - 'mautic.dashboard.label.contact.email.address', - 'mautic.dashboard.label.contact.open', - 'mautic.dashboard.label.contact.click', - 'mautic.dashboard.label.contact.links.clicked', + 'mautic.dashboard.label.url', + 'mautic.dashboard.label.unique.hit.count', + 'mautic.dashboard.label.total.hit.count', 'mautic.dashboard.label.email.id', 'mautic.dashboard.label.email.name', - 'mautic.dashboard.label.segment.id', - 'mautic.dashboard.label.segment.name', - 'mautic.dashboard.label.contact.company.id', - 'mautic.dashboard.label.contact.company.name', ], - 'bodyItems' => $this->emailModel->getSentEmailToContactData( + 'bodyItems' => $this->emailModel->getMostHitEmailRedirects( $limit, $params['dateFrom'], $params['dateTo'], diff --git a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php new file mode 100644 index 00000000000..d1cafb3d36d --- /dev/null +++ b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php @@ -0,0 +1,58 @@ +companyRepository = $companyRepository; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $companies = $this->companyRepository->getCompanies(); + $companiesChoises = []; + foreach ($companies as $company) { + $companiesChoises[$company['id']] = $company['companyname']; + } + $builder->add('companyId', 'choice', [ + 'label' => 'mautic.email.companyId.filter', + 'choices' => $companiesChoises, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); + } + + /** + * @return string + */ + public function getName() + { + return 'email_dashboard_most_hit_email_redirects_widget'; + } +} diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 2f007c0057a..019733b766d 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -43,6 +43,7 @@ use Mautic\LeadBundle\Model\CompanyModel; use Mautic\LeadBundle\Model\LeadModel; use Mautic\LeadBundle\Tracker\DeviceTracker; +use Mautic\PageBundle\Entity\RedirectRepository; use Mautic\PageBundle\Model\TrackableModel; use Mautic\UserBundle\Model\UserModel; use Symfony\Component\Console\Helper\ProgressBar; @@ -130,6 +131,11 @@ class EmailModel extends FormModel implements AjaxLookupModelInterface */ private $companyRepository; + /** + * @var RedirectRepository + */ + private $redirectRepository; + /** * EmailModel constructor. * @@ -145,6 +151,7 @@ class EmailModel extends FormModel implements AjaxLookupModelInterface * @param SendEmailToContact $sendModel * @param DeviceTracker $deviceTracker * @param CompanyRepository $companyRepository + * @param RedirectRepository $redirectRepository */ public function __construct( IpLookupHelper $ipLookupHelper, @@ -158,7 +165,8 @@ public function __construct( MessageQueueModel $messageQueueModel, SendEmailToContact $sendModel, DeviceTracker $deviceTracker, - CompanyRepository $companyRepository + CompanyRepository $companyRepository, + RedirectRepository $redirectRepository ) { $this->ipLookupHelper = $ipLookupHelper; $this->themeHelper = $themeHelper; @@ -172,6 +180,7 @@ public function __construct( $this->sendModel = $sendModel; $this->deviceTracker = $deviceTracker; $this->companyRepository = $companyRepository; + $this->redirectRepository = $redirectRepository; } /** @@ -592,6 +601,25 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime return $data; } + /** + * @param $limit + * @param \DateTime $dateFrom + * @param \DateTime $dateTo + * @param array $options + * @param int|null $companyId + * + * @return array + */ + public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null) + { + $createdByUserId = null; + if (!empty($options['canViewOthers'])) { + $createdByUserId = $this->userHelper->getUser()->getId(); + } + + return $this->redirectRepository->getMostHitEmailRedirects($limit, $dateFrom, $dateTo, $createdByUserId, $companyId); + } + /** * @param $idHash * diff --git a/app/bundles/PageBundle/Config/config.php b/app/bundles/PageBundle/Config/config.php index 7c76a45ffe4..2ea1a135ee6 100644 --- a/app/bundles/PageBundle/Config/config.php +++ b/app/bundles/PageBundle/Config/config.php @@ -302,6 +302,15 @@ ], ], ], + 'repositories' => [ + 'mautic.page.repository.redirect' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\PageBundle\Entity\Redirect::class, + ], + ], + ], 'other' => [ 'mautic.page.helper.token' => [ 'class' => 'Mautic\PageBundle\Helper\TokenHelper', diff --git a/app/bundles/PageBundle/Entity/RedirectRepository.php b/app/bundles/PageBundle/Entity/RedirectRepository.php index aed8171b5af..ca9182b6812 100644 --- a/app/bundles/PageBundle/Entity/RedirectRepository.php +++ b/app/bundles/PageBundle/Entity/RedirectRepository.php @@ -90,4 +90,42 @@ public function upHitCount($id, $increaseBy = 1, $unique = false) $q->execute(); } + + /** + * @param int $limit + * @param \DateTime $dateFrom + * @param \DateTime $dateTo + * @param int|null $createdByUserId + * @param int|null $companyId + * + * @return array + */ + public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $dateTo, $createdByUserId = null, $companyId = null) + { + $q = $this->_em->getConnection()->createQueryBuilder(); + $q->addSelect('pr.id') + ->addSelect('pr.url') + ->addSelect('pr.hits') + ->addSelect('pr.unique_hits') + ->from(MAUTIC_TABLE_PREFIX.'page_redirects', 'pr') + ->innerJoin('pr', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'pr.redirect_id = ph.redirect_id') + ->innerJoin('ph', MAUTIC_TABLE_PREFIX.'emails', 'e', 'ph.email_id = e.id') + ->addSelect('e.id AS email_id') + ->addSelect('e.name AS email_name'); + if ($createdByUserId !== null) { + $q->andWhere('e.created_by = :userId') + ->setParameter('userId', $createdByUserId); + } + $q->andWhere('pr.date_added BETWEEN :dateFrom AND :dateTo') + ->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s')) + ->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s')); + if ($companyId !== null) { + $q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'ph.lead_id = cl.lead_id') + ->andWhere('cl.company_id = :companyId') + ->setParameter('companyId', $companyId); + } + $q->setMaxResults($limit); + + return $q->execute()->fetchAll(); + } } From c4add66c1c5b21d1360e2fc18390bc97c1f71739 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 23 May 2018 12:18:55 +0200 Subject: [PATCH 407/778] Remove unused code - prepared cache for QueryBuilder --- .../LeadBundle/Segment/ContactSegmentService.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index bd645778501..867d1be33dd 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -33,11 +33,6 @@ class ContactSegmentService */ private $logger; - /** - * @var QueryBuilder - */ - private $preparedQB; - /** * @param ContactSegmentFilterFactory $contactSegmentFilterFactory * @param ContactSegmentQueryBuilder $queryBuilder @@ -64,10 +59,6 @@ public function __construct( */ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) { - if (!is_null($this->preparedQB)) { - return $this->preparedQB; - } - $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); @@ -86,10 +77,6 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) */ private function getTotalSegmentContactsQuery(LeadList $segment) { - if (!is_null($this->preparedQB)) { - return $this->preparedQB; - } - $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); From 8ac1d67ef1d8e4930195c0a4124c29818a756bcf Mon Sep 17 00:00:00 2001 From: John Linhart Date: Wed, 23 May 2018 13:24:12 +0200 Subject: [PATCH 408/778] Add field type url to work with contains, startsWith, endsWith on reports --- app/bundles/ReportBundle/Builder/MauticReportBuilder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/bundles/ReportBundle/Builder/MauticReportBuilder.php b/app/bundles/ReportBundle/Builder/MauticReportBuilder.php index 8a47e907fda..09a20da5870 100644 --- a/app/bundles/ReportBundle/Builder/MauticReportBuilder.php +++ b/app/bundles/ReportBundle/Builder/MauticReportBuilder.php @@ -460,6 +460,7 @@ private function applyFilters(array $filters, QueryBuilder $queryBuilder, array case 'string': case 'email': + case 'url': switch ($exprFunction) { case 'startsWith': $exprFunction = 'like'; From 49abf1a9c52ed9af11fe07ace4135c16bbdb2f0e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 23 May 2018 16:06:45 +0200 Subject: [PATCH 409/778] Remove unnecessary foreach from SegmentQB --- .../SegmentReferenceFilterQueryBuilder.php | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index caa94734a82..5bd0d8d5349 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Segment\Query\Filter; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\ContactSegmentFilterFactory; use Mautic\LeadBundle\Segment\Query\ContactSegmentQueryBuilder; @@ -90,43 +91,43 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil foreach ($segmentIds as $segmentId) { $exclusion = in_array($filter->getOperator(), ['notExists', 'notIn']); - $contactSegments = $this->entityManager->getRepository('MauticLeadBundle:LeadList')->findBy( - ['id' => $segmentId] - ); - - foreach ($contactSegments as $contactSegment) { - $filters = $this->leadSegmentFilterFactory->getSegmentFilters($contactSegment); - - $segmentQueryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($filters); - - // If the segment contains no filters; it means its for manually subscribed only - if (count($filters)) { - $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); - } - - $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); - $segmentQueryBuilder->select('l.id'); - - $parameters = $segmentQueryBuilder->getParameters(); - foreach ($parameters as $key => $value) { - $queryBuilder->setParameter($key, $value); - } - - $segmentAlias = $this->generateRandomParameterName(); - if ($exclusion) { - $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); - $expression = $queryBuilder->expr()->isNull($segmentAlias.'.id'); - } else { - $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); - $expression = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); - } - $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); - - if (!$exclusion && count($segmentIds) > 1) { - $orLogic[] = $expression; - } else { - $queryBuilder->addLogic($expression, $filter->getGlue()); - } + /** @var LeadList $contactSegment */ + $contactSegment = $this->entityManager->getRepository('MauticLeadBundle:LeadList')->find($segmentId); + if (!$contactSegment) { + continue; + } + + $filters = $this->leadSegmentFilterFactory->getSegmentFilters($contactSegment); + + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($filters); + + // If the segment contains no filters; it means its for manually subscribed only + if (count($filters)) { + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallyUnsubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); + } + + $segmentQueryBuilder = $this->leadSegmentQueryBuilder->addManuallySubscribedQuery($segmentQueryBuilder, $contactSegment->getId()); + $segmentQueryBuilder->select('l.id'); + + $parameters = $segmentQueryBuilder->getParameters(); + foreach ($parameters as $key => $value) { + $queryBuilder->setParameter($key, $value); + } + + $segmentAlias = $this->generateRandomParameterName(); + if ($exclusion) { + $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); + $expression = $queryBuilder->expr()->isNull($segmentAlias.'.id'); + } else { + $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); + $expression = $queryBuilder->expr()->isNotNull($segmentAlias.'.id'); + } + $queryBuilder->addSelect($segmentAlias.'.id as '.$segmentAlias.'_id'); + + if (!$exclusion && count($segmentIds) > 1) { + $orLogic[] = $expression; + } else { + $queryBuilder->addLogic($expression, $filter->getGlue()); } } From 146dcdf2124d2b4e9a3b47f8e0bfbd1e81ffbebb Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 23 May 2018 16:56:45 +0200 Subject: [PATCH 410/778] Dispatch event when QueryBuilder for segment is build --- .../LeadListQueryBuilderGeneratedEvent.php | 54 +++++++++++++++++++ app/bundles/LeadBundle/LeadEvents.php | 10 ++++ .../Segment/ContactSegmentService.php | 2 + .../Query/ContactSegmentQueryBuilder.php | 18 ++++++- .../SegmentReferenceFilterQueryBuilder.php | 2 + 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 app/bundles/LeadBundle/Event/LeadListQueryBuilderGeneratedEvent.php diff --git a/app/bundles/LeadBundle/Event/LeadListQueryBuilderGeneratedEvent.php b/app/bundles/LeadBundle/Event/LeadListQueryBuilderGeneratedEvent.php new file mode 100644 index 00000000000..39191567983 --- /dev/null +++ b/app/bundles/LeadBundle/Event/LeadListQueryBuilderGeneratedEvent.php @@ -0,0 +1,54 @@ +segment = $segment; + $this->queryBuilder = $queryBuilder; + } + + /** + * @return LeadList + */ + public function getSegment() + { + return $this->segment; + } + + /** + * @return QueryBuilder + */ + public function getQueryBuilder() + { + return $this->queryBuilder; + } +} diff --git a/app/bundles/LeadBundle/LeadEvents.php b/app/bundles/LeadBundle/LeadEvents.php index c69a0358949..80ab65cd510 100644 --- a/app/bundles/LeadBundle/LeadEvents.php +++ b/app/bundles/LeadBundle/LeadEvents.php @@ -534,6 +534,16 @@ final class LeadEvents */ const LIST_FILTERS_ON_FILTERING = 'mautic.list_filters_on_filtering'; + /** + * The mautic.list_filters_querybuilder_generated event is dispatched when the queryBuilder for segment was generated. + * + * The event listener receives a + * Mautic\LeadBundle\Event\LeadListQueryBuilderGeneratedEvent instance. + * + * @var string + */ + const LIST_FILTERS_QUERYBUILDER_GENERATED = 'mautic.list_filters_querybuilder_generated'; + /** * The mautic.list_filters_on_filtering event is dispatched when the lists are updated. * diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 867d1be33dd..f6f5cc9ef45 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -64,6 +64,8 @@ private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); + $this->contactSegmentQueryBuilder->queryBuilderGenerated($segment, $queryBuilder); + return $queryBuilder; } diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index ca43e066918..d8a322dfc57 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -12,7 +12,9 @@ namespace Mautic\LeadBundle\Segment\Query; use Doctrine\ORM\EntityManager; +use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Event\LeadListFilteringEvent; +use Mautic\LeadBundle\Event\LeadListQueryBuilderGeneratedEvent; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\ContactSegmentFilters; @@ -83,7 +85,7 @@ public function assembleContactsSegmentQueryBuilder(ContactSegmentFilters $conta $queryBuilder = $filter->applyQuery($queryBuilder); - if ($this->dispatcher && $this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { + if ($this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_ON_FILTERING)) { $alias = $this->generateRandomParameterName(); $event = new LeadListFilteringEvent($filterCrate, null, $alias, $filterCrate['operator'], $queryBuilder, $this->entityManager); $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_ON_FILTERING, $event); @@ -238,6 +240,20 @@ public function addManuallyUnsubscribedQuery(QueryBuilder $queryBuilder, $leadLi return $queryBuilder; } + /** + * @param LeadList $segment + * @param QueryBuilder $queryBuilder + */ + public function queryBuilderGenerated(LeadList $segment, QueryBuilder $queryBuilder) + { + if (!$this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_QUERYBUILDER_GENERATED)) { + return; + } + + $event = new LeadListQueryBuilderGeneratedEvent($segment, $queryBuilder); + $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_QUERYBUILDER_GENERATED, $event); + } + /** * Generate a unique parameter name. * diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php index 5bd0d8d5349..667543686d9 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/SegmentReferenceFilterQueryBuilder.php @@ -114,6 +114,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->setParameter($key, $value); } + $this->leadSegmentQueryBuilder->queryBuilderGenerated($contactSegment, $segmentQueryBuilder); + $segmentAlias = $this->generateRandomParameterName(); if ($exclusion) { $queryBuilder->leftJoin('l', '('.$segmentQueryBuilder->getSQL().') ', $segmentAlias, $segmentAlias.'.id = l.id'); From caa2497ac1ee7ee3056ab640d95a30e1c465c52f Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 23 May 2018 17:11:39 +0200 Subject: [PATCH 411/778] Typo - move privete methods below public ones --- .../Segment/ContactSegmentService.php | 188 +++++++++--------- 1 file changed, 95 insertions(+), 93 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index f6f5cc9ef45..f293bf0926a 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -48,46 +48,6 @@ public function __construct( $this->logger = $logger; } - /** - * @param LeadList $segment - * @param $batchLimiters - * - * @return QueryBuilder - * - * @throws Exception\SegmentQueryException - * @throws \Exception - */ - private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) - { - $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); - - $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); - $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); - - $this->contactSegmentQueryBuilder->queryBuilderGenerated($segment, $queryBuilder); - - return $queryBuilder; - } - - /** - * @param LeadList $segment - * - * @return QueryBuilder - * - * @throws Exception\SegmentQueryException - * @throws \Exception - */ - private function getTotalSegmentContactsQuery(LeadList $segment) - { - $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); - - $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); - $queryBuilder = $this->contactSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $segment->getId()); - $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId()); - - return $queryBuilder; - } - /** * @param LeadList $segment * @param array $batchLimiters @@ -198,28 +158,21 @@ public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $li /** * @param LeadList $segment * - * @return QueryBuilder + * @return array * * @throws Exception\SegmentQueryException * @throws \Exception */ - private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) + public function getOrphanedLeadListLeadsCount(LeadList $segment) { - $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); + $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment); + $queryBuilder = $this->contactSegmentQueryBuilder->wrapInCount($queryBuilder); - $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $this->logger->debug('Segment QB: Orphan Leads Count SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); - $qbO = new QueryBuilder($queryBuilder->getConnection()); - $qbO->select('orp.lead_id as id, orp.leadlist_id') - ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp'); - $qbO->leftJoin('orp', '('.$queryBuilder->getSQL().')', 'members', 'members.id=orp.lead_id'); - $qbO->setParameters($queryBuilder->getParameters()); - $qbO->andWhere($qbO->expr()->eq('orp.leadlist_id', ':orpsegid')); - $qbO->andWhere($qbO->expr()->isNull('members.id')); - $qbO->andWhere($qbO->expr()->eq('orp.manually_added', $qbO->expr()->literal(0))); - $qbO->setParameter(':orpsegid', $segment->getId()); + $result = $this->timedFetch($queryBuilder, $segment->getId()); - return $qbO; + return [$segment->getId() => $result]; } /** @@ -230,37 +183,115 @@ private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) * @throws Exception\SegmentQueryException * @throws \Exception */ - public function getOrphanedLeadListLeadsCount(LeadList $segment) + public function getOrphanedLeadListLeads(LeadList $segment) { $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment); - $queryBuilder = $this->contactSegmentQueryBuilder->wrapInCount($queryBuilder); - $this->logger->debug('Segment QB: Orphan Leads Count SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); + $this->logger->debug('Segment QB: Orphan Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); - $result = $this->timedFetch($queryBuilder, $segment->getId()); + $result = $this->timedFetchAll($queryBuilder, $segment->getId()); return [$segment->getId() => $result]; } /** * @param LeadList $segment + * @param $batchLimiters * - * @return array + * @return QueryBuilder * * @throws Exception\SegmentQueryException * @throws \Exception */ - public function getOrphanedLeadListLeads(LeadList $segment) + private function getNewSegmentContactsQuery(LeadList $segment, $batchLimiters) { - $queryBuilder = $this->getOrphanedLeadListLeadsQueryBuilder($segment); + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); - $this->logger->debug('Segment QB: Orphan Leads SQL: '.$queryBuilder->getDebugOutput(), ['segmentId' => $segment->getId()]); + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $queryBuilder = $this->contactSegmentQueryBuilder->addNewContactsRestrictions($queryBuilder, $segment->getId(), $batchLimiters); - $result = $this->timedFetchAll($queryBuilder, $segment->getId()); + $this->contactSegmentQueryBuilder->queryBuilderGenerated($segment, $queryBuilder); - return [$segment->getId() => $result]; + return $queryBuilder; } + /** + * @param LeadList $segment + * + * @return QueryBuilder + * + * @throws Exception\SegmentQueryException + * @throws \Exception + */ + private function getTotalSegmentContactsQuery(LeadList $segment) + { + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); + + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallySubscribedQuery($queryBuilder, $segment->getId()); + $queryBuilder = $this->contactSegmentQueryBuilder->addManuallyUnsubscribedQuery($queryBuilder, $segment->getId()); + + return $queryBuilder; + } + + /** + * @param LeadList $segment + * + * @return QueryBuilder + * + * @throws Exception\SegmentQueryException + * @throws \Exception + */ + private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) + { + $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); + + $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + + $qbO = new QueryBuilder($queryBuilder->getConnection()); + $qbO->select('orp.lead_id as id, orp.leadlist_id') + ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp'); + $qbO->leftJoin('orp', '('.$queryBuilder->getSQL().')', 'members', 'members.id=orp.lead_id'); + $qbO->setParameters($queryBuilder->getParameters()); + $qbO->andWhere($qbO->expr()->eq('orp.leadlist_id', ':orpsegid')); + $qbO->andWhere($qbO->expr()->isNull('members.id')); + $qbO->andWhere($qbO->expr()->eq('orp.manually_added', $qbO->expr()->literal(0))); + $qbO->setParameter(':orpsegid', $segment->getId()); + + return $qbO; + } + + /** + * @param QueryBuilder $queryBuilder + * @param array $batchLimiters + */ + private function addMinMaxLimiters(QueryBuilder $queryBuilder, array $batchLimiters) + { + if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { + $queryBuilder->andWhere( + $queryBuilder->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") + ); + } elseif (!empty($batchLimiters['maxId'])) { + $queryBuilder->andWhere( + $queryBuilder->expr()->lte('l.id', $batchLimiters['maxId']) + ); + } elseif (!empty($batchLimiters['minId'])) { + $queryBuilder->andWhere( + $queryBuilder->expr()->gte('l.id', $queryBuilder->expr()->literal((int) $batchLimiters['minId'])) + ); + } + } + + /** + * @param QueryBuilder $queryBuilder + */ + private function excludeVisitors(QueryBuilder $queryBuilder) + { + $queryBuilder->andWhere($queryBuilder->expr()->isNotNull('l.date_identified')); + } + + /***** DEBUG *****/ + /** * Formatting helper. * @@ -329,33 +360,4 @@ private function timedFetchAll(QueryBuilder $qb, $segmentId) return $result; } - - /** - * @param QueryBuilder $queryBuilder - * @param array $batchLimiters - */ - private function addMinMaxLimiters(QueryBuilder $queryBuilder, array $batchLimiters) - { - if (!empty($batchLimiters['minId']) && !empty($batchLimiters['maxId'])) { - $queryBuilder->andWhere( - $queryBuilder->expr()->comparison('l.id', 'BETWEEN', "{$batchLimiters['minId']} and {$batchLimiters['maxId']}") - ); - } elseif (!empty($batchLimiters['maxId'])) { - $queryBuilder->andWhere( - $queryBuilder->expr()->lte('l.id', $batchLimiters['maxId']) - ); - } elseif (!empty($batchLimiters['minId'])) { - $queryBuilder->andWhere( - $queryBuilder->expr()->gte('l.id', $queryBuilder->expr()->literal((int) $batchLimiters['minId'])) - ); - } - } - - /** - * @param QueryBuilder $queryBuilder - */ - private function excludeVisitors(QueryBuilder $queryBuilder) - { - $queryBuilder->andWhere($queryBuilder->expr()->isNotNull('l.date_identified')); - } } From ddbdbaf4e5cfdedf45bf3bbc5cf2a5e17145e331 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 24 May 2018 11:53:23 +0200 Subject: [PATCH 412/778] Add FormBuilder to CustomFormEvent so plugins could modify Mautic forms --- .../CoreBundle/Event/CustomFormEvent.php | 33 ++++++++++++++----- .../Form/Extension/CustomFormExtension.php | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/bundles/CoreBundle/Event/CustomFormEvent.php b/app/bundles/CoreBundle/Event/CustomFormEvent.php index 978427714b0..bbc0fa9d3fa 100644 --- a/app/bundles/CoreBundle/Event/CustomFormEvent.php +++ b/app/bundles/CoreBundle/Event/CustomFormEvent.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormBuilderInterface; /** * Class CustomFormEvent. @@ -20,12 +21,12 @@ class CustomFormEvent extends Event { /** - * @var + * @var string */ protected $formName; /** - * @var + * @var string */ protected $formType; @@ -39,19 +40,27 @@ class CustomFormEvent extends Event */ protected $subscribers = []; + /** + * @var FormBuilderInterface + */ + private $formBuilder; + /** * CustomFormEvent constructor. * - * @param $formName + * @param string $formName + * @param string $formType + * @param FormBuilderInterface $formBuilder */ - public function __construct($formName, $formType) + public function __construct($formName, $formType, FormBuilderInterface $formBuilder) { - $this->formName = $formName; - $this->formType = $formType; + $this->formName = $formName; + $this->formType = $formType; + $this->formBuilder = $formBuilder; } /** - * @return mixed + * @return string */ public function getFormName() { @@ -59,13 +68,21 @@ public function getFormName() } /** - * @return mixed + * @return string */ public function getFormType() { return $this->formType; } + /** + * @return FormBuilderInterface + */ + public function getFormBuilder() + { + return $this->formBuilder; + } + /** * @return array */ diff --git a/app/bundles/CoreBundle/Form/Extension/CustomFormExtension.php b/app/bundles/CoreBundle/Form/Extension/CustomFormExtension.php index 34a48b6aa4a..01df08d13e7 100644 --- a/app/bundles/CoreBundle/Form/Extension/CustomFormExtension.php +++ b/app/bundles/CoreBundle/Form/Extension/CustomFormExtension.php @@ -45,7 +45,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) if ($this->dispatcher->hasListeners(CoreEvents::ON_FORM_TYPE_BUILD)) { $event = $this->dispatcher->dispatch( CoreEvents::ON_FORM_TYPE_BUILD, - new CustomFormEvent($builder->getName(), $builder->getType()->getName()) + new CustomFormEvent($builder->getName(), $builder->getType()->getName(), $builder) ); if ($listeners = $event->getListeners()) { From 8ab27ca280568b85352076338c9428d1e59adfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 24 May 2018 13:46:56 +0200 Subject: [PATCH 413/778] Add campaign and segment filters --- app/bundles/CampaignBundle/Config/config.php | 9 +++ app/bundles/EmailBundle/Config/config.php | 4 ++ .../EmailBundle/Entity/StatRepository.php | 28 ++++++++- .../EventListener/DashboardSubscriber.php | 28 +++++++-- ...shboardMostHitEmailRedirectsWidgetType.php | 61 +++++++++++++++++-- ...DashboardSentEmailToContactsWidgetType.php | 59 ++++++++++++++++-- app/bundles/EmailBundle/Model/EmailModel.php | 34 +++++++++-- app/bundles/LeadBundle/Config/config.php | 7 +++ .../PageBundle/Entity/RedirectRepository.php | 29 ++++++++- 9 files changed, 237 insertions(+), 22 deletions(-) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 93c44bb6616..ce8a81b7a3f 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -238,6 +238,15 @@ ], ], ], + 'repositories' => [ + 'mautic.campaign.repository.campaign' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\CampaignBundle\Entity\Campaign::class, + ], + ], + ], ], 'parameters' => [ 'campaign_time_wait_on_event_false' => 'PT1H', diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 7b491983a4f..3037938fb3f 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -320,14 +320,18 @@ 'class' => \Mautic\EmailBundle\Form\Type\DashboardSentEmailToContactsWidgetType::class, 'alias' => 'email_dashboard_sent_email_to_contacts_widget', 'arguments' => [ + 'mautic.campaign.repository.campaign', 'mautic.lead.repository.company', + 'mautic.lead.repository.lead_list', ], ], 'mautic.form.type.email_dashboard_most_hit_email_redirects_widget' => [ 'class' => \Mautic\EmailBundle\Form\Type\DashboardMostHitEmailRedirectsWidgetType::class, 'alias' => 'email_dashboard_most_hit_email_redirects_widget', 'arguments' => [ + 'mautic.campaign.repository.campaign', 'mautic.lead.repository.company', + 'mautic.lead.repository.lead_list', ], ], 'mautic.form.type.email_to_user' => [ diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index 08c21ce427e..413f3707c08 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -52,11 +52,20 @@ public function getEmailStatus($trackingHash) * @param \DateTime $dateTo * @param int|null $createdByUserId * @param int|null $companyId + * @param int|null $campaignId + * @param int|null $segmentId * * @return array */ - public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime $dateTo, $createdByUserId = null, $companyId = null) - { + public function getSentEmailToContactData( + $limit, + \DateTime $dateFrom, + \DateTime $dateTo, + $createdByUserId = null, + $companyId = null, + $campaignId = null, + $segmentId = null + ) { $q = $this->_em->getConnection()->createQueryBuilder(); $q->select('s.*') ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's') @@ -81,6 +90,21 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime ->addSelect('c.id AS company_id') ->addSelect('c.companyname AS company_name'); } + if ($campaignId !== null) { + $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source_id = ce.id AND s.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId) + ->addSelect('campaign.id AS campaign_id') + ->addSelect('campaign.name AS campaign_name'); + } + if ($segmentId !== null) { + $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id') + ->andWhere('s.list_id = :segmentId') + ->setParameter('segmentId', $segmentId) + ->addSelect('ll.id AS segment_id') + ->addSelect('ll.name AS segment_name'); + } $q->setMaxResults($limit); return $q->execute()->fetchAll(); diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index 94c68901533..03e88107924 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -127,16 +127,24 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['companyId'])) { $companyId = $params['companyId']; } + $campaignId = null; + if (isset($params['campaignId'])) { + $campaignId = $params['campaignId']; + } + $segmentId = null; + if (isset($params['segmentId'])) { + $segmentId = $params['segmentId']; + } $event->setTemplateData( [ 'headItems' => [ 'mautic.dashboard.label.contact.id', 'mautic.dashboard.label.contact.email.address', 'mautic.dashboard.label.contact.open', - 'mautic.dashboard.label.contact.click', - 'mautic.dashboard.label.contact.links.clicked', 'mautic.dashboard.label.email.id', 'mautic.dashboard.label.email.name', + 'mautic.dashboard.label.contact.click', + 'mautic.dashboard.label.contact.links.clicked', 'mautic.dashboard.label.segment.id', 'mautic.dashboard.label.segment.name', 'mautic.dashboard.label.contact.company.id', @@ -147,7 +155,9 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $params['dateFrom'], $params['dateTo'], ['groupBy' => 'sends', 'canViewOthers' => $canViewOthers], - $companyId + $companyId, + $segmentId, + $campaignId ), ] ); @@ -172,6 +182,14 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['companyId'])) { $companyId = $params['companyId']; } + $campaignId = null; + if (isset($params['campaignId'])) { + $campaignId = $params['campaignId']; + } + $segmentId = null; + if (isset($params['segmentId'])) { + $segmentId = $params['segmentId']; + } $event->setTemplateData([ 'headItems' => [ 'mautic.dashboard.label.url', @@ -185,7 +203,9 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $params['dateFrom'], $params['dateTo'], ['groupBy' => 'sends', 'canViewOthers' => $canViewOthers], - $companyId + $companyId, + $campaignId, + $segmentId ), ]); } diff --git a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php index d1cafb3d36d..a0adee0d943 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php @@ -2,7 +2,11 @@ namespace Mautic\EmailBundle\Form\Type; +use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Entity\CampaignRepository; use Mautic\LeadBundle\Entity\CompanyRepository; +use Mautic\LeadBundle\Entity\LeadList; +use Mautic\LeadBundle\Entity\LeadListRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -11,19 +15,36 @@ */ class DashboardMostHitEmailRedirectsWidgetType extends AbstractType { + /** + * @var CampaignRepository + */ + private $campaignRepository; + /** * @var CompanyRepository */ private $companyRepository; /** - * DashboardSentEmailToContactsWidgetType constructor. + * @var LeadListRepository + */ + private $segmentsRepository; + + /** + * DashboardMostHitEmailRedirectsWidgetType constructor. * - * @param CompanyRepository $companyRepository + * @param CampaignRepository $campaignRepository + * @param CompanyRepository $companyRepository + * @param LeadListRepository $leadListRepository */ - public function __construct(CompanyRepository $companyRepository) - { - $this->companyRepository = $companyRepository; + public function __construct( + CampaignRepository $campaignRepository, + CompanyRepository $companyRepository, + LeadListRepository $leadListRepository + ) { + $this->campaignRepository = $campaignRepository; + $this->companyRepository = $companyRepository; + $this->segmentsRepository = $leadListRepository; } /** @@ -46,6 +67,36 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ] ); + /** @var Campaign[] $campaigns */ + $campaigns = $this->campaignRepository->findAll(); + $campaignsChoices = []; + foreach ($campaigns as $campaign) { + $campaignsChoices[$campaign->getId()] = $campaign->getName(); + } + $builder->add('campaignId', 'choice', [ + 'label' => 'mautic.email.campaignId.filter', + 'choices' => $campaignsChoices, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); + /** @var LeadList[] $segments */ + $segments = $this->segmentsRepository->findAll(); + $segmentsChoices = []; + foreach ($segments as $segment) { + $segmentsChoices[$segment->getId()] = $segment->getName(); + } + $builder->add('segmentId', 'choice', [ + 'label' => 'mautic.email.segmentId.filter', + 'choices' => $segmentsChoices, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); } /** diff --git a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php index 6cc219b89bd..5d73f619bab 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php @@ -2,7 +2,11 @@ namespace Mautic\EmailBundle\Form\Type; +use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Entity\CampaignRepository; use Mautic\LeadBundle\Entity\CompanyRepository; +use Mautic\LeadBundle\Entity\LeadList; +use Mautic\LeadBundle\Entity\LeadListRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -11,19 +15,36 @@ */ class DashboardSentEmailToContactsWidgetType extends AbstractType { + /** + * @var CampaignRepository + */ + private $campaignRepository; + /** * @var CompanyRepository */ private $companyRepository; + /** + * @var LeadListRepository + */ + private $segmentsRepository; + /** * DashboardSentEmailToContactsWidgetType constructor. * - * @param CompanyRepository $companyRepository + * @param CampaignRepository $campaignRepository + * @param CompanyRepository $companyRepository + * @param LeadListRepository $leadListRepository */ - public function __construct(CompanyRepository $companyRepository) - { - $this->companyRepository = $companyRepository; + public function __construct( + CampaignRepository $campaignRepository, + CompanyRepository $companyRepository, + LeadListRepository $leadListRepository + ) { + $this->campaignRepository = $campaignRepository; + $this->companyRepository = $companyRepository; + $this->segmentsRepository = $leadListRepository; } /** @@ -46,6 +67,36 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ] ); + /** @var Campaign[] $campaigns */ + $campaigns = $this->campaignRepository->findAll(); + $campaignsChoices = []; + foreach ($campaigns as $campaign) { + $campaignsChoices[$campaign->getId()] = $campaign->getName(); + } + $builder->add('campaignId', 'choice', [ + 'label' => 'mautic.email.campaignId.filter', + 'choices' => $campaignsChoices, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); + /** @var LeadList[] $segments */ + $segments = $this->segmentsRepository->findAll(); + $segmentsChoices = []; + foreach ($segments as $segment) { + $segmentsChoices[$segment->getId()] = $segment->getName(); + } + $builder->add('segmentId', 'choice', [ + 'label' => 'mautic.email.segmentId.filter', + 'choices' => $segmentsChoices, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); } /** diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 019733b766d..34b343bc326 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -557,16 +557,18 @@ public function getBuilderComponents(Email $email = null, $requestedComponents = * @param \DateTime $dateTo * @param array $options * @param int|null $companyId + * @param int|null $campaignId + * @param int|null $segmentId * * @return array */ - public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null) + public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null, $campaignId = null, $segmentId = null) { $createdByUserId = null; if (!empty($options['canViewOthers'])) { $createdByUserId = $this->userHelper->getUser()->getId(); } - $stats = $this->getStatRepository()->getSentEmailToContactData($limit, $dateFrom, $dateTo, $createdByUserId, $companyId); + $stats = $this->getStatRepository()->getSentEmailToContactData($limit, $dateFrom, $dateTo, $createdByUserId, $companyId, $campaignId, $segmentId); $data = []; foreach ($stats as $stat) { $statId = $stat['id']; @@ -583,7 +585,15 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime if ($stat['link_url'] !== null) { $item['links_clicked'][] = $stat['link_url']; } - if (isset($stat['companyId'])) { + /*if (isset($stat['campaign_id'])) { + $item['campaign_id'] = $stat['campaign_id']; + $item['campaign_name'] = $stat['campaign_name']; + }*/ + if (isset($stat['segment_id'])) { + $item['segment_id'] = $stat['segment_id']; + $item['segment_name'] = $stat['segment_name']; + } + if (isset($stat['company_id'])) { $item['company_id'] = $stat['company_id']; $item['company_name'] = $stat['company_name']; } @@ -607,17 +617,31 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime * @param \DateTime $dateTo * @param array $options * @param int|null $companyId + * @param int|null $campaignId + * @param int|null $segmentId * * @return array */ - public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null) + public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null, $campaignId = null, $segmentId = null) { $createdByUserId = null; if (!empty($options['canViewOthers'])) { $createdByUserId = $this->userHelper->getUser()->getId(); } - return $this->redirectRepository->getMostHitEmailRedirects($limit, $dateFrom, $dateTo, $createdByUserId, $companyId); + $redirects = $this->redirectRepository->getMostHitEmailRedirects($limit, $dateFrom, $dateTo, $createdByUserId, $companyId, $campaignId, $segmentId); + $data = []; + foreach ($redirects as $redirect) { + $data[] = [ + 'url' => $redirect['url'], + 'unique_hits' => $redirect['unique_hits'], + 'hits' => $redirect['hits'], + 'email_id' => $redirect['email_id'], + 'email_name' => $redirect['email_name'], + ]; + } + + return $data; } /** diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index df26cd25e2c..d221f383d15 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -746,6 +746,13 @@ \Mautic\LeadBundle\Entity\Lead::class, ], ], + 'mautic.lead.repository.lead_list' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\LeadBundle\Entity\LeadList::class, + ], + ], 'mautic.lead.repository.lead_event_log' => [ 'class' => Doctrine\ORM\EntityRepository::class, 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], diff --git a/app/bundles/PageBundle/Entity/RedirectRepository.php b/app/bundles/PageBundle/Entity/RedirectRepository.php index ca9182b6812..3cd9eae52df 100644 --- a/app/bundles/PageBundle/Entity/RedirectRepository.php +++ b/app/bundles/PageBundle/Entity/RedirectRepository.php @@ -97,11 +97,20 @@ public function upHitCount($id, $increaseBy = 1, $unique = false) * @param \DateTime $dateTo * @param int|null $createdByUserId * @param int|null $companyId + * @param int|null $campaignId + * @param int|null $segmentId * * @return array */ - public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $dateTo, $createdByUserId = null, $companyId = null) - { + public function getMostHitEmailRedirects( + $limit, + \DateTime $dateFrom, + \DateTime $dateTo, + $createdByUserId = null, + $companyId = null, + $campaignId = null, + $segmentId = null + ) { $q = $this->_em->getConnection()->createQueryBuilder(); $q->addSelect('pr.id') ->addSelect('pr.url') @@ -124,6 +133,22 @@ public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime ->andWhere('cl.company_id = :companyId') ->setParameter('companyId', $companyId); } + + if ($campaignId !== null) { + $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source_id = ce.id AND s.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId) + ->addSelect('campaign.id AS campaign_id') + ->addSelect('campaign.name AS campaign_name'); + } + if ($segmentId !== null) { + $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id') + ->andWhere('s.list_id = :segmentId') + ->setParameter('segmentId', $segmentId) + ->addSelect('ll.id AS segment_id') + ->addSelect('ll.name AS segment_name'); + } $q->setMaxResults($limit); return $q->execute()->fetchAll(); From 6ecd878deff7e5a037653523bb12dcc27a8cd369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 24 May 2018 13:53:04 +0200 Subject: [PATCH 414/778] Fix most hit emails redirects query --- app/bundles/PageBundle/Entity/RedirectRepository.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/bundles/PageBundle/Entity/RedirectRepository.php b/app/bundles/PageBundle/Entity/RedirectRepository.php index 3cd9eae52df..3eaa581a43f 100644 --- a/app/bundles/PageBundle/Entity/RedirectRepository.php +++ b/app/bundles/PageBundle/Entity/RedirectRepository.php @@ -135,7 +135,7 @@ public function getMostHitEmailRedirects( } if ($campaignId !== null) { - $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source_id = ce.id AND s.source = "campaign.event"') + $q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 'ph.source_id = ce.id AND ph.source = "campaign.event"') ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') ->andWhere('ce.campaign_id = :campaignId') ->setParameter('campaignId', $campaignId) @@ -143,8 +143,9 @@ public function getMostHitEmailRedirects( ->addSelect('campaign.name AS campaign_name'); } if ($segmentId !== null) { - $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id') - ->andWhere('s.list_id = :segmentId') + $q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll', 'ph.lead_id = lll.lead_id') + ->innerJoin('lll', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'lll.leadlist_id = ll.id') + ->andWhere('lll.leadlist_id = :segmentId') ->setParameter('segmentId', $segmentId) ->addSelect('ll.id AS segment_id') ->addSelect('ll.name AS segment_name'); From 213ad0f9139c9587718d1b2c7c5eb1b27c0dd4d6 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 24 May 2018 14:27:57 +0200 Subject: [PATCH 415/778] Dispatch event for Qb generate for orphaned leads too --- app/bundles/LeadBundle/Segment/ContactSegmentService.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index f293bf0926a..dc7c68c25ee 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -248,6 +248,8 @@ private function getOrphanedLeadListLeadsQueryBuilder(LeadList $segment) $queryBuilder = $this->contactSegmentQueryBuilder->assembleContactsSegmentQueryBuilder($segmentFilters); + $this->contactSegmentQueryBuilder->queryBuilderGenerated($segment, $queryBuilder); + $qbO = new QueryBuilder($queryBuilder->getConnection()); $qbO->select('orp.lead_id as id, orp.leadlist_id') ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'orp'); From 354d67136ba6dcfd6f59ee024a1ac169aa9dfc05 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 24 May 2018 14:29:45 +0200 Subject: [PATCH 416/778] Fix for logger in command - correct service name --- app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php index 39d5497b5ac..da7e08d6ae5 100644 --- a/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php +++ b/app/bundles/LeadBundle/Command/UpdateLeadListsCommand.php @@ -61,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output) try { $processed = $listModel->rebuildListLeads($list, $batch, $max, $output); } catch (QueryException $e) { - $this->getContainer()->get('mautic.logger')->error('Query Builder Exception: '.$e->getMessage()); + $this->getContainer()->get('monolog.logger.mautic')->error('Query Builder Exception: '.$e->getMessage()); } $output->writeln( From 548223e5a79001669759a4f56250c449f61be85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 24 May 2018 15:35:26 +0200 Subject: [PATCH 417/778] Condition widgets headers --- .../EventListener/DashboardSubscriber.php | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index 03e88107924..0dbc2844d71 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -135,21 +135,26 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['segmentId'])) { $segmentId = $params['segmentId']; } + $headItems = [ + 'mautic.dashboard.label.contact.id', + 'mautic.dashboard.label.contact.email.address', + 'mautic.dashboard.label.contact.open', + 'mautic.dashboard.label.email.id', + 'mautic.dashboard.label.email.name', + 'mautic.dashboard.label.contact.click', + 'mautic.dashboard.label.contact.links.clicked', + ]; + if ($segmentId !== null) { + $headItems[] = 'mautic.dashboard.label.segment.id'; + $headItems[] = 'mautic.dashboard.label.segment.name'; + } + if ($companyId !== null) { + $headItems[] = 'mautic.dashboard.label.company.id'; + $headItems[] = 'mautic.dashboard.label.company.name'; + } $event->setTemplateData( [ - 'headItems' => [ - 'mautic.dashboard.label.contact.id', - 'mautic.dashboard.label.contact.email.address', - 'mautic.dashboard.label.contact.open', - 'mautic.dashboard.label.email.id', - 'mautic.dashboard.label.email.name', - 'mautic.dashboard.label.contact.click', - 'mautic.dashboard.label.contact.links.clicked', - 'mautic.dashboard.label.segment.id', - 'mautic.dashboard.label.segment.name', - 'mautic.dashboard.label.contact.company.id', - 'mautic.dashboard.label.contact.company.name', - ], + 'headItems' => $headItems, 'bodyItems' => $this->emailModel->getSentEmailToContactData( $limit, $params['dateFrom'], From 29bd05b1ad497f5d95b20fcc302ce924bc1d43fa Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Fri, 25 May 2018 08:55:45 +0200 Subject: [PATCH 418/778] Add close to form results --- .../FormBundle/Views/Result/index.html.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/bundles/FormBundle/Views/Result/index.html.php b/app/bundles/FormBundle/Views/Result/index.html.php index e356d7d3cf8..4bf9b4854d0 100644 --- a/app/bundles/FormBundle/Views/Result/index.html.php +++ b/app/bundles/FormBundle/Views/Result/index.html.php @@ -54,6 +54,29 @@ ]; } +$buttons[] = [ + 'attr' => [ + 'data-toggle' => 'index', + 'data-toggle' => '', + 'class' => 'btn btn-default btn-nospin', + 'href' => $view['router']->path('mautic_form_export', ['objectId' => $form->getId(), 'format' => 'xlsx']), + ], + 'btnText' => $view['translator']->trans('mautic.form.result.export.xlsx'), + 'iconClass' => 'fa fa-file-excel-o', + 'primary' => true, +]; + +$buttons[] = + [ + 'attr' => [ + 'class' => 'btn btn-default', + 'href' => $view['router']->path('mautic_form_action', ['objectAction' => 'view', 'objectId'=> $form->getId()]), + 'data-toggle' => 'ajax', + ], + 'iconClass' => 'fa fa-remove', + 'btnText' => $view['translator']->trans('mautic.core.form.close'), + ]; + $view['slots']->set('actions', $view->render('MauticCoreBundle:Helper:page_actions.html.php', ['customButtons' => $buttons])); ?> From 8f8685a06a171f7b672a9107d2ac8c50bed76834 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Fri, 25 May 2018 08:58:18 +0200 Subject: [PATCH 419/778] Minor --- app/bundles/FormBundle/Views/Result/index.html.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/bundles/FormBundle/Views/Result/index.html.php b/app/bundles/FormBundle/Views/Result/index.html.php index 4bf9b4854d0..7157ff573fd 100644 --- a/app/bundles/FormBundle/Views/Result/index.html.php +++ b/app/bundles/FormBundle/Views/Result/index.html.php @@ -54,18 +54,6 @@ ]; } -$buttons[] = [ - 'attr' => [ - 'data-toggle' => 'index', - 'data-toggle' => '', - 'class' => 'btn btn-default btn-nospin', - 'href' => $view['router']->path('mautic_form_export', ['objectId' => $form->getId(), 'format' => 'xlsx']), - ], - 'btnText' => $view['translator']->trans('mautic.form.result.export.xlsx'), - 'iconClass' => 'fa fa-file-excel-o', - 'primary' => true, -]; - $buttons[] = [ 'attr' => [ From 67f561431db5f12c0088dd1869aeb3883723c1a1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 23 May 2018 14:49:35 -0500 Subject: [PATCH 420/778] Prevent points from getting reset after primary company is set --- .../InstallFixtures/ORM/LeadFieldData.php | 4 ++ .../LeadBundle/Entity/LeadRepository.php | 16 +++++- app/bundles/LeadBundle/Model/FieldModel.php | 1 + .../Tests/Model/LeadModelFunctionalTest.php | 54 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/app/bundles/InstallBundle/InstallFixtures/ORM/LeadFieldData.php b/app/bundles/InstallBundle/InstallFixtures/ORM/LeadFieldData.php index e2801e5645c..1de96b15623 100644 --- a/app/bundles/InstallBundle/InstallFixtures/ORM/LeadFieldData.php +++ b/app/bundles/InstallBundle/InstallFixtures/ORM/LeadFieldData.php @@ -77,6 +77,10 @@ public function load(ObjectManager $manager) $entity->setIsListable(!empty($field['listable'])); $entity->setIsShortVisible(!empty($field['short'])); + if (isset($field['default'])) { + $entity->setDefaultValue($field['default']); + } + $manager->persist($entity); $manager->flush(); diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index b69f0e26334..f1336552256 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -27,7 +27,10 @@ */ class LeadRepository extends CommonRepository implements CustomFieldRepositoryInterface { - use CustomFieldRepositoryTrait; + use CustomFieldRepositoryTrait { + prepareDbalFieldsForSave as defaultPrepareDbalFieldsForSave; + } + use ExpressionHelperTrait; use OperatorListTrait; @@ -1169,4 +1172,15 @@ protected function postSaveEntity($entity) } } } + + /** + * @param $fields + */ + protected function prepareDbalFieldsForSave(&$fields) + { + // Do not save points as they are handled by postSaveEntity + unset($fields['points']); + + $this->defaultPrepareDbalFieldsForSave($fields); + } } diff --git a/app/bundles/LeadBundle/Model/FieldModel.php b/app/bundles/LeadBundle/Model/FieldModel.php index 6e6ab0182fa..ab57b460486 100644 --- a/app/bundles/LeadBundle/Model/FieldModel.php +++ b/app/bundles/LeadBundle/Model/FieldModel.php @@ -86,6 +86,7 @@ class FieldModel extends FormModel 'fixed' => true, 'listable' => true, 'object' => 'lead', + 'default' => 0, ], 'fax' => [ 'type' => 'tel', diff --git a/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php index bcd4771bea5..518d09310dd 100644 --- a/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php @@ -14,9 +14,15 @@ use Doctrine\ORM\EntityManager; use Mautic\CoreBundle\Test\MauticWebTestCase; use Mautic\LeadBundle\Entity\Lead; +use Mautic\LeadBundle\Event\LeadEvent; +use Mautic\LeadBundle\LeadEvents; +use Mautic\LeadBundle\Model\LeadModel; +use Symfony\Component\EventDispatcher\EventDispatcher; class LeadModelFunctionalTest extends MauticWebTestCase { + private $pointsAdded = false; + public function testMergedContactFound() { $model = $this->container->get('mautic.lead.model.lead'); @@ -124,4 +130,52 @@ public function testMergedContactsPointsAreAccurate() $this->assertEquals(56, $jane->getPoints()); } + + public function testSavingPrimaryCompanyAfterPointsAreSetByListenerAreNotResetToDefaultOf0BecauseOfPointsFieldDefaultIs0() + { + /** @var EventDispatcher $eventDispatcher */ + $eventDispatcher = $this->container->get('event_dispatcher'); + $eventDispatcher->addListener(LeadEvents::LEAD_POST_SAVE, [$this, 'addPointsListener']); + + /** @var LeadModel $model */ + $model = $this->container->get('mautic.lead.model.lead'); + /** @var EntityManager $em */ + $em = $this->container->get('doctrine.orm.entity_manager'); + + // Set company to trigger setPrimaryCompany() + $lead = new Lead(); + $data = ['email' => 'pointtest@test.com', 'company' => 'PointTest']; + $model->setFieldValues($lead, $data, false, true, true); + + // Save to trigger points listener and setting primary company + $model->saveEntity($lead); + + // Clear from doctrine memory so we get a fresh entity to ensure the points are definitely saved + $em->clear(Lead::class); + $lead = $model->getEntity($lead->getId()); + + $this->assertEquals(10, $lead->getPoints()); + } + + /** + * Simulate a PointModel::triggerAction. + * + * @param LeadEvent $event + */ + public function addPointsListener(LeadEvent $event) + { + // Prevent a loop + if ($this->pointsAdded) { + return; + } + + $this->pointsAdded = true; + + $lead = $event->getLead(); + $lead->adjustPoints(10); + + /** @var LeadModel $model */ + $model = $this->container->get('mautic.lead.model.lead'); + $model->saveEntity($lead); + } } From f46ac252640aa5de2dff425feaad31abf977b115 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 23 May 2018 17:21:07 -0500 Subject: [PATCH 421/778] Prevent SQL error if $fields are removed --- app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php b/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php index 0ad069a029e..933c498811e 100644 --- a/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php +++ b/app/bundles/LeadBundle/Entity/CustomFieldRepositoryTrait.php @@ -263,8 +263,9 @@ public function saveEntity($entity, $flush = true) $fields = array_diff_key($fields, $changes); } + $this->prepareDbalFieldsForSave($fields); + if (!empty($fields)) { - $this->prepareDbalFieldsForSave($fields); $this->getEntityManager()->getConnection()->update($table, $fields, ['id' => $entity->getId()]); } From 1de3626fcac9ec19203100975f69db2fdc1e9145 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 23 May 2018 18:56:37 -0500 Subject: [PATCH 422/778] Prevent setter values from getting overwritten by defaults --- .../LeadBundle/Entity/CustomFieldEntityTrait.php | 4 ++++ app/bundles/LeadBundle/Model/DefaultValueTrait.php | 12 +++++++----- .../Tests/Model/LeadModelFunctionalTest.php | 7 +++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php b/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php index 2b4c0a133ef..c2dc3978b21 100644 --- a/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php +++ b/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php @@ -187,6 +187,10 @@ public function getUpdatedFields() */ public function getFieldValue($field, $group = null) { + if (property_exists($this, $field)) { + return $this->{'get'.ucfirst($field)}(); + } + if (array_key_exists($field, $this->updatedFields)) { return $this->updatedFields[$field]; } diff --git a/app/bundles/LeadBundle/Model/DefaultValueTrait.php b/app/bundles/LeadBundle/Model/DefaultValueTrait.php index ae21d797449..fab728c9246 100644 --- a/app/bundles/LeadBundle/Model/DefaultValueTrait.php +++ b/app/bundles/LeadBundle/Model/DefaultValueTrait.php @@ -11,22 +11,24 @@ namespace Mautic\LeadBundle\Model; +use Mautic\LeadBundle\Entity\CustomFieldEntityInterface; + trait DefaultValueTrait { /** * @param $entity * @param string $object */ - protected function setEntityDefaultValues($entity, $object = 'lead') + protected function setEntityDefaultValues(CustomFieldEntityInterface $entity, $object = 'lead') { if (!$entity->getId()) { - // New contact so default values if not already set - $updatedFields = $entity->getUpdatedFields(); - /** @var FieldModel $fieldModel */ $fields = $this->leadFieldModel->getFieldListWithProperties($object); foreach ($fields as $alias => $field) { - if (!isset($updatedFields[$alias]) && '' !== $field['defaultValue'] && null !== $field['defaultValue']) { + // Prevent defaults from overwriting values already set + $value = $entity->getFieldValue($alias); + + if ((null === $value || '' === $value) && '' !== $field['defaultValue'] && null !== $field['defaultValue']) { $entity->addUpdatedField($alias, $field['defaultValue']); } } diff --git a/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php b/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php index 518d09310dd..8baed527737 100644 --- a/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php +++ b/app/bundles/LeadBundle/Tests/Model/LeadModelFunctionalTest.php @@ -12,14 +12,14 @@ namespace Mautic\LeadBundle\Tests\Model; use Doctrine\ORM\EntityManager; -use Mautic\CoreBundle\Test\MauticWebTestCase; +use Mautic\CoreBundle\Test\MauticMysqlTestCase; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Event\LeadEvent; use Mautic\LeadBundle\LeadEvents; use Mautic\LeadBundle\Model\LeadModel; use Symfony\Component\EventDispatcher\EventDispatcher; -class LeadModelFunctionalTest extends MauticWebTestCase +class LeadModelFunctionalTest extends MauticMysqlTestCase { private $pointsAdded = false; @@ -75,6 +75,7 @@ public function testMergedContactFound() public function testMergedContactsPointsAreAccurate() { + /** @var LeadModel $model */ $model = $this->container->get('mautic.lead.model.lead'); /** @var EntityManager $em */ $em = $this->container->get('doctrine.orm.entity_manager'); @@ -85,7 +86,9 @@ public function testMergedContactsPointsAreAccurate() ->setLastname('Smith') ->setEmail('jane.smith@test.com') ->setPoints(50); + $model->saveEntity($jane); + $em->clear(Lead::class); $jane = $model->getEntity($jane->getId()); $this->assertEquals(50, $jane->getPoints()); From fcee477059b7c2d0721b94235502a5b43559f6c1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 23 May 2018 19:13:37 -0500 Subject: [PATCH 423/778] Prevent some tests from failing by only returning values from getter if it's not null --- app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php b/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php index c2dc3978b21..aa8b938f595 100644 --- a/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php +++ b/app/bundles/LeadBundle/Entity/CustomFieldEntityTrait.php @@ -188,7 +188,11 @@ public function getUpdatedFields() public function getFieldValue($field, $group = null) { if (property_exists($this, $field)) { - return $this->{'get'.ucfirst($field)}(); + $value = $this->{'get'.ucfirst($field)}(); + + if (null !== $value) { + return $value; + } } if (array_key_exists($field, $this->updatedFields)) { From 05e627755227521b807a7189b275da112a151403 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Fri, 25 May 2018 15:35:09 +0200 Subject: [PATCH 424/778] Add option to add lables to segment list table --- app/bundles/LeadBundle/Views/List/list.html.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/LeadBundle/Views/List/list.html.php b/app/bundles/LeadBundle/Views/List/list.html.php index 2aa96e4bb61..3a139421d56 100644 --- a/app/bundles/LeadBundle/Views/List/list.html.php +++ b/app/bundles/LeadBundle/Views/List/list.html.php @@ -67,6 +67,7 @@ + isGlobal()): ?> + getCustomContent('segment.name', $mauticTemplateVars); ?>
getDescription()): ?>
From dcca7837ed3f65d99f535f9ed3af75dfe35e055a Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 25 May 2018 17:38:30 -0500 Subject: [PATCH 425/778] Prevent device from tracking against a Mautic user --- app/bundles/LeadBundle/Config/config.php | 1 + .../DeviceTrackingServiceTest.php | 25 ++++++++++++++++++- .../DeviceTrackingService.php | 16 +++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1fe269edca3..e8f17428393 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -916,6 +916,7 @@ 'mautic.lead.repository.lead_device', 'mautic.helper.random', 'request_stack', + 'mautic.security', ], ], 'mautic.tracker.contact' => [ diff --git a/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php b/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php index 78ba135fd66..8fbd7dbb161 100644 --- a/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php +++ b/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManagerInterface; use Mautic\CoreBundle\Helper\CookieHelper; use Mautic\CoreBundle\Helper\RandomHelper\RandomHelperInterface; +use Mautic\CoreBundle\Security\Permissions\CorePermissions; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadDevice; use Mautic\LeadBundle\Entity\LeadDeviceRepository; @@ -51,6 +52,11 @@ final class DeviceTrackingServiceTest extends \PHPUnit_Framework_TestCase */ private $requestStackMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $security; + protected function setUp() { $this->cookieHelperMock = $this->createMock(CookieHelper::class); @@ -58,6 +64,7 @@ protected function setUp() $this->randomHelperMock = $this->createMock(RandomHelperInterface::class); $this->leadDeviceRepositoryMock = $this->createMock(LeadDeviceRepository::class); $this->requestStackMock = $this->createMock(RequestStack::class); + $this->security = $this->createMock(CorePermissions::class); } public function testIsTrackedTrue() @@ -362,6 +369,21 @@ public function testTrackCurrentDeviceNotTrackedYet() $this->assertInstanceOf(LeadDevice::class, $returnedLeadDevice); } + /** + * Test that a user is not tracked. + */ + public function testUserIsNotTracked() + { + $this->leadDeviceRepositoryMock->expects($this->never()) + ->method('getByTrackingId'); + + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(false); + + $this->getDeviceTrackingService()->getTrackedDevice(); + } + /** * @return DeviceTrackingService */ @@ -372,7 +394,8 @@ private function getDeviceTrackingService() $this->entityManagerMock, $this->leadDeviceRepositoryMock, $this->randomHelperMock, - $this->requestStackMock + $this->requestStackMock, + $this->security ); } } diff --git a/app/bundles/LeadBundle/Tracker/Service/DeviceTrackingService/DeviceTrackingService.php b/app/bundles/LeadBundle/Tracker/Service/DeviceTrackingService/DeviceTrackingService.php index e05e8b44278..bef12456c97 100644 --- a/app/bundles/LeadBundle/Tracker/Service/DeviceTrackingService/DeviceTrackingService.php +++ b/app/bundles/LeadBundle/Tracker/Service/DeviceTrackingService/DeviceTrackingService.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManagerInterface; use Mautic\CoreBundle\Helper\CookieHelper; use Mautic\CoreBundle\Helper\RandomHelper\RandomHelperInterface; +use Mautic\CoreBundle\Security\Permissions\CorePermissions; use Mautic\LeadBundle\Entity\LeadDevice; use Mautic\LeadBundle\Entity\LeadDeviceRepository; use Symfony\Component\BrowserKit\Request; @@ -54,6 +55,11 @@ final class DeviceTrackingService implements DeviceTrackingServiceInterface */ private $trackedDevice; + /** + * @var CorePermissions + */ + private $security; + /** * DeviceTrackingService constructor. * @@ -62,19 +68,22 @@ final class DeviceTrackingService implements DeviceTrackingServiceInterface * @param LeadDeviceRepository $leadDeviceRepository * @param RandomHelperInterface $randomHelper * @param RequestStack $requestStack + * @param CorePermissions $security */ public function __construct( CookieHelper $cookieHelper, EntityManagerInterface $entityManager, LeadDeviceRepository $leadDeviceRepository, RandomHelperInterface $randomHelper, - RequestStack $requestStack + RequestStack $requestStack, + CorePermissions $security ) { $this->cookieHelper = $cookieHelper; $this->entityManager = $entityManager; $this->randomHelper = $randomHelper; $this->leadDeviceRepository = $leadDeviceRepository; $this->request = $requestStack->getCurrentRequest(); + $this->security = $security; } /** @@ -90,6 +99,11 @@ public function isTracked() */ public function getTrackedDevice() { + if (!$this->security->isAnonymous()) { + // Do not track Mautic users + return; + } + if ($this->trackedDevice) { return $this->trackedDevice; } From 9e1c819a52be94aeb4d5e5dae0d01e62927d2835 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 29 Jan 2018 01:00:12 -0600 Subject: [PATCH 426/778] Refactor campaign kickoff --- app/bundles/CampaignBundle/CampaignEvents.php | 54 ++- .../Command/TriggerCampaignCommand.php | 13 +- app/bundles/CampaignBundle/Config/config.php | 206 ++++++++--- .../CampaignBundle/Entity/Campaign.php | 23 +- .../Entity/ChannelInterface.php | 39 +++ app/bundles/CampaignBundle/Entity/Event.php | 9 +- .../Entity/FailedLeadEventLog.php | 4 + .../CampaignBundle/Entity/LeadEventLog.php | 2 +- .../Event/CampaignBuilderEvent.php | 9 +- .../Event/CampaignDecisionEvent.php | 2 + .../Event/CampaignExecutionEvent.php | 2 + .../Event/CampaignScheduledEvent.php | 2 + .../CampaignBundle/Event/DecisionEvent.php | 16 + .../CampaignBundle/Event/EventArrayTrait.php | 41 +++ .../CampaignBundle/Event/ExecutedEvent.php | 57 ++++ .../CampaignBundle/Event/FailedEvent.php | 57 ++++ .../CampaignBundle/Event/PendingEvent.php | 202 +++++++++++ .../CampaignBundle/Event/ScheduledEvent.php | 77 +++++ .../Accessor/Event/AbstractEventAccessor.php | 165 +++++++++ .../Accessor/Event/ActionAccessor.php | 16 + .../Accessor/Event/ConditionAccessor.php | 16 + .../Accessor/Event/DecisionAccessor.php | 16 + .../EventCollector/Accessor/EventAccessor.php | 156 +++++++++ .../Exception/EventNotFoundException.php | 16 + .../Exception/TypeNotFoundException.php | 16 + .../Builder/ConnectionBuilder.php | 127 +++++++ .../EventCollector/Builder/EventBuilder.php | 64 ++++ .../EventCollector/EventCollector.php | 122 +++++++ .../ContactFinder/InactiveContacts.php | 16 + .../ContactFinder/KickoffContacts.php | 106 ++++++ .../Executioner/DecisionExecutioner.php | 17 + .../Dispatcher/EventDispatcher.php | 156 +++++++++ .../Exception/LogNotProcessedException.php | 27 ++ .../Exception/LogPassedAndFailedException.php | 27 ++ .../Dispatcher/LegacyEventDispatcher.php | 322 ++++++++++++++++++ .../Executioner/Event/Action.php | 93 +++++ .../Executioner/Event/Condition.php | 55 +++ .../Executioner/Event/Decision.php | 55 +++ .../Executioner/Event/EventInterface.php | 28 ++ .../Executioner/EventExecutioner.php | 100 ++++++ .../Executioner/Exception/NoContactsFound.php | 16 + .../Executioner/Exception/NoEventsFound.php | 16 + .../Executioner/InactiveExecutioner.php | 16 + .../Executioner/KickoffExecutioner.php | 236 +++++++++++++ .../Executioner/Logger/EventLogger.php | 155 +++++++++ .../Executioner/ScheduledExecutioner.php | 16 + .../Executioner/Scheduler/EventScheduler.php | 193 +++++++++++ .../ExecutionProhibitedException.php | 16 + .../Exception/NotSchedulableException.php | 16 + .../Exception/NotTimeYetException.php | 16 + .../Executioner/Scheduler/Mode/DateTime.php | 89 +++++ .../Executioner/Scheduler/Mode/Interval.php | 71 ++++ .../Scheduler/Mode/ScheduleModeInterface.php | 26 ++ .../Helper/ChannelExtractor.php | 55 +++ .../CampaignBundle/Model/CampaignModel.php | 260 ++++++-------- .../CoreBundle/Helper/DateTimeHelper.php | 8 +- 56 files changed, 3509 insertions(+), 227 deletions(-) create mode 100644 app/bundles/CampaignBundle/Entity/ChannelInterface.php create mode 100644 app/bundles/CampaignBundle/Event/DecisionEvent.php create mode 100644 app/bundles/CampaignBundle/Event/EventArrayTrait.php create mode 100644 app/bundles/CampaignBundle/Event/ExecutedEvent.php create mode 100644 app/bundles/CampaignBundle/Event/FailedEvent.php create mode 100644 app/bundles/CampaignBundle/Event/PendingEvent.php create mode 100644 app/bundles/CampaignBundle/Event/ScheduledEvent.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/Exception/EventNotFoundException.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Accessor/Exception/TypeNotFoundException.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php create mode 100644 app/bundles/CampaignBundle/EventCollector/Builder/EventBuilder.php create mode 100644 app/bundles/CampaignBundle/EventCollector/EventCollector.php create mode 100644 app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php create mode 100644 app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php create mode 100644 app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php create mode 100644 app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php create mode 100644 app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogNotProcessedException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php create mode 100644 app/bundles/CampaignBundle/Executioner/Event/Action.php create mode 100644 app/bundles/CampaignBundle/Executioner/Event/Condition.php create mode 100644 app/bundles/CampaignBundle/Executioner/Event/Decision.php create mode 100644 app/bundles/CampaignBundle/Executioner/Event/EventInterface.php create mode 100644 app/bundles/CampaignBundle/Executioner/EventExecutioner.php create mode 100644 app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php create mode 100644 app/bundles/CampaignBundle/Executioner/Exception/NoEventsFound.php create mode 100644 app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php create mode 100644 app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php create mode 100644 app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php create mode 100644 app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/Exception/ExecutionProhibitedException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/Exception/NotSchedulableException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/Exception/NotTimeYetException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php create mode 100644 app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php create mode 100644 app/bundles/CampaignBundle/Helper/ChannelExtractor.php diff --git a/app/bundles/CampaignBundle/CampaignEvents.php b/app/bundles/CampaignBundle/CampaignEvents.php index 99e6ae5353d..0a5170e19a4 100644 --- a/app/bundles/CampaignBundle/CampaignEvents.php +++ b/app/bundles/CampaignBundle/CampaignEvents.php @@ -98,33 +98,61 @@ final class CampaignEvents const LEAD_CAMPAIGN_BATCH_CHANGE = 'mautic.lead_campaign_batch_change'; /** - * The mautic.campaign_on_event_execution event is dispatched when a campaign event is executed. + * The mautic.campaign_on_event_executed event is dispatched when a campaign event is executed. * - * The event listener receives a - * Mautic\CampaignBundle\Event\CampaignExecutionEvent instance. + * The event listener receives a Mautic\CampaignBundle\Event\ExecutedEvent instance. * * @var string */ - const ON_EVENT_EXECUTION = 'mautic.campaign_on_event_execution'; + const ON_EVENT_EXECUTED = 'mautic.campaign_on_event_executed'; /** - * The mautic.campaign_on_event_decision_trigger event is dispatched after a lead decision triggers a set of actions or if the decision is set - * as a root level event. + * The mautic.campaign_on_event_scheduled event is dispatched when a campaign event is scheduled or scheduling is modified. * - * The event listener receives a - * Mautic\CampaignBundle\Event\CampaignDecisionEvent instance. + * The event listener receives a Mautic\CampaignBundle\Event\ScheduledEvent instance. * * @var string */ - const ON_EVENT_DECISION_TRIGGER = 'mautic.campaign_on_event_decision_trigger'; + const ON_EVENT_SCHEDULED = 'matuic.campaign_on_event_scheduled'; /** - * The mautic.campaign_on_event_scheduled event is dispatched when a campaign event is scheduled or scheduling is modified. + * The mautic.campaign_on_event_failed event is dispatched when an event fails for whatever reason. * - * The event listener receives a - * Mautic\CampaignBundle\Event\CampaignScheduledEvent instance. + * The event listener receives a Mautic\CampaignBundle\Event\FailedEvent instance. * * @var string */ - const ON_EVENT_SCHEDULED = 'mautic.campaign_on_event_scheduled'; + const ON_EVENT_FAILED = 'matuic.campaign_on_event_failed'; + + /** + * The mautic.campaign_on_event_decision event is dispatched when a campaign event is executed. + * + * The event listener receives a Mautic\CampaignBundle\Event\DecisionEvent instance. + * + * @var string + */ + const ON_EVENT_DECISION = 'mautic.campaign_on_event_decision'; + + /** + * @deprecated 2.13.0; to be removed in 3.0. Listen to ON_EVENT_EXECUTED and ON_EVENT_FAILED + * + * The mautic.campaign_on_event_execution event is dispatched when a campaign event is executed. + * + * The event listener receives a Mautic\CampaignBundle\Event\CampaignExecutionEvent instance. + * + * @var string + */ + const ON_EVENT_EXECUTION = 'mautic.campaign_on_event_execution'; + + /** + * @deprecated 2.13.0; to be removed in 3.0; Listen to ON_EVENT_DECISION instead + * + * The mautic.campaign_on_event_decision_trigger event is dispatched after a lead decision triggers a set of actions or if the decision is set + * as a root level event. + * + * The event listener receives a Mautic\CampaignBundle\Event\CampaignDecisionEvent instance. + * + * @var string + */ + const ON_EVENT_DECISION_TRIGGER = 'mautic.campaign_on_event_decision_trigger'; } diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index b56e2c438f2..edc80e06d94 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -79,6 +79,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $batch = $input->getOption('batch-limit'); $max = $input->getOption('max-events'); + $kickoff = $container->get('mautic.campaign.executioner.kickoff'); + if (!$this->checkRunStatus($input, $output, $id)) { return 0; } @@ -146,13 +148,16 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$negativeOnly && !$scheduleOnly) { //trigger starting action events for newly added contacts $output->writeln(''.$translator->trans('mautic.campaign.trigger.starting').''); - $processed = $model->triggerStartingEvents($c, $totalProcessed, $batch, $max, $output); - $output->writeln( + //$processed = $model->triggerStartingEvents($c, $totalProcessed, $batch, $max, $output); + + $kickoff->executeForCampaign($c, $batch, $max, $output); + /*$output->writeln( ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' ."\n" - ); + );*/ } + /* if ($max && $totalProcessed >= $max) { continue; } @@ -180,6 +185,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ."\n" ); } + */ } $em->detach($c); @@ -189,6 +195,7 @@ protected function execute(InputInterface $input, OutputInterface $output) unset($campaigns); } + $this->output->writeln('done'); $this->completeRun(); return 0; diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 93c44bb6616..5464c58493f 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -12,7 +12,7 @@ return [ 'routes' => [ 'main' => [ - 'mautic_campaignevent_action' => [ + 'mautic_campaignevent_action' => [ 'path' => '/campaigns/events/{objectAction}/{objectId}', 'controller' => 'MauticCampaignBundle:Event:execute', ], @@ -20,41 +20,41 @@ 'path' => '/campaigns/sources/{objectAction}/{objectId}', 'controller' => 'MauticCampaignBundle:Source:execute', ], - 'mautic_campaign_index' => [ + 'mautic_campaign_index' => [ 'path' => '/campaigns/{page}', 'controller' => 'MauticCampaignBundle:Campaign:index', ], - 'mautic_campaign_action' => [ + 'mautic_campaign_action' => [ 'path' => '/campaigns/{objectAction}/{objectId}', 'controller' => 'MauticCampaignBundle:Campaign:execute', ], - 'mautic_campaign_contacts' => [ + 'mautic_campaign_contacts' => [ 'path' => '/campaigns/view/{objectId}/contact/{page}', 'controller' => 'MauticCampaignBundle:Campaign:contacts', ], - 'mautic_campaign_preview' => [ + 'mautic_campaign_preview' => [ 'path' => '/campaign/preview/{objectId}', 'controller' => 'MauticEmailBundle:Public:preview', ], ], - 'api' => [ - 'mautic_api_campaignsstandard' => [ + 'api' => [ + 'mautic_api_campaignsstandard' => [ 'standard_entity' => true, 'name' => 'campaigns', 'path' => '/campaigns', 'controller' => 'MauticCampaignBundle:Api\CampaignApi', ], - 'mautic_api_campaigneventsstandard' => [ + 'mautic_api_campaigneventsstandard' => [ 'standard_entity' => true, 'supported_endpoints' => [ 'getone', 'getall', ], - 'name' => 'events', - 'path' => '/campaigns/events', - 'controller' => 'MauticCampaignBundle:Api\EventApi', + 'name' => 'events', + 'path' => '/campaigns/events', + 'controller' => 'MauticCampaignBundle:Api\EventApi', ], - 'mautic_api_campaigns_events_contact' => [ + 'mautic_api_campaigns_events_contact' => [ 'path' => '/campaigns/events/contact/{contactId}', 'controller' => 'MauticCampaignBundle:Api\EventLogApi:getContactEvents', 'method' => 'GET', @@ -64,38 +64,38 @@ 'controller' => 'MauticCampaignBundle:Api\EventLogApi:editContactEvent', 'method' => 'PUT', ], - 'mautic_api_campaigns_batchedit_events' => [ + 'mautic_api_campaigns_batchedit_events' => [ 'path' => '/campaigns/events/batch/edit', 'controller' => 'MauticCampaignBundle:Api\EventLogApi:editEvents', 'method' => 'PUT', ], - 'mautic_api_campaign_contact_events' => [ + 'mautic_api_campaign_contact_events' => [ 'path' => '/campaigns/{campaignId}/events/contact/{contactId}', 'controller' => 'MauticCampaignBundle:Api\EventLogApi:getContactEvents', 'method' => 'GET', ], - 'mautic_api_campaigngetcontacts' => [ + 'mautic_api_campaigngetcontacts' => [ 'path' => '/campaigns/{id}/contacts', 'controller' => 'MauticCampaignBundle:Api\CampaignApi:getContacts', ], - 'mautic_api_campaignaddcontact' => [ + 'mautic_api_campaignaddcontact' => [ 'path' => '/campaigns/{id}/contact/{leadId}/add', 'controller' => 'MauticCampaignBundle:Api\CampaignApi:addLead', 'method' => 'POST', ], - 'mautic_api_campaignremovecontact' => [ + 'mautic_api_campaignremovecontact' => [ 'path' => '/campaigns/{id}/contact/{leadId}/remove', 'controller' => 'MauticCampaignBundle:Api\CampaignApi:removeLead', 'method' => 'POST', ], // @deprecated 2.6.0 to be removed 3.0 - 'bc_mautic_api_campaignaddcontact' => [ + 'bc_mautic_api_campaignaddcontact' => [ 'path' => '/campaigns/{id}/contact/add/{leadId}', 'controller' => 'MauticCampaignBundle:Api\CampaignApi:addLead', 'method' => 'POST', ], - 'bc_mautic_api_campaignremovecontact' => [ + 'bc_mautic_api_campaignremovecontact' => [ 'path' => '/campaigns/{id}/contact/remove/{leadId}', 'controller' => 'MauticCampaignBundle:Api\CampaignApi:removeLead', 'method' => 'POST', @@ -118,16 +118,16 @@ 'campaign' => null, ], - 'services' => [ - 'events' => [ - 'mautic.campaign.subscriber' => [ + 'services' => [ + 'events' => [ + 'mautic.campaign.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\CampaignSubscriber', 'arguments' => [ 'mautic.helper.ip_lookup', 'mautic.core.model.auditlog', ], ], - 'mautic.campaign.leadbundle.subscriber' => [ + 'mautic.campaign.leadbundle.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\LeadSubscriber', 'arguments' => [ 'mautic.campaign.model.campaign', @@ -137,54 +137,54 @@ 'mautic.campaign.calendarbundle.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\CalendarSubscriber', ], - 'mautic.campaign.pointbundle.subscriber' => [ + 'mautic.campaign.pointbundle.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\PointSubscriber', ], - 'mautic.campaign.search.subscriber' => [ + 'mautic.campaign.search.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\SearchSubscriber', 'arguments' => [ 'mautic.campaign.model.campaign', ], ], - 'mautic.campaign.dashboard.subscriber' => [ + 'mautic.campaign.dashboard.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\DashboardSubscriber', 'arguments' => [ 'mautic.campaign.model.campaign', 'mautic.campaign.model.event', ], ], - 'mautic.campaignconfigbundle.subscriber' => [ + 'mautic.campaignconfigbundle.subscriber' => [ 'class' => 'Mautic\CampaignBundle\EventListener\ConfigSubscriber', ], - 'mautic.campaign.stats.subscriber' => [ + 'mautic.campaign.stats.subscriber' => [ 'class' => \Mautic\CampaignBundle\EventListener\StatsSubscriber::class, 'arguments' => [ 'doctrine.orm.entity_manager', ], ], - 'mautic.campaign.report.subscriber' => [ + 'mautic.campaign.report.subscriber' => [ 'class' => \Mautic\CampaignBundle\EventListener\ReportSubscriber::class, 'arguments' => [ 'mautic.lead.model.company_report_data', ], ], ], - 'forms' => [ - 'mautic.campaign.type.form' => [ + 'forms' => [ + 'mautic.campaign.type.form' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\CampaignType', 'arguments' => 'mautic.factory', 'alias' => 'campaign', ], - 'mautic.campaignrange.type.action' => [ + 'mautic.campaignrange.type.action' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\EventType', 'alias' => 'campaignevent', ], - 'mautic.campaign.type.campaignlist' => [ + 'mautic.campaign.type.campaignlist' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\CampaignListType', 'arguments' => 'mautic.factory', 'alias' => 'campaign_list', ], - 'mautic.campaign.type.trigger.leadchange' => [ + 'mautic.campaign.type.trigger.leadchange' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\CampaignEventLeadChangeType', 'alias' => 'campaignevent_leadchange', ], @@ -192,32 +192,33 @@ 'class' => 'Mautic\CampaignBundle\Form\Type\CampaignEventAddRemoveLeadType', 'alias' => 'campaignevent_addremovelead', ], - 'mautic.campaign.type.canvassettings' => [ + 'mautic.campaign.type.canvassettings' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\EventCanvasSettingsType', 'alias' => 'campaignevent_canvassettings', ], - 'mautic.campaign.type.leadsource' => [ + 'mautic.campaign.type.leadsource' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\CampaignLeadSourceType', 'arguments' => 'mautic.factory', 'alias' => 'campaign_leadsource', ], - 'mautic.form.type.campaignconfig' => [ + 'mautic.form.type.campaignconfig' => [ 'class' => 'Mautic\CampaignBundle\Form\Type\ConfigType', 'arguments' => 'translator', 'alias' => 'campaignconfig', ], ], - 'models' => [ - 'mautic.campaign.model.campaign' => [ + 'models' => [ + 'mautic.campaign.model.campaign' => [ 'class' => 'Mautic\CampaignBundle\Model\CampaignModel', 'arguments' => [ 'mautic.helper.core_parameters', 'mautic.lead.model.lead', 'mautic.lead.model.list', 'mautic.form.model.form', + 'mautic.campaign.event_collector', ], ], - 'mautic.campaign.model.event' => [ + 'mautic.campaign.model.event' => [ 'class' => 'Mautic\CampaignBundle\Model\EventModel', 'arguments' => [ 'mautic.helper.ip_lookup', @@ -238,6 +239,131 @@ ], ], ], + 'repositories' => [ + 'mautic.campaign.repository.campaign' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\CampaignBundle\Entity\Campaign::class, + ], + ], + 'mautic.campaign.repository.lead_event_log' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\CampaignBundle\Entity\LeadEventLog::class, + ], + ], + ], + 'execution' => [ + 'mautic.campaign.contact_finder.kickoff' => [ + 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts::class, + 'arguments' => [ + 'mautic.lead.repository.lead', + 'mautic.campaign.repository.campaign', + 'monolog.logger.mautic', + ], + ], + 'mautic.campaign.event_dispatcher' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher::class, + 'arguments' => [ + 'event_dispatcher', + 'monolog.logger.mautic', + 'mautic.campaign.scheduler', + 'mautic.campaign.legacy_event_dispatcher', + ], + ], + 'mautic.campaign.event_logger' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Logger\EventLogger::class, + 'arguments' => [ + 'mautic.helper.ip_lookup', + 'mautic.lead.model.lead', + 'mautic.campaign.repository.lead_event_log', + ], + ], + 'mautic.campaign.event_collector' => [ + 'class' => \Mautic\CampaignBundle\EventCollector\EventCollector::class, + 'arguments' => [ + 'translator', + 'event_dispatcher', + ], + ], + 'mautic.campaign.scheduler.datetime' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Scheduler\Mode\DateTime::class, + 'arguments' => [ + 'monolog.logger.mautic', + ], + ], + 'mautic.campaign.scheduler.interval' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Scheduler\Mode\Interval::class, + 'arguments' => [ + 'monolog.logger.mautic', + ], + ], + 'mautic.campaign.scheduler' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler::class, + 'arguments' => [ + 'monolog.logger.mautic', + 'mautic.campaign.event_logger', + 'mautic.campaign.scheduler.interval', + 'mautic.campaign.scheduler.datetime', + 'mautic.campaign.event_collector', + 'event_dispatcher', + 'mautic.helper.core_parameters', + ], + ], + 'mautic.campaign.executioner.action' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Event\Action::class, + 'arguments' => [ + 'mautic.campaign.event_logger', + 'mautic.campaign.event_dispatcher', + ], + ], + 'mautic.campaign.executioner.condition' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Event\Condition::class, + 'arguments' => [ + 'mautic.campaign.event_logger', + 'mautic.campaign.event_dispatcher', + ], + ], + 'mautic.campaign.executioner.decision' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Event\Decision::class, + 'arguments' => [ + 'mautic.campaign.event_logger', + 'mautic.campaign.event_dispatcher', + ], + ], + 'mautic.campaign.executioner' => [ + 'class' => \Mautic\CampaignBundle\Executioner\EventExecutioner::class, + 'arguments' => [ + 'mautic.campaign.event_collector', + 'mautic.campaign.executioner.action', + 'mautic.campaign.executioner.condition', + 'mautic.campaign.executioner.decision', + 'monolog.logger.mautic', + ], + ], + 'mautic.campaign.executioner.kickoff' => [ + 'class' => \Mautic\CampaignBundle\Executioner\KickoffExecutioner::class, + 'arguments' => [ + 'monolog.logger.mautic', + 'mautic.campaign.contact_finder.kickoff', + 'translator', + 'mautic.campaign.executioner', + 'mautic.campaign.scheduler', + ], + ], + // @deprecated 2.13.0 for BC support; to be removed in 3.0 + 'mautic.campaign.legacy_event_dispatcher' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher::class, + 'arguments' => [ + 'event_dispatcher', + 'mautic.campaign.scheduler', + 'monolog.logger.mautic', + 'mautic.factory', + ], + ], + ], ], 'parameters' => [ 'campaign_time_wait_on_event_false' => 'PT1H', diff --git a/app/bundles/CampaignBundle/Entity/Campaign.php b/app/bundles/CampaignBundle/Entity/Campaign.php index 56752320871..52f531c95d4 100644 --- a/app/bundles/CampaignBundle/Entity/Campaign.php +++ b/app/bundles/CampaignBundle/Entity/Campaign.php @@ -160,9 +160,14 @@ public static function loadMetadata(ORM\ClassMetadata $metadata) */ public static function loadValidatorMetadata(ClassMetadata $metadata) { - $metadata->addPropertyConstraint('name', new Assert\NotBlank([ - 'message' => 'mautic.core.name.required', - ])); + $metadata->addPropertyConstraint( + 'name', + new Assert\NotBlank( + [ + 'message' => 'mautic.core.name.required', + ] + ) + ); } /** @@ -325,13 +330,23 @@ public function removeEvent(\Mautic\CampaignBundle\Entity\Event $event) /** * Get events. * - * @return \Doctrine\Common\Collections\Collection + * @return \Doctrine\Common\Collections\ArrayCollection */ public function getEvents() { return $this->events; } + /** + * @return ArrayCollection|\Doctrine\Common\Collections\Collection + */ + public function getRootEvents() + { + $criteria = Criteria::create()->where(Criteria::expr()->isNull('parent')); + + return $this->getEvents()->matching($criteria); + } + /** * Set publishUp. * diff --git a/app/bundles/CampaignBundle/Entity/ChannelInterface.php b/app/bundles/CampaignBundle/Entity/ChannelInterface.php new file mode 100644 index 00000000000..4d537afeb11 --- /dev/null +++ b/app/bundles/CampaignBundle/Entity/ChannelInterface.php @@ -0,0 +1,39 @@ +log = $log; + if ($log) { + $log->setFailedLog($this); + } + return $this; } diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLog.php b/app/bundles/CampaignBundle/Entity/LeadEventLog.php index aaf561265d6..7c979e79913 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLog.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLog.php @@ -20,7 +20,7 @@ /** * Class LeadEventLog. */ -class LeadEventLog +class LeadEventLog implements ChannelInterface { /** * @var diff --git a/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php b/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php index 5dcd58d9ba4..a1979c48d2c 100644 --- a/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php @@ -14,6 +14,7 @@ use Mautic\CoreBundle\Event\ComponentValidationTrait; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Translation\TranslatorInterface; /** * Class CampaignBuilderEvent. @@ -50,9 +51,11 @@ class CampaignBuilderEvent extends Event private $sortCache = []; /** - * @param \Symfony\Bundle\FrameworkBundle\Translation\Translator $translator + * CampaignBuilderEvent constructor. + * + * @param TranslatorInterface $translator */ - public function __construct($translator) + public function __construct(TranslatorInterface $translator) { $this->translator = $translator; } @@ -226,7 +229,7 @@ public function addAction($key, array $action) //check for required keys and that given functions are callable $this->verifyComponent( - ['label', ['eventName', 'callback']], + ['label', ['batchEventName', 'eventName', 'callback']], $action, ['callback'] ); diff --git a/app/bundles/CampaignBundle/Event/CampaignDecisionEvent.php b/app/bundles/CampaignBundle/Event/CampaignDecisionEvent.php index 4e473c34d12..fd7923eae85 100644 --- a/app/bundles/CampaignBundle/Event/CampaignDecisionEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignDecisionEvent.php @@ -16,6 +16,8 @@ /** * Class CampaignDecisionEvent. + * + * @deprecated 2.13.0; to be removed in 3.0 */ class CampaignDecisionEvent extends Event { diff --git a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php index 7bd8b3cc8e6..e3877cd6198 100644 --- a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php @@ -17,6 +17,8 @@ /** * Class CampaignExecutionEvent. + * + * @deprecated 2.13.0; to be removed in 3.0 */ class CampaignExecutionEvent extends Event { diff --git a/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php b/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php index a443ede2ff1..b4ee6e5426e 100644 --- a/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php @@ -16,6 +16,8 @@ /** * Class CampaignScheduledEvent. + * + * @deprecated 2.13.0; to be removed in 3.0 */ class CampaignScheduledEvent extends Event { diff --git a/app/bundles/CampaignBundle/Event/DecisionEvent.php b/app/bundles/CampaignBundle/Event/DecisionEvent.php new file mode 100644 index 00000000000..c572604fae7 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/DecisionEvent.php @@ -0,0 +1,16 @@ +convertToArray(); + $campaign = $event->getCampaign(); + + $eventArray['campaign'] = [ + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), + ]; + + return $eventArray; + } +} diff --git a/app/bundles/CampaignBundle/Event/ExecutedEvent.php b/app/bundles/CampaignBundle/Event/ExecutedEvent.php new file mode 100644 index 00000000000..5b817b0bf22 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/ExecutedEvent.php @@ -0,0 +1,57 @@ +config = $config; + $this->log = $log; + } + + /** + * @return AbstractEventAccessor + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return LeadEventLog + */ + public function getLog() + { + return $this->log; + } +} diff --git a/app/bundles/CampaignBundle/Event/FailedEvent.php b/app/bundles/CampaignBundle/Event/FailedEvent.php new file mode 100644 index 00000000000..e01de92031a --- /dev/null +++ b/app/bundles/CampaignBundle/Event/FailedEvent.php @@ -0,0 +1,57 @@ +config = $config; + $this->log = $log; + } + + /** + * @return AbstractEventAccessor + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return LeadEventLog + */ + public function getLog() + { + return $this->log; + } +} diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php new file mode 100644 index 00000000000..d9a143be694 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -0,0 +1,202 @@ +config = $config; + $this->event = $event; + $this->pending = $pending; + + $this->failures = new ArrayCollection(); + $this->successful = new ArrayCollection(); + } + + /** + * @return AbstractEventAccessor + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return Event + */ + public function getEvent() + { + return $this->event; + } + + /** + * @return ArrayCollection + */ + public function getPending() + { + return $this->pending; + } + + /** + * @param LeadEventLog $log + * @param string $reason + */ + public function fail(LeadEventLog $log, $reason) + { + if (!$failedLog = $log->getFailedLog()) { + $failedLog = new FailedLeadEventLog(); + } + + $failedLog->setLog($log) + ->setDateAdded(new \DateTime()) + ->setReason($reason); + + // Used by the UI + $metadata = $log->getMetadata(); + $metadata = array_merge( + $metadata, + [ + 'failed' => 1, + 'reason' => $reason, + ] + ); + $log->setMetadata($metadata); + + $this->logChannel($log); + + $this->failures->add($log); + } + + /** + * @param $reason + */ + public function failAll($reason) + { + foreach ($this->pending as $log) { + $this->fail($log, $reason); + } + } + + /** + * @param LeadEventLog $log + */ + public function pass(LeadEventLog $log) + { + if ($failedLog = $log->getFailedLog()) { + // Delete existing entries + $failedLog->setLog(null); + $log->setFailedLog(null); + + $metadata = $log->getMetadata(); + unset($metadata['errors']); + $log->setMetadata($metadata); + } + $this->logChannel($log); + $this->successful->add($log); + } + + /** + * @return ArrayCollection + */ + public function getFailures() + { + return $this->failures; + } + + /** + * @return ArrayCollection + */ + public function getSuccessful() + { + return $this->successful; + } + + /** + * @param $channel + * @param null $channelId + */ + public function setChannel($channel, $channelId = null) + { + $this->channel = $channel; + $this->channelId = $channelId; + } + + /** + * Check if an event is applicable. + * + * @param $eventType + */ + public function checkContext($eventType) + { + return strtolower($eventType) === strtolower($this->event->getType()); + } + + /** + * @param LeadEventLog $log + */ + private function logChannel(LeadEventLog $log) + { + if ($this->channel) { + $log->setChannel($this->channel) + ->setChannelId($this->channelId); + } + } +} diff --git a/app/bundles/CampaignBundle/Event/ScheduledEvent.php b/app/bundles/CampaignBundle/Event/ScheduledEvent.php new file mode 100644 index 00000000000..4fecf468828 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/ScheduledEvent.php @@ -0,0 +1,77 @@ +config = $config; + $this->log = $log; + + // @deprecated support for pre 2.13.0; to be removed in 3.0 + parent::__construct( + [ + 'eventSettings' => $config->getConfig(), + 'eventDetails' => null, + 'event' => $this->getEventArray($log->getEvent()), + 'lead' => $log->getLead(), + 'systemTriggered' => true, + 'dateScheduled' => $log->getTriggerDate(), + ], + $log + ); + } + + /** + * @return AbstractEventAccessor + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return ArrayCollection + */ + public function getLog() + { + return $this->log; + } +} diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php new file mode 100644 index 00000000000..be3ff415c79 --- /dev/null +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php @@ -0,0 +1,165 @@ +config = $config; + + $this->filterExtraProperties(); + } + + /** + * @return string + */ + public function getLabel() + { + return $this->getProperty('label'); + } + + /** + * @return string + */ + public function getDescription() + { + return $this->getProperty('description'); + } + + /** + * @return mixed + */ + public function getBatchEventName() + { + return $this->getProperty('batchEventName'); + } + + /** + * @return string + */ + public function getFormType() + { + return $this->getProperty('formType'); + } + + /** + * @return array + */ + public function getFormTypeOptions() + { + return $this->getProperty('formTypeOptions', []); + } + + /** + * @return string + */ + public function getFormTheme() + { + return $this->getProperty('formTheme'); + } + + /** + * @return string + */ + public function getTimelineTemplate() + { + return $this->getProperty('timelineTemplate'); + } + + /** + * @return array + */ + public function getConnectionRestrictions() + { + return $this->getProperty('connectionRestrictions', []); + } + + /** + * @return array + */ + public function getExtraProperties() + { + return $this->extraProperties; + } + + /** + * @return string + */ + public function getChannel() + { + return $this->getProperty('channel'); + } + + /** + * @return mixed + */ + public function getChannelIdField() + { + return $this->getProperty('channelIdField'); + } + + /** + * @deprecated pre 2.13.0 support; to be removed in 3.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param string $property + * @param mixed $default + * + * @return mixed + */ + protected function getProperty($property, $default = null) + { + return (isset($this->config[$property])) ? $this->config[$property] : $default; + } + + private function filterExtraProperties() + { + $this->extraProperties = array_diff_key($this->config, array_flip($this->systemProperties)); + } +} diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php new file mode 100644 index 00000000000..f16ef421fc0 --- /dev/null +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php @@ -0,0 +1,16 @@ +buildEvents($events); + } + + /** + * @param $type + * @param $key + * + * @return AbstractEventAccessor + * + * @throws TypeNotFoundException + * @throws EventNotFoundException + */ + public function getEvent($type, $key) + { + switch ($type) { + case Event::TYPE_ACTION: + return $this->getAction($key); + case Event::TYPE_CONDITION: + return $this->getCondition($key); + case Event::TYPE_DECISION: + return $this->getDecision($key); + default: + throw new TypeNotFoundException("$type is not a valid event type"); + } + } + + /** + * @param $key + * + * @return ActionAccessor + */ + public function getAction($key) + { + if (!isset($this->actions[$key])) { + throw new EventNotFoundException("Action $key is not valid"); + } + + return $this->actions[$key]; + } + + /** + * @return array + */ + public function getActions() + { + return $this->actions; + } + + /** + * @param $key + * + * @return ConditionAccessor + */ + public function getCondition($key) + { + if (!isset($this->conditions[$key])) { + throw new EventNotFoundException("Condition $key is not valid"); + } + + return $this->conditions[$key]; + } + + /** + * @return array + */ + public function getConditions() + { + return $this->conditions; + } + + /** + * @param $key + * + * @return DecisionAccessor + */ + public function getDecision($key) + { + if (!isset($this->decisions[$key])) { + throw new EventNotFoundException("Decision $key is not valid"); + } + + return $this->decisions[$key]; + } + + /** + * @return array + */ + public function getDecisions() + { + return $this->decisions; + } + + /** + * @param array $events + */ + private function buildEvents(array $events) + { + if (isset($events[Event::TYPE_ACTION])) { + $this->actions = EventBuilder::buildActions($events[Event::TYPE_ACTION]); + } + + if (isset($events[Event::TYPE_CONDITION])) { + $this->conditions = EventBuilder::buildConditions($events[Event::TYPE_CONDITION]); + } + + if (isset($events[Event::TYPE_DECISION])) { + $this->decisions = EventBuilder::buildDecisions($events[Event::TYPE_DECISION]); + } + } +} diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Exception/EventNotFoundException.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Exception/EventNotFoundException.php new file mode 100644 index 00000000000..3e8d93c22f5 --- /dev/null +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Exception/EventNotFoundException.php @@ -0,0 +1,16 @@ + []]; + + /** + * Used by JS/JsPlumb to restrict how events can be associated to each other in the UI. + * + * @param array $events + * + * @return array + */ + public static function buildRestrictionsArray(array $events) + { + self::$eventTypes = array_fill_keys(array_keys($events), []); + foreach ($events as $eventType => $typeEvents) { + foreach ($typeEvents as $key => $event) { + self::addTypeConnection($eventType, $key, $event); + } + } + + return self::$connectionRestrictions; + } + + /** + * @param $eventType + * @param $key + * @param array $event + */ + private static function addTypeConnection($eventType, $key, array $event) + { + if (!isset(self::$connectionRestrictions[$key])) { + self::$connectionRestrictions[$key] = [ + 'source' => self::$eventTypes, + 'target' => self::$eventTypes, + ]; + } + + if (!isset($connectionRestrictions[$key])) { + $connectionRestrictions['anchor'][$key] = []; + } + + if (isset($event['connectionRestrictions'])) { + foreach ($event['connectionRestrictions'] as $restrictionType => $restrictions) { + self::addRestriction($key, $restrictionType, $restrictions); + } + } + + self::addDeprecatedAnchorRestrictions($eventType, $key, $event); + } + + /** + * @param $key + * @param $restrictionType + * @param array $restrictions + */ + private static function addRestriction($key, $restrictionType, array $restrictions) + { + switch ($restrictionType) { + case 'source': + case 'target': + foreach ($restrictions as $groupType => $groupRestrictions) { + self::$connectionRestrictions[$key][$restrictionType][$groupType] += $groupRestrictions; + } + break; + case 'anchor': + foreach ($restrictions as $anchor) { + list($group, $anchor) = explode('.', $anchor); + self::$connectionRestrictions[$restrictionType][$group][$key][] = $anchor; + } + + break; + } + } + + /** + * @deprecated 2.6.0 to be removed in 3.0; BC support + * + * @param $eventType + * @param $event + * @param $key + */ + private static function addDeprecatedAnchorRestrictions($eventType, $key, array $event) + { + switch ($eventType) { + case Event::TYPE_ACTION: + if (isset($event['associatedActions'])) { + self::$connectionRestrictions[$key]['target']['action'] += $event['associatedActions']; + } + break; + case Event::TYPE_DECISION: + if (isset($event['associatedDecisions'])) { + self::$connectionRestrictions[$key]['source']['decision'] += $event['associatedDecisions']; + } + break; + } + + if (isset($event['anchorRestrictions'])) { + foreach ($event['anchorRestrictions'] as $restriction) { + list($group, $anchor) = explode('.', $restriction); + self::$connectionRestrictions['anchor'][$key][$group][] = $anchor; + } + } + } +} diff --git a/app/bundles/CampaignBundle/EventCollector/Builder/EventBuilder.php b/app/bundles/CampaignBundle/EventCollector/Builder/EventBuilder.php new file mode 100644 index 00000000000..383095fe2af --- /dev/null +++ b/app/bundles/CampaignBundle/EventCollector/Builder/EventBuilder.php @@ -0,0 +1,64 @@ + $actionArray) { + $converted[$key] = new ActionAccessor($actionArray); + } + + return $converted; + } + + /** + * @param array $conditions + * + * @return array + */ + public static function buildConditions(array $conditions) + { + $converted = []; + foreach ($conditions as $key => $conditionArray) { + $converted[$key] = new ConditionAccessor($conditionArray); + } + + return $converted; + } + + /** + * @param array $decisions + * + * @return array + */ + public static function buildDecisions(array $decisions) + { + $converted = []; + foreach ($decisions as $key => $decisionArray) { + $converted[$key] = new DecisionAccessor($decisionArray); + } + + return $converted; + } +} diff --git a/app/bundles/CampaignBundle/EventCollector/EventCollector.php b/app/bundles/CampaignBundle/EventCollector/EventCollector.php new file mode 100644 index 00000000000..9be1aadd517 --- /dev/null +++ b/app/bundles/CampaignBundle/EventCollector/EventCollector.php @@ -0,0 +1,122 @@ +translator = $translator; + $this->dispatcher = $dispatcher; + } + + /** + * @return EventAccessor + */ + public function getEvents() + { + if (empty($this->eventsArray)) { + $this->buildEventList(); + } + + if (empty($this->events)) { + $this->events = new EventAccessor($this->eventsArray); + } + + return $this->events; + } + + /** + * @param $type + * @param $key + * + * @return AbstractEventAccessor + */ + public function getEventConfig(Event $event) + { + return $this->getEvents()->getEvent($event->getEventType(), $event->getType()); + } + + /** + * Deprecated support for pre 2.13. + * + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param null $type + * + * @return array|mixed + */ + public function getEventsArray($type = null) + { + if (empty($this->eventsArray)) { + $this->buildEventList(); + } + + if (null !== $type) { + if (!isset($this->events[$type])) { + throw new \InvalidArgumentException("$type not found as array key"); + } + + return $this->eventsArray[$type]; + } + + return $this->eventsArray; + } + + private function buildEventList() + { + //build them + $event = new CampaignBuilderEvent($this->translator); + $this->dispatcher->dispatch(CampaignEvents::CAMPAIGN_ON_BUILD, $event); + + $this->eventsArray[Event::TYPE_ACTION] = $event->getActions(); + $this->eventsArray[Event::TYPE_CONDITION] = $event->getConditions(); + $this->eventsArray[Event::TYPE_DECISION] = $event->getDecisions(); + + $this->eventsArray['connectionRestrictions'] = ConnectionBuilder::buildRestrictionsArray($this->eventsArray); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php new file mode 100644 index 00000000000..43738b9b102 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php @@ -0,0 +1,16 @@ +leadRepository = $leadRepository; + $this->campaignRepository = $campaignRepository; + $this->logger = $logger; + } + + /** + * @param $campaignId + * @param $limit + * @param null $specificContactId + * + * @return Lead[]|ArrayCollection + * + * @throws NoContactsFound + */ + public function getContacts($campaignId, $limit, $specificContactId = null) + { + // Get list of all campaign leads; start is always zero in practice because of $pendingOnly + if ($campaignLeads = ($specificContactId) ? [$specificContactId] : $this->campaignRepository->getCampaignLeadIds($campaignId, 0, $limit, true)) { + $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignLeads)); + } + + if (empty($campaignLeads)) { + // No new contacts found in the campaign + + throw new NoContactsFound(); + } + + // Fetch entity objects for the found contacts + $contacts = $this->leadRepository->getContactCollection($campaignLeads); + + if (!count($contacts)) { + // Just a precaution in case non-existent contacts are lingering in the campaign leads table + $this->logger->debug('CAMPAIGN: No contact entities found.'); + + throw new NoContactsFound(); + } + + return $contacts; + } + + /** + * @param $campaignId + * @param array $eventIds + * @param null $specificContactId + * + * @return mixed + */ + public function getContactCount($campaignId, array $eventIds, $specificContactId = null) + { + return $this->campaignRepository->getCampaignLeadCount($campaignId, $specificContactId, $eventIds); + } + + /** + * Clear Lead entities from memory. + */ + public function clear() + { + $this->leadRepository->clear(); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php new file mode 100644 index 00000000000..b1affb8088b --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php @@ -0,0 +1,17 @@ +getDecisionPath() && Event::TYPE_CONDITION !== $event->getType() && $inactionPathProhibted) { +} diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php new file mode 100644 index 00000000000..9c1be706e13 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -0,0 +1,156 @@ +dispatcher = $dispatcher; + $this->logger = $logger; + $this->scheduler = $scheduler; + $this->legacyDispatcher = $legacyDispatcher; + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * + * @throws LogNotProcessedException + * @throws LogPassedAndFailedException + */ + public function executeEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + { + // this if statement can be removed when legacy dispatcher is removed + if ($customEvent = $config->getBatchEventName()) { + $pendingEvent = new PendingEvent($config, $event, $logs); + $this->dispatcher->dispatch($customEvent, $pendingEvent); + + $success = $pendingEvent->getSuccessful(); + $this->dispatchExecutedEvent($config, $success); + + $failed = $pendingEvent->getFailures(); + $this->dispatchedFailedEvent($config, $failed); + + $this->validateProcessedLogs($logs, $success, $failed); + + // Dispatch legacy ON_EVENT_EXECUTION event for BC + $this->legacyDispatcher->dispatchExecutionEvents($config, $success, $failed); + } + + // Execute BC eventName or callback. Or support case where the listener has been converted to batchEventName but still wants to execute + // eventName for BC support for plugins that could be listening to it's own custom event. + $this->legacyDispatcher->dispatchCustomEvent($config, $event, $logs, ($customEvent)); + } + + /** + * @param AbstractEventAccessor $config + * @param ArrayCollection $logs + */ + public function dispatchExecutedEvent(AbstractEventAccessor $config, ArrayCollection $logs) + { + foreach ($logs as $log) { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_EXECUTED, + new ExecutedEvent($config, $log) + ); + } + } + + /** + * @param AbstractEventAccessor $config + * @param ArrayCollection $logs + */ + public function dispatchedFailedEvent(AbstractEventAccessor $config, ArrayCollection $logs) + { + foreach ($logs as $log) { + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($log->getEvent()->getEventType()).' ID# '.$log->getEvent()->getId().' for contact ID# '.$log->getLead()->getId() + ); + + $this->scheduler->rescheduleFailure($log); + + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_FAILED, + new FailedEvent($config, $log) + ); + } + } + + /** + * @param ArrayCollection $pending + * @param ArrayCollection $success + * @param ArrayCollection $failed + * + * @throws LogNotProcessedException + * @throws LogPassedAndFailedException + */ + private function validateProcessedLogs(ArrayCollection $pending, ArrayCollection $success, ArrayCollection $failed) + { + foreach ($pending as $log) { + if (!$success->contains($log) && !$failed->contains($log)) { + throw new LogNotProcessedException($log); + } + + if ($success->contains($log) && $failed->contains($log)) { + throw new LogPassedAndFailedException($log); + } + } + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogNotProcessedException.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogNotProcessedException.php new file mode 100644 index 00000000000..2cfb3b6d04b --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogNotProcessedException.php @@ -0,0 +1,27 @@ +getId()} must be passed to either pass() or fail()", 0, null); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php new file mode 100644 index 00000000000..d0bc7509849 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php @@ -0,0 +1,27 @@ +getId()} was passed to both pass() or fail(). Pass or fail the log, not both.", 0, null); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php new file mode 100644 index 00000000000..d203b383a94 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -0,0 +1,322 @@ +dispatcher = $dispatcher; + $this->scheduler = $scheduler; + $this->logger = $logger; + $this->factory = $factory; + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param bool $wasBatchProcessed + */ + public function dispatchCustomEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, $wasBatchProcessed) + { + $settings = $config->getConfig(); + + if (!isset($settings['eventName']) && !isset($settings['callback'])) { + return; + } + + $eventArray = $this->getEventArray($event); + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + if (isset($settings['eventName'])) { + $result = $this->dispatchEventName($settings['eventName'], $settings, $eventArray, $log); + } else { + if (!is_callable($settings['callback'])) { + // No use to keep trying for the other logs as it won't ever work + break; + } + + $result = $this->dispatchCallback($settings, $eventArray, $log); + } + + if (!$wasBatchProcessed) { + $this->dispatchExecutionEvent($config, $log, $result); + + // Dispatch new events for legacy processed logs + if ($this->isFailed($result)) { + $this->processFailedLog($result, $log); + $this->scheduler->rescheduleFailure($log); + + $this->dispatchFailedEvent($config, $log); + + return; + } + + $this->dispatchExecutedEvent($config, $log); + } + } + } + + /** + * Execute the new ON_EVENT_FAILED and ON_EVENT_EXECUTED events for logs processed by BC code. + * + * @param AbstractEventAccessor $config + * @param ArrayCollection $success + * @param ArrayCollection $failures + */ + public function dispatchExecutionEvents(AbstractEventAccessor $config, ArrayCollection $success, ArrayCollection $failures) + { + foreach ($success as $log) { + $this->dispatchExecutionEvent($config, $log, true); + } + + foreach ($failures as $log) { + $this->dispatchExecutionEvent($config, $log, false); + } + } + + /** + * @param $eventName + * @param array $settings + * @param array $eventArray + * @param LeadEventLog $log + * + * @return bool + */ + private function dispatchEventName($eventName, array $settings, array $eventArray, LeadEventLog $log) + { + @trigger_error('eventName is deprecated. Convert to using batchEventName.', E_USER_DEPRECATED); + + // Create a campaign event with a default successful result + $campaignEvent = new CampaignExecutionEvent( + [ + 'eventSettings' => $settings, + 'eventDetails' => null, // @todo fix when procesing decisions, + 'event' => $eventArray, + 'lead' => $log->getLead(), + 'systemTriggered' => $log->getSystemTriggered(), + 'config' => $eventArray['properties'], + ], + null, + $log + ); + + $this->dispatcher->dispatch($eventName, $campaignEvent); + + $result = $campaignEvent->getResult(); + + $log->setChannel($campaignEvent->getChannel()) + ->setChannelId($campaignEvent->getChannelId()); + + return $result; + } + + /** + * @param array $settings + * @param array $eventArray + * @param LeadEventLog $log + * + * @return mixed + * + * @throws \ReflectionException + */ + private function dispatchCallback(array $settings, array $eventArray, LeadEventLog $log) + { + @trigger_error('callback is deprecated. Convert to using batchEventName.', E_USER_DEPRECATED); + + $args = [ + 'eventSettings' => $settings, + 'eventDetails' => null, // @todo fix when procesing decisions, + 'event' => $eventArray, + 'lead' => $log->getLead(), + 'factory' => $this->factory, + 'systemTriggered' => $log->getSystemTriggered(), + 'config' => $eventArray['properties'], + ]; + + if (is_array($settings['callback'])) { + $reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]); + } elseif (strpos($settings['callback'], '::') !== false) { + $parts = explode('::', $settings['callback']); + $reflection = new \ReflectionMethod($parts[0], $parts[1]); + } else { + $reflection = new \ReflectionMethod(null, $settings['callback']); + } + + $pass = []; + foreach ($reflection->getParameters() as $param) { + if (isset($args[$param->getName()])) { + $pass[] = $args[$param->getName()]; + } else { + $pass[] = null; + } + } + + return $reflection->invokeArgs($this, $pass); + } + + /** + * @param AbstractEventAccessor $config + * @param LeadEventLog $log + * @param $result + */ + private function dispatchExecutionEvent(AbstractEventAccessor $config, LeadEventLog $log, $result) + { + $eventArray = $this->getEventArray($log->getEvent()); + + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_EXECUTION, + new CampaignExecutionEvent( + [ + 'eventSettings' => $config->getConfig(), + 'eventDetails' => null, // @todo fix when procesing decisions, + 'event' => $eventArray, + 'lead' => $log->getLead(), + 'systemTriggered' => $log->getSystemTriggered(), + 'config' => $eventArray['properties'], + ], + $result, + $log + ) + ); + } + + /** + * @param AbstractEventAccessor $config + * @param LeadEventLog $log + */ + private function dispatchExecutedEvent(AbstractEventAccessor $config, LeadEventLog $log) + { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_EXECUTED, + new ExecutedEvent($config, $log) + ); + } + + /** + * @param AbstractEventAccessor $config + * @param LeadEventLog $log + */ + private function dispatchFailedEvent(AbstractEventAccessor $config, LeadEventLog $log) + { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_FAILED, + new FailedEvent($config, $log) + ); + } + + /** + * @param $result + * + * @return bool + */ + private function isFailed($result) + { + return + false === $result + || (is_array($result) && isset($result['result']) && false === $result['result']) + ; + } + + /** + * @param $result + * @param LeadEventLog $log + */ + private function processFailedLog($result, LeadEventLog $log) + { + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($log->getEvent()->getEventType()).' ID# '.$log->getEvent()->getId().' for contact ID# '.$log->getLead()->getId() + ); + + if (is_array($result)) { + $log->setMetadata($result); + } + + $metadata = $log->getMetadata(); + if (is_array($result)) { + $metadata = array_merge($metadata, $result); + } + + $reason = null; + if (isset($metadata['errors'])) { + $reason = (is_array($metadata['errors'])) ? implode('
', $metadata['errors']) : $metadata['errors']; + } elseif (isset($metadata['reason'])) { + $reason = $metadata['reason']; + } + + if (!$failedLog = $log->getFailedLog()) { + $failedLog = new FailedLeadEventLog(); + } + + $failedLog->setLog($log) + ->setDateAdded(new \DateTime()) + ->setReason($reason); + + $log->setFailedLog($failedLog); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Event/Action.php b/app/bundles/CampaignBundle/Executioner/Event/Action.php new file mode 100644 index 00000000000..83616136ec2 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Event/Action.php @@ -0,0 +1,93 @@ +eventLogger = $eventLogger; + $this->dispatcher = $dispatcher; + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $contacts + * + * @return mixed|void + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\LogPassedAndFailedException + */ + public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + { + // Ensure each contact has a log entry to prevent them from being picked up again prematurely + foreach ($contacts as $contact) { + $log = $this->getLogEntry($event, $contact); + ChannelExtractor::setChannel($log, $event, $config); + + $this->eventLogger->addToQueue($log, false); + } + $this->eventLogger->persistQueued(false); + + // Execute to process the batch of contacts + $this->dispatcher->executeEvent($config, $event, $this->eventLogger->getLogs()); + + // Update log entries or persist failed entries + $this->eventLogger->persist(); + } + + /** + * @param Event $event + * @param Lead $contact + * + * @return \Mautic\CampaignBundle\Entity\LeadEventLog + */ + private function getLogEntry(Event $event, Lead $contact) + { + // Create the entry + $log = $this->eventLogger->buildLogEntry($event, $contact); + + $log->setIsScheduled(false); + $log->setDateTriggered(new \DateTime()); + + $this->eventLogger->persistLog($log); + + return $log; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Event/Condition.php b/app/bundles/CampaignBundle/Executioner/Event/Condition.php new file mode 100644 index 00000000000..d3f3020e650 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Event/Condition.php @@ -0,0 +1,55 @@ +eventLogger = $eventLogger; + $this->dispatcher = $dispatcher; + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $contacts + * + * @return mixed|void + */ + public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + { + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Event/Decision.php b/app/bundles/CampaignBundle/Executioner/Event/Decision.php new file mode 100644 index 00000000000..722f0c63178 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Event/Decision.php @@ -0,0 +1,55 @@ +eventLogger = $eventLogger; + $this->dispatcher = $dispatcher; + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $contacts + * + * @return mixed|void + */ + public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + { + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php new file mode 100644 index 00000000000..c13c1ea1c2a --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php @@ -0,0 +1,28 @@ +actionExecutioner = $actionExecutioner; + $this->conditionExecutioner = $conditionExecutioner; + $this->decisionExecutioner = $decisionExecutioner; + $this->collector = $eventCollector; + $this->logger = $logger; + } + + /** + * @param Event $event + * @param ArrayCollection $contacts + * + * @throws Dispatcher\LogNotProcessedException + * @throws Dispatcher\LogPassedAndFailedException + */ + public function execute(Event $event, ArrayCollection $contacts) + { + $this->logger->debug('CAMPAIGN: Executing event ID '.$event->getId()); + + $config = $this->collector->getEventConfig($event); + + switch ($event->getEventType()) { + case Event::TYPE_ACTION: + $this->actionExecutioner->execute($config, $event, $contacts); + break; + case Event::TYPE_CONDITION: + $this->conditionExecutioner->execute($config, $event, $contacts); + break; + case Event::TYPE_DECISION: + $this->decisionExecutioner->execute($config, $event, $contacts); + break; + default: + throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); + } + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php b/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php new file mode 100644 index 00000000000..58f974bdcbb --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php @@ -0,0 +1,16 @@ +logger = $logger; + $this->kickoffContacts = $kickoffContacts; + $this->translator = $translator; + $this->executioner = $executioner; + $this->scheduler = $scheduler; + } + + /** + * @param Campaign $campaign + * @param int $eventLimit + * @param null $maxEventsToExecute + * @param OutputInterface|null $output + * + * @throws NoContactsFound + * @throws NoEventsFound + * @throws NotSchedulableException + */ + public function executeForCampaign(Campaign $campaign, $eventLimit = 100, $maxEventsToExecute = null, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->contactId = null; + $this->eventLimit = $eventLimit; + $this->maxEventsToExecute = $maxEventsToExecute; + $this->output = ($output) ? $output : new NullOutput(); + + $this->prepareForExecution(); + $this->executeOrSchedule(); + } + + /** + * @param Campaign $campaign + * @param $contactId + * @param OutputInterface|null $output + * + * @throws NoContactsFound + * @throws NoEventsFound + * @throws NotSchedulableException + */ + public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->contactId = $contactId; + $this->output = ($output) ? $output : new NullOutput(); + + // Process all events for this contact + $this->eventLimit = null; + $this->maxEventsToExecute = null; + + $this->prepareForExecution(); + $this->executeOrSchedule(); + } + + /** + * @throws NoEventsFound + */ + private function prepareForExecution() + { + $this->logger->debug('CAMPAIGN: Triggering kickoff events'); + + defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); + + $this->batchCounter = 0; + $this->rootEvents = $this->campaign->getRootEvents(); + $totalRootEvents = $this->rootEvents->count(); + $this->logger->debug('CAMPAIGN: Processing the following events: '.implode(', ', $this->rootEvents->getKeys())); + + $totalContacts = $this->kickoffContacts->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->contactId); + $totalKickoffEvents = $totalRootEvents * $totalContacts; + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalKickoffEvents, + '%batch%' => $this->eventLimit, + ] + ) + ); + + if (!$totalKickoffEvents) { + $this->logger->debug('CAMPAIGN: No contacts/events to process'); + + throw new NoEventsFound(); + } + + if (!$this->maxEventsToExecute) { + $this->maxEventsToExecute = $totalKickoffEvents; + } + + $this->progressBar = ProgressBarHelper::init($this->output, $this->maxEventsToExecute); + $this->progressBar->start(); + } + + /** + * @throws Dispatcher\LogNotProcessedException + * @throws Dispatcher\LogPassedAndFailedException + * @throws NotSchedulableException + */ + private function executeOrSchedule() + { + // Use the same timestamp across all contacts processed + $now = new \DateTime(); + + // Loop over contacts until the entire campaign is executed + try { + while ($contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->eventLimit, $this->contactId)) { + /** @var Event $event */ + foreach ($this->rootEvents as $event) { + // Check if the event should be scheduled (let the scheduler's do the debug logging) + $executionDate = $this->scheduler->getExecutionDateTime($event, $now); + if ($executionDate > $now) { + $this->scheduler->schedule($event, $executionDate, $contacts); + continue; + } + + // Execute the event for the batch of contacts + $this->executioner->execute($event, $contacts); + } + + $this->kickoffContacts->clear(); + } + } catch (NoContactsFound $exception) { + // We're done here + } + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php new file mode 100644 index 00000000000..bf9613cdf27 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -0,0 +1,155 @@ +ipLookupHelper = $ipLookupHelper; + $this->leadModel = $leadModel; + $this->repo = $repo; + + $this->queued = new ArrayCollection(); + $this->processed = new ArrayCollection(); + } + + /** + * @param LeadEventLog $log + */ + public function addToQueue(LeadEventLog $log, $clearPostPersist = true) + { + $this->queued->add($log); + + if ($this->queued->count() >= 20) { + $this->persistQueued($clearPostPersist); + } + } + + /** + * @param LeadEventLog $log + */ + public function persistLog(LeadEventLog $log) + { + $this->repo->saveEntity($log); + } + + /** + * @param Event $event + * @param null $lead + * @param bool $systemTriggered + * + * @return LeadEventLog + */ + public function buildLogEntry(Event $event, $lead = null, $systemTriggered = false) + { + $log = new LeadEventLog(); + + $log->setIpAddress($this->ipLookupHelper->getIpAddress()); + + $log->setEvent($event); + + if ($lead == null) { + $lead = $this->leadModel->getCurrentLead(); + } + $log->setLead($lead); + + $log->setDateTriggered(new \DateTime()); + $log->setSystemTriggered($systemTriggered); + + return $log; + } + + /** + * Persist the queue, clear the entities from memory, and reset the queue. + */ + public function persistQueued($clearPostPersist = true) + { + if ($this->queued) { + $this->repo->saveEntities($this->queued->getValues()); + + if ($clearPostPersist) { + $this->repo->clear(); + } + } + + if (!$clearPostPersist) { + // The logs are needed so don't clear + foreach ($this->queued as $log) { + $this->processed->add($log); + } + } + + $this->queued->clear(); + } + + /** + * @return ArrayCollection + */ + public function getLogs() + { + return $this->processed; + } + + /** + * Persist processed entities after they've been updated. + */ + public function persist() + { + if (!$this->processed) { + return; + } + + $this->repo->saveEntities($this->processed->getValues()); + $this->repo->clear(); + $this->processed->clear(); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php new file mode 100644 index 00000000000..54d0a31a71c --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -0,0 +1,16 @@ +logger = $logger; + $this->dispatcher = $dispatcher; + $this->eventLogger = $eventLogger; + $this->intervalScheduler = $intervalScheduler; + $this->dateTimeScheduler = $dateTimeScheduler; + $this->collector = $collector; + $this->coreParametersHelper = $coreParametersHelper; + } + + /** + * @param Event $event + * @param ArrayCollection $contacts + */ + public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts) + { + $config = $this->collector->getEventConfig($event); + + foreach ($contacts as $contact) { + // Create the entry + $log = $this->eventLogger->buildLogEntry($event, $contact); + + // Schedule it + $log->setTriggerDate($executionDate); + + // Add it to the queue to persist to the DB + $this->eventLogger->addToQueue($log); + + //lead actively triggered this event, a decision wasn't involved, or it was system triggered and a "no" path so schedule the event to be fired at the defined time + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($event->getEventType()).' ID# '.$event->getId().' for contact ID# '.$contact->getId() + .' has timing that is not appropriate and thus scheduled for '.$executionDate->format('Y-m-d H:m:i T') + ); + + $this->dispatchScheduledEvent($config, $log); + } + + // Persist any pending in the queue + $this->eventLogger->persistQueued(); + } + + /** + * @param LeadEventLog $log + */ + public function reschedule(LeadEventLog $log, \DateTime $toBeExecutedOn) + { + $log->setTriggerDate($toBeExecutedOn); + + $event = $log->getEvent(); + $config = $this->collector->getEventConfig($event); + + $this->dispatchScheduledEvent($config, $log); + } + + /** + * @param LeadEventLog $log + */ + public function rescheduleFailure(LeadEventLog $log) + { + if ($interval = $this->coreParametersHelper->getParameter('campaign_time_wait_on_event_false')) { + try { + $date = new \DateTime(); + $date->add(new \DateInterval($interval)); + } catch (\Exception $exception) { + // Bad interval + return; + } + + $this->reschedule($log, $date); + } + } + + /** + * @param Event $event + * @param \DateTime $now + * + * @return \DateTime + * + * @throws NotSchedulableException + */ + public function getExecutionDateTime(Event $event, \DateTime $now = null) + { + if (null === $now) { + $now = new \DateTime(); + } + + switch ($event->getTriggerMode()) { + case Event::TRIGGER_MODE_IMMEDIATE: + return $now; + case Event::TRIGGER_MODE_INTERVAL: + return $this->intervalScheduler->getExecutionDateTime($event, $now, $now); + case Event::TRIGGER_MODE_DATE: + return $this->dateTimeScheduler->getExecutionDateTime($event, $now, $now); + } + + throw new NotSchedulableException(); + } + + /** + * @param AbstractEventAccessor $config + * @param LeadEventLog $log + */ + private function dispatchScheduledEvent(AbstractEventAccessor $config, LeadEventLog $log) + { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_SCHEDULED, + new ScheduledEvent($config, $log) + ); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Exception/ExecutionProhibitedException.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Exception/ExecutionProhibitedException.php new file mode 100644 index 00000000000..ec294aec8f9 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Exception/ExecutionProhibitedException.php @@ -0,0 +1,16 @@ +logger = $logger; + } + + /** + * @param Event $event + * @param \DateTime $now + * @param \DateTime $comparedToDateTime + * + * @return \DateTime|mixed + */ + public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime) + { + $triggerDate = $event->getTriggerDate(); + + if (null === $triggerDate) { + $this->logger->debug('CAMPAIGN: Trigger date is null'); + + return $now; + } + + if ($now >= $triggerDate) { + $this->logger->debug( + 'CAMPAIGN: Date to execute ('.$triggerDate->format('Y-m-d H:i:s T').') compared to now (' + .$now->format('Y-m-d H:i:s T').') and is thus overdue' + ); + + return $now; + } + + return $triggerDate; + + /* + if ($negate) { + $this->logger->debug( + 'CAMPAIGN: Negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' + .$now->format('Y-m-d H:i:s T').') and is thus '.(($pastDue) ? 'overdue' : 'not past due') + ); + + //it is past the scheduled trigger date and the lead has done nothing so return true to trigger + //the event otherwise false to do nothing + $return = ($pastDue) ? true : $action['triggerDate']; + + // Save some RAM for batch processing + unset($now, $action); + + return $return; + } elseif (!$pastDue) { + $this->logger->debug( + 'CAMPAIGN: Non-negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' + .$now->format('Y-m-d H:i:s T').') and is thus not past due' + ); + + //schedule the event + return $action['triggerDate']; + } + * */ + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php new file mode 100644 index 00000000000..8df143e50bb --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -0,0 +1,71 @@ +logger = $logger; + } + + /** + * @param Event $event + * @param \DateTime|null $comparedToDateTime + * + * @return \Datetime + * + * @throws NotSchedulableException + */ + public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime) + { + // $triggerOn = $negate ? clone $parentTriggeredDate : new \DateTime(); + + $interval = $event->getTriggerInterval(); + $unit = $event->getTriggerIntervalUnit(); + + $this->logger->debug('CAMPAIGN: Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format('Y-m-d H:i:s T')); + + try { + $comparedToDateTime->add((new DateTimeHelper())->buildInterval($interval, $unit)); + } catch (\Exception $exception) { + $this->logger->error('CAMPAIGN: Determining interval scheduled failed with "'.$exception->getMessage().'"'); + + throw new NotSchedulableException(); + } + + if ($comparedToDateTime > $now) { + $this->logger->debug("CAMPAIGN: Interval of $interval $unit to execute (".$comparedToDateTime->format('Y-m-d H:i:s T').') is later than now ('.$now->format('Y-m-d H:i:s T')); + + //the event is to be scheduled based on the time interval + return $comparedToDateTime; + } + + return $now; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php new file mode 100644 index 00000000000..76238dc3c0d --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php @@ -0,0 +1,26 @@ +getChannel()) { + return; + } + + if (!$channel = $eventConfig->getChannel()) { + return; + } + + $entity->setChannel($channel); + + if (!$channelIdField = $eventConfig->getChannelIdField()) { + return; + } + + $properties = $event->getProperties(); + if (!empty($properties['properties'][$channelIdField])) { + if (is_array($properties['properties'][$channelIdField])) { + if (count($properties['properties'][$channelIdField]) === 1) { + // Only store channel ID if a single item was selected + $entity->setChannelId($properties['properties'][$channelIdField]); + } + + return; + } + + $entity->setChannelId($properties['properties'][$channelIdField]); + } + } +} diff --git a/app/bundles/CampaignBundle/Model/CampaignModel.php b/app/bundles/CampaignBundle/Model/CampaignModel.php index 406b7cc9eaf..ec97cf3a8cc 100644 --- a/app/bundles/CampaignBundle/Model/CampaignModel.php +++ b/app/bundles/CampaignBundle/Model/CampaignModel.php @@ -17,6 +17,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\Lead as CampaignLead; use Mautic\CampaignBundle\Event as Events; +use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\CoreParametersHelper; @@ -26,7 +27,6 @@ use Mautic\FormBundle\Entity\Form; use Mautic\FormBundle\Model\FormModel; use Mautic\LeadBundle\Entity\Lead; -use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Model\LeadModel; use Mautic\LeadBundle\Model\ListModel; use Symfony\Component\Console\Output\OutputInterface; @@ -64,9 +64,9 @@ class CampaignModel extends CommonFormModel protected $formModel; /** - * @var + * @var EventCollector */ - protected static $events; + private $eventCollector; /** * @var array @@ -81,13 +81,19 @@ class CampaignModel extends CommonFormModel * @param ListModel $leadListModel * @param FormModel $formModel */ - public function __construct(CoreParametersHelper $coreParametersHelper, LeadModel $leadModel, ListModel $leadListModel, FormModel $formModel) - { + public function __construct( + CoreParametersHelper $coreParametersHelper, + LeadModel $leadModel, + ListModel $leadListModel, + FormModel $formModel, + EventCollector $eventCollector + ) { $this->leadModel = $leadModel; $this->leadListModel = $leadListModel; $this->formModel = $formModel; $this->batchSleepTime = $coreParametersHelper->getParameter('mautic.batch_sleep_time'); $this->batchCampaignSleepTime = $coreParametersHelper->getParameter('mautic.batch_campaign_sleep_time'); + $this->eventCollector = $eventCollector; } /** @@ -363,15 +369,18 @@ public function setEvents(Campaign $entity, $sessionEvents, $sessionConnections, //set event order used when querying the events $this->buildOrder($hierarchy, $events, $entity); - uasort($events, function ($a, $b) { - $aOrder = $a->getOrder(); - $bOrder = $b->getOrder(); - if ($aOrder == $bOrder) { - return 0; - } + uasort( + $events, + function ($a, $b) { + $aOrder = $a->getOrder(); + $bOrder = $b->getOrder(); + if ($aOrder == $bOrder) { + return 0; + } - return ($aOrder < $bOrder) ? -1 : 1; - }); + return ($aOrder < $bOrder) ? -1 : 1; + } + ); // Persist events if campaign is being edited if ($entity->getId()) { @@ -381,37 +390,6 @@ public function setEvents(Campaign $entity, $sessionEvents, $sessionConnections, return $events; } - /** - * @param $entity - * @param $properties - * @param $eventSettings - * - * @return bool - */ - public function setChannelFromEventProperties($entity, $properties, &$eventSettings) - { - $channelSet = false; - if (!$entity->getChannel() && !empty($eventSettings[$properties['type']]['channel'])) { - $entity->setChannel($eventSettings[$properties['type']]['channel']); - if (isset($eventSettings[$properties['type']]['channelIdField'])) { - $channelIdField = $eventSettings[$properties['type']]['channelIdField']; - if (!empty($properties['properties'][$channelIdField])) { - if (is_array($properties['properties'][$channelIdField])) { - if (count($properties['properties'][$channelIdField]) === 1) { - // Only store channel ID if a single item was selected - $entity->setChannelId($properties['properties'][$channelIdField]); - } - } else { - $entity->setChannelId($properties['properties'][$channelIdField]); - } - } - } - $channelSet = true; - } - - return $channelSet; - } - /** * @param $entity * @param $settings @@ -485,101 +463,6 @@ public function setCanvasSettings($entity, $settings, $persist = true, $events = return $settings; } - /** - * Gets array of custom events from bundles subscribed CampaignEvents::CAMPAIGN_ON_BUILD. - * - * @param string|null $type Specific type of events to retreive - * - * @return mixed - */ - public function getEvents($type = null) - { - if (null === self::$events) { - self::$events = []; - - //build them - $events = []; - $event = new Events\CampaignBuilderEvent($this->translator); - $this->dispatcher->dispatch(CampaignEvents::CAMPAIGN_ON_BUILD, $event); - - $events['decision'] = $event->getDecisions(); - $events['condition'] = $event->getConditions(); - $events['action'] = $event->getActions(); - - $connectionRestrictions = ['anchor' => []]; - - $eventTypes = array_fill_keys(array_keys($events), []); - foreach ($events as $eventType => $typeEvents) { - foreach ($typeEvents as $key => $event) { - if (!isset($connectionRestrictions[$key])) { - $connectionRestrictions[$key] = [ - 'source' => $eventTypes, - 'target' => $eventTypes, - ]; - } - if (!isset($connectionRestrictions[$key])) { - $connectionRestrictions['anchor'][$key] = []; - } - - // @deprecated 2.6.0 to be removed in 3.0 - switch ($eventType) { - case 'decision': - if (isset($event['associatedActions'])) { - $connectionRestrictions[$key]['target']['action'] += $event['associatedActions']; - } - break; - case 'action': - if (isset($event['associatedDecisions'])) { - $connectionRestrictions[$key]['source']['decision'] += $event['associatedDecisions']; - } - break; - } - - if (isset($event['anchorRestrictions'])) { - foreach ($event['anchorRestrictions'] as $restriction) { - list($group, $anchor) = explode('.', $restriction); - $connectionRestrictions['anchor'][$key][$group][] = $anchor; - } - } - // end deprecation - - if (isset($event['connectionRestrictions'])) { - foreach ($event['connectionRestrictions'] as $restrictionType => $restrictions) { - switch ($restrictionType) { - case 'source': - case 'target': - foreach ($restrictions as $groupType => $groupRestrictions) { - $connectionRestrictions[$key][$restrictionType][$groupType] += $groupRestrictions; - } - break; - case 'anchor': - foreach ($restrictions as $anchor) { - list($group, $anchor) = explode('.', $anchor); - $connectionRestrictions[$restrictionType][$group][$key][] = $anchor; - } - - break; - } - } - } - } - } - - $events['connectionRestrictions'] = $connectionRestrictions; - self::$events = $events; - } - - if (null !== $type) { - if (!isset(self::$events[$type])) { - throw new \InvalidArgumentException("$type not found as array key"); - } - - return self::$events[$type]; - } - - return self::$events; - } - /** * Get list of sources for a campaign. * @@ -782,15 +665,20 @@ public function addLeads(Campaign $campaign, array $leads, $manuallyAdded = fals if ($searchListLead == -1) { $campaignLead = null; } elseif ($searchListLead) { - $campaignLead = $this->getCampaignLeadRepository()->findOneBy([ - 'lead' => $lead, - 'campaign' => $campaign, - ]); + $campaignLead = $this->getCampaignLeadRepository()->findOneBy( + [ + 'lead' => $lead, + 'campaign' => $campaign, + ] + ); } else { - $campaignLead = $this->em->getReference('MauticCampaignBundle:Lead', [ - 'lead' => $leadId, - 'campaign' => $campaign->getId(), - ]); + $campaignLead = $this->em->getReference( + 'MauticCampaignBundle:Lead', + [ + 'lead' => $leadId, + 'campaign' => $campaign->getId(), + ] + ); } $dispatchEvent = true; @@ -895,15 +783,22 @@ public function removeLeads(Campaign $campaign, array $leads, $manuallyRemoved = } $this->removedLeads[$campaign->getId()][$leadId] = $leadId; - $campaignLead = (!$skipFindOne) ? - $this->getCampaignLeadRepository()->findOneBy([ - 'lead' => $lead, - 'campaign' => $campaign, - ]) : - $this->em->getReference('MauticCampaignBundle:Lead', [ - 'lead' => $leadId, - 'campaign' => $campaign->getId(), - ]); + $campaignLead = (!$skipFindOne) + ? + $this->getCampaignLeadRepository()->findOneBy( + [ + 'lead' => $lead, + 'campaign' => $campaign, + ] + ) + : + $this->em->getReference( + 'MauticCampaignBundle:Lead', + [ + 'lead' => $leadId, + 'campaign' => $campaign->getId(), + ] + ); if ($campaignLead == null) { if ($batchProcess) { @@ -1111,7 +1006,7 @@ public function rebuildCampaignLeads(Campaign $campaign, $limit = 1000, $maxLead ); // Restart batching - $start = $lastRoundPercentage = 0; + $start = $lastRoundPercentage = 0; $leadCount = $removeLeadCount['count']; $batchLimiters['maxId'] = $removeLeadCount['maxId']; @@ -1264,8 +1159,8 @@ public function getLeadsAddedLineChartData($unit, \DateTime $dateFrom, \DateTime if (!$canViewOthers) { $q->join('t', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = c.campaign_id') - ->andWhere('c.created_by = :userId') - ->setParameter('userId', $this->userHelper->getUser()->getId()); + ->andWhere('c.created_by = :userId') + ->setParameter('userId', $this->userHelper->getUser()->getId()); } $data = $query->loadAndBuildTimeData($q); @@ -1364,4 +1259,51 @@ protected function buildOrder($hierarchy, &$events, $entity, $root = 'null', $or } } } + + /** + * @deprecated 2.13.0 to be removed in 3.0; use EventCollector instead + * + * Gets array of custom events from bundles subscribed CampaignEvents::CAMPAIGN_ON_BUILD. + * + * @param string|null $type Specific type of events to retreive + * + * @return mixed + */ + public function getEvents($type = null) + { + return $this->eventCollector->getEventsArray($type); + } + + /** + * @deprecated 2.13.0 to be removed in 3.0; use \Mautic\CampaignBundle\Helper\ChannelExtractor instead + * + * @param $entity + * @param $properties + * @param $eventSettings + * + * @return bool + */ + public function setChannelFromEventProperties($entity, $properties, &$eventSettings) + { + $channelSet = false; + if (!$entity->getChannel() && !empty($eventSettings[$properties['type']]['channel'])) { + $entity->setChannel($eventSettings[$properties['type']]['channel']); + if (isset($eventSettings[$properties['type']]['channelIdField'])) { + $channelIdField = $eventSettings[$properties['type']]['channelIdField']; + if (!empty($properties['properties'][$channelIdField])) { + if (is_array($properties['properties'][$channelIdField])) { + if (count($properties['properties'][$channelIdField]) === 1) { + // Only store channel ID if a single item was selected + $entity->setChannelId($properties['properties'][$channelIdField]); + } + } else { + $entity->setChannelId($properties['properties'][$channelIdField]); + } + } + } + $channelSet = true; + } + + return $channelSet; + } } diff --git a/app/bundles/CoreBundle/Helper/DateTimeHelper.php b/app/bundles/CoreBundle/Helper/DateTimeHelper.php index 11ce4c3fa4b..8ecba85cc69 100644 --- a/app/bundles/CoreBundle/Helper/DateTimeHelper.php +++ b/app/bundles/CoreBundle/Helper/DateTimeHelper.php @@ -285,10 +285,12 @@ public function sub($intervalString, $clone = false) /** * Returns interval based on $interval number and $unit. * - * @param int $interval - * @param string $unit + * @param $interval + * @param $unit + * + * @return \DateInterval * - * @return DateInterval + * @throws \Exception */ public function buildInterval($interval, $unit) { From a3c11b1138e8213148971b74175c1e4f2c0d5537 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 29 Jan 2018 01:01:47 -0600 Subject: [PATCH 427/778] Crude support for sending email in batches for campaigns --- app/bundles/EmailBundle/EmailEvents.php | 19 +++- .../EmailBundle/Entity/StatRepository.php | 36 ++++-- .../EventListener/CampaignSubscriber.php | 106 ++++++++++-------- .../LeadBundle/Entity/LeadRepository.php | 29 +++++ 4 files changed, 129 insertions(+), 61 deletions(-) diff --git a/app/bundles/EmailBundle/EmailEvents.php b/app/bundles/EmailBundle/EmailEvents.php index 5304da5ee61..cea43e8aaf8 100644 --- a/app/bundles/EmailBundle/EmailEvents.php +++ b/app/bundles/EmailBundle/EmailEvents.php @@ -156,14 +156,13 @@ final class EmailEvents const EMAIL_RESEND = 'mautic.on_email_resend'; /** - * The mautic.email.on_campaign_trigger_action event is fired when the campaign action triggers. + * The mautic.email.on_campaign_batch_action event is dispatched when the campaign action triggers. * - * The event listener receives a - * Mautic\CampaignBundle\Event\CampaignExecutionEvent + * The event listener receives a Mautic\CampaignBundle\Event\PendingEvent * * @var string */ - const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.email.on_campaign_trigger_action'; + const ON_CAMPAIGN_BATCH_ACTION = 'mautic.email.on_campaign_batch_action'; /** * The mautic.email.on_campaign_trigger_decision event is fired when the campaign action triggers. @@ -213,4 +212,16 @@ final class EmailEvents * @var string */ const ON_SENT_EMAIL_TO_USER = 'mautic.email.on_sent_email_to_user'; + + /** + * @deprecated 2.13.0; to be removed in 3.0. Listen to ON_CAMPAIGN_BATCH_ACTION instead. + * + * The mautic.email.on_campaign_trigger_action event is fired when the campaign action triggers. + * + * The event listener receives a + * Mautic\CampaignBundle\Event\CampaignExecutionEvent + * + * @var string + */ + const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.email.on_campaign_trigger_action'; } diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index 70266f0dd6f..b9327b1dd08 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -11,6 +11,7 @@ namespace Mautic\EmailBundle\Entity; +use Doctrine\DBAL\Connection; use Mautic\CoreBundle\Entity\CommonRepository; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\DateTimeHelper; @@ -540,24 +541,43 @@ public function findContactEmailStats($leadId, $emailId) } /** - * @param $contacts - * @param $emailId + * @param $contacts + * @param $emailId + * @param bool $organizeByContact * - * @return mixed + * @return array|mixed|string */ - public function checkContactsSentEmail($contacts, $emailId) + public function checkContactsSentEmail($contacts, $emailId, $organizeByContact = false) { + if (is_array($contacts)) { + $contacts = implode(',', $contacts); + } + $query = $this->getEntityManager()->getConnection()->createQueryBuilder(); $query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's'); $query->select('id, lead_id') - ->where('s.email_id = :email') - ->andWhere('s.lead_id in (:contacts)') + ->where('s.email_id = :email') + ->andWhere('s.lead_id in (:contacts)') ->andWhere('is_failed = 0') - ->setParameter(':email', $emailId) - ->setParameter(':contacts', $contacts); + ->setParameter(':email', $emailId) + ->setParameter(':contacts', $contacts, Connection::PARAM_INT_ARRAY) + ->groupBy('lead_id'); $results = $query->execute()->fetch(); + if ($organizeByContact) { + $contacts = []; + foreach ($results as $result) { + if (!isset($contacts[$result['lead_id']])) { + $contacts[$result['lead_id']] = []; + } + + $contacts[$result['lead_id']][] = $result['id']; + } + + return $contacts; + } + return $results; } } diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 10f7a9638ff..99c33b6c246 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -12,8 +12,10 @@ namespace Mautic\EmailBundle\EventListener; use Mautic\CampaignBundle\CampaignEvents; +use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Event\CampaignBuilderEvent; use Mautic\CampaignBundle\Event\CampaignExecutionEvent; +use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\Model\EventModel; use Mautic\ChannelBundle\Model\MessageQueueModel; use Mautic\CoreBundle\EventListener\CommonSubscriber; @@ -87,7 +89,7 @@ public static function getSubscribedEvents() return [ CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0], EmailEvents::EMAIL_ON_OPEN => ['onEmailOpen', 0], - EmailEvents::ON_CAMPAIGN_TRIGGER_ACTION => [ + EmailEvents::ON_CAMPAIGN_BATCH_ACTION => [ ['onCampaignTriggerActionSendEmailToContact', 0], ['onCampaignTriggerActionSendEmailToUser', 1], ], @@ -137,14 +139,14 @@ public function onCampaignBuild(CampaignBuilderEvent $event) $event->addAction( 'email.send', [ - 'label' => 'mautic.email.campaign.event.send', - 'description' => 'mautic.email.campaign.event.send_descr', - 'eventName' => EmailEvents::ON_CAMPAIGN_TRIGGER_ACTION, - 'formType' => 'emailsend_list', - 'formTypeOptions' => ['update_select' => 'campaignevent_properties_email', 'with_email_types' => true], - 'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList', - 'channel' => 'email', - 'channelIdField' => 'email', + 'label' => 'mautic.email.campaign.event.send', + 'description' => 'mautic.email.campaign.event.send_descr', + 'batchEventName' => EmailEvents::ON_CAMPAIGN_BATCH_ACTION, + 'formType' => 'emailsend_list', + 'formTypeOptions' => ['update_select' => 'campaignevent_properties_email', 'with_email_types' => true], + 'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList', + 'channel' => 'email', + 'channelIdField' => 'email', ] ); @@ -253,34 +255,29 @@ public function onCampaignTriggerDecision(CampaignExecutionEvent $event) /** * Triggers the action which sends email to contact. * - * @param CampaignExecutionEvent $event + * @param PendingEvent $event * - * @return CampaignExecutionEvent|null + * @return PendingEvent|null */ - public function onCampaignTriggerActionSendEmailToContact(CampaignExecutionEvent $event) + public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) { if (!$event->checkContext('email.send')) { return; } - $leadCredentials = $event->getLeadFields(); - - if (empty($leadCredentials['email'])) { - return $event->setFailed('Contact does not have an email'); - } - - $config = $event->getConfig(); + $config = $event->getEvent()->getProperties(); $emailId = (int) $config['email']; $email = $this->emailModel->getEntity($emailId); if (!$email || !$email->isPublished()) { - return $event->setFailed('Email not found or published'); + return $event->failAll('Email not found or published'); } - $emailSent = false; - $type = (isset($config['email_type'])) ? $config['email_type'] : 'transactional'; - $options = [ - 'source' => ['campaign.event', $event->getEvent()['id']], + $event->setChannel('email', $emailId); + + $type = (isset($config['email_type'])) ? $config['email_type'] : 'transactional'; + $options = [ + 'source' => ['campaign.event', $event->getEvent()->getId()], 'email_attempts' => (isset($config['attempts'])) ? $config['attempts'] : 3, 'email_priority' => (isset($config['priority'])) ? $config['priority'] : 2, 'email_type' => $type, @@ -288,37 +285,48 @@ public function onCampaignTriggerActionSendEmailToContact(CampaignExecutionEvent 'dnc_as_error' => true, ]; - $event->setChannel('email', $emailId); - // Determine if this email is transactional/marketing - $stats = []; + $contacts = []; + $logAssignments = []; + $pending = $event->getPending(); + /** @var LeadEventLog $log */ + foreach ($pending as $id => $log) { + $lead = $log->getLead(); + $leadCredentials = $lead->getProfileFields(); + if (empty($leadCredentials['email'])) { + $event->fail($log, $lead->getPrimaryIdentifier().' does not have an email'); + continue; + } + + $contacts[$id] = $leadCredentials; + $logAssignments[$lead->getId()] = $id; + } + if ('marketing' == $type) { // Determine if this lead has received the email before - $leadIds = implode(',', [$leadCredentials['id']]); - $stats = $this->emailModel->getStatRepository()->checkContactsSentEmail($leadIds, $emailId); - $emailSent = true; // Assume it was sent to prevent the campaign event from getting rescheduled over and over - } + $stats = $this->emailModel->getStatRepository()->checkContactsSentEmail(array_keys($logAssignments), $emailId, true); - if (empty($stats)) { - $emailSent = $this->emailModel->sendEmail($email, $leadCredentials, $options); + foreach ($stats as $contactId => $sent) { + /** @var LeadEventLog $log */ + $log = $pending->get($id); + $event->fail($log, 'Already received email'); + unset($contacts[$id]); + } } - if (is_array($emailSent)) { - $errors = implode('
', $emailSent); - - // Add to the metadata of the failed event - $emailSent = [ - 'result' => false, - 'errors' => $errors, - ]; - } elseif (true !== $emailSent) { - $emailSent = [ - 'result' => false, - 'errors' => $emailSent, - ]; - } + if (count($contacts)) { + $errors = $this->emailModel->sendEmail($email, $contacts, $options); - return $event->setResult($emailSent); + if (empty($errors)) { + foreach ($contacts as $logId => $contactId) { + $event->pass($pending->get($logId)); + } + } else { + foreach ($errors as $failedContactId => $reason) { + $event->fail($pending->get($logAssignments[$failedContactId]), $reason); + } + } + } } /** @@ -328,7 +336,7 @@ public function onCampaignTriggerActionSendEmailToContact(CampaignExecutionEvent * * @return CampaignExecutionEvent|null */ - public function onCampaignTriggerActionSendEmailToUser(CampaignExecutionEvent $event) + public function onCampaignTriggerActionSendEmailToUser(PendingEvent $event) { if (!$event->checkContext('email.send.to.user')) { return; diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index b678aec4ca5..fa77b58bce7 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -11,6 +11,7 @@ namespace Mautic\LeadBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Query\QueryBuilder; use Mautic\CoreBundle\Entity\CommonRepository; @@ -1067,6 +1068,34 @@ public function getContacts(array $contactIds) return []; } + /** + * @param array $ids + * + * @return ArrayCollection + */ + public function getContactCollection(array $ids) + { + $contacts = $this->getEntities( + [ + 'filter' => [ + 'force' => [ + [ + 'column' => 'l.id', + 'expr' => 'in', + 'value' => $ids, + ], + ], + ], + 'orderBy' => 'l.id', + 'orderByDir' => 'asc', + 'withPrimaryCompany' => true, + 'withChannelRules' => true, + ] + ); + + return new ArrayCollection($contacts); + } + /** * @return string */ From 5be625f48a1b8e38f4b1b3567d1029f59ab7c4db Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 29 Jan 2018 23:23:30 -0600 Subject: [PATCH 428/778] Execute scheduled actions --- app/bundles/CampaignBundle/CampaignEvents.php | 9 + .../Command/TriggerCampaignCommand.php | 13 +- app/bundles/CampaignBundle/Config/config.php | 17 ++ .../CampaignBundle/Entity/Campaign.php | 13 +- .../CampaignBundle/Entity/EventRepository.php | 112 +++---- .../Entity/LeadEventLogRepository.php | 89 +++++- .../Event/AbstractLogCollectionEvent.php | 127 ++++++++ .../CampaignBundle/Event/PendingEvent.php | 75 ++--- .../Event/ScheduledBatchEvent.php | 25 ++ .../CampaignBundle/Event/ScheduledEvent.php | 12 +- .../ContactFinder/ScheduledContacts.php | 64 ++++ .../Dispatcher/EventDispatcher.php | 1 + .../Exception/LogPassedAndFailedException.php | 2 +- .../Executioner/Event/Action.php | 27 +- .../Executioner/Event/Condition.php | 6 +- .../Executioner/Event/Decision.php | 6 +- .../Executioner/Event/EventInterface.php | 2 +- .../Executioner/EventExecutioner.php | 52 +++- .../Executioner/ExecutionerInterface.php | 36 +++ .../Executioner/KickoffExecutioner.php | 132 +++++---- .../Executioner/Logger/EventLogger.php | 36 ++- .../Executioner/ScheduledExecutioner.php | 275 +++++++++++++++++- .../Executioner/Scheduler/EventScheduler.php | 33 ++- .../Executioner/Scheduler/Mode/Interval.php | 1 - .../Translations/en_US/messages.ini | 2 +- .../IpLookup/MaxmindDownloadLookup.php | 3 - .../CoreBundle/Model/AbstractCommonModel.php | 4 +- app/bundles/EmailBundle/Config/config.php | 3 +- .../EventListener/CampaignSubscriber.php | 78 +++-- app/bundles/EmailBundle/Helper/MailHelper.php | 5 +- .../Translations/en_US/messages.ini | 2 + 31 files changed, 1014 insertions(+), 248 deletions(-) create mode 100644 app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php create mode 100644 app/bundles/CampaignBundle/Event/ScheduledBatchEvent.php create mode 100644 app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php create mode 100644 app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php diff --git a/app/bundles/CampaignBundle/CampaignEvents.php b/app/bundles/CampaignBundle/CampaignEvents.php index 0a5170e19a4..2ff1819cf08 100644 --- a/app/bundles/CampaignBundle/CampaignEvents.php +++ b/app/bundles/CampaignBundle/CampaignEvents.php @@ -115,6 +115,15 @@ final class CampaignEvents */ const ON_EVENT_SCHEDULED = 'matuic.campaign_on_event_scheduled'; + /** + * The mautic.campaign_on_event_scheduled_batch event is dispatched when a batch of events are scheduled at once. + * + * The event listener receives a Mautic\CampaignBundle\Event\ScheduledBatchEvent instance. + * + * @var string + */ + const ON_EVENT_SCHEDULED_BATCH = 'matuic.campaign_on_event_scheduled_batch'; + /** * The mautic.campaign_on_event_failed event is dispatched when an event fails for whatever reason. * diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index edc80e06d94..35b9f2beb6a 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -14,6 +14,8 @@ use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Event\CampaignTriggerEvent; +use Mautic\CampaignBundle\Executioner\KickoffExecutioner; +use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CoreBundle\Command\ModeratedCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -79,7 +81,12 @@ protected function execute(InputInterface $input, OutputInterface $output) $batch = $input->getOption('batch-limit'); $max = $input->getOption('max-events'); + /** @var KickoffExecutioner $kickoff */ $kickoff = $container->get('mautic.campaign.executioner.kickoff'); + /** @var ScheduledExecutioner $scheduled */ + $scheduled = $container->get('mautic.campaign.executioner.scheduled'); + + defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); if (!$this->checkRunStatus($input, $output, $id)) { return 0; @@ -148,9 +155,10 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$negativeOnly && !$scheduleOnly) { //trigger starting action events for newly added contacts $output->writeln(''.$translator->trans('mautic.campaign.trigger.starting').''); - //$processed = $model->triggerStartingEvents($c, $totalProcessed, $batch, $max, $output); + $kickoff->executeForCampaign($c, $batch, $output); + $output->writeln(''.$translator->trans('mautic.campaign.trigger.scheduled').''); + $scheduled->executeForCampaign($c, $batch, $output); - $kickoff->executeForCampaign($c, $batch, $max, $output); /*$output->writeln( ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' ."\n" @@ -195,7 +203,6 @@ protected function execute(InputInterface $input, OutputInterface $output) unset($campaigns); } - $this->output->writeln('done'); $this->completeRun(); return 0; diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 5464c58493f..399be78b998 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -264,6 +264,12 @@ 'monolog.logger.mautic', ], ], + 'mautic.campaign.contact_finder.scheduled' => [ + 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts::class, + 'arguments' => [ + 'mautic.lead.repository.lead', + ], + ], 'mautic.campaign.event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher::class, 'arguments' => [ @@ -353,6 +359,17 @@ 'mautic.campaign.scheduler', ], ], + 'mautic.campaign.executioner.scheduled' => [ + 'class' => \Mautic\CampaignBundle\Executioner\ScheduledExecutioner::class, + 'arguments' => [ + 'mautic.campaign.repository.lead_event_log', + 'monolog.logger.mautic', + 'translator', + 'mautic.campaign.executioner', + 'mautic.campaign.scheduler', + 'mautic.campaign.contact_finder.scheduled', + ], + ], // @deprecated 2.13.0 for BC support; to be removed in 3.0 'mautic.campaign.legacy_event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher::class, diff --git a/app/bundles/CampaignBundle/Entity/Campaign.php b/app/bundles/CampaignBundle/Entity/Campaign.php index 52f531c95d4..739c6fce0cf 100644 --- a/app/bundles/CampaignBundle/Entity/Campaign.php +++ b/app/bundles/CampaignBundle/Entity/Campaign.php @@ -343,8 +343,19 @@ public function getEvents() public function getRootEvents() { $criteria = Criteria::create()->where(Criteria::expr()->isNull('parent')); + $events = $this->getEvents()->matching($criteria); + + // Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them. + // @see https://github.com/doctrine/doctrine2/issues/4693 + $keyedArrayCollection = new ArrayCollection(); + /** @var Event $event */ + foreach ($events as $event) { + $keyedArrayCollection->set($event->getId(), $event); + } + + unset($events); - return $this->getEvents()->matching($criteria); + return $keyedArrayCollection; } /** diff --git a/app/bundles/CampaignBundle/Entity/EventRepository.php b/app/bundles/CampaignBundle/Entity/EventRepository.php index a8a472e10cb..cf1732612e4 100644 --- a/app/bundles/CampaignBundle/Entity/EventRepository.php +++ b/app/bundles/CampaignBundle/Entity/EventRepository.php @@ -258,61 +258,6 @@ public function getLeadTriggeredEvents($leadId) return $return; } - /** - * Get a list of scheduled events. - * - * @param $campaignId - * @param bool $count - * @param int $limit - * - * @return array|bool - */ - public function getScheduledEvents($campaignId, $count = false, $limit = 0) - { - $date = new \Datetime(); - - $q = $this->getEntityManager()->createQueryBuilder() - ->from('MauticCampaignBundle:LeadEventLog', 'o'); - - $q->where( - $q->expr()->andX( - $q->expr()->eq('IDENTITY(o.campaign)', (int) $campaignId), - $q->expr()->eq('o.isScheduled', ':true'), - $q->expr()->lte('o.triggerDate', ':now') - ) - ) - ->setParameter('now', $date) - ->setParameter('true', true, 'boolean'); - - if ($count) { - $q->select('COUNT(o) as event_count'); - - $results = $results = $q->getQuery()->getArrayResult(); - $count = $results[0]['event_count']; - - return $count; - } - - $q->select('o, IDENTITY(o.lead) as lead_id, IDENTITY(o.event) AS event_id') - ->orderBy('o.triggerDate', 'DESC'); - - if ($limit) { - $q->setFirstResult(0) - ->setMaxResults($limit); - } - - $results = $q->getQuery()->getArrayResult(); - - // Organize by lead - $logs = []; - foreach ($results as $e) { - $logs[$e['lead_id']][$e['event_id']] = array_merge($e[0], ['lead_id' => $e['lead_id'], 'event_id' => $e['event_id']]); - } - unset($results); - - return $logs; - } - /** * @param $campaignId * @@ -603,4 +548,61 @@ protected function addSearchCommandWhereClause($q, $filter) { return $this->addStandardSearchCommandWhereClause($q, $filter); } + + /** + * @deprecated 2.13.0 to be removed in 3.0; use LeadEventLogRepository::getScheduled() instead + * + * Get a list of scheduled events. + * + * @param $campaignId + * @param bool $count + * @param int $limit + * + * @return array|bool + */ + public function getScheduledEvents($campaignId, $count = false, $limit = 0) + { + $date = new \Datetime(); + + $q = $this->getEntityManager()->createQueryBuilder() + ->from('MauticCampaignBundle:LeadEventLog', 'o'); + + $q->where( + $q->expr()->andX( + $q->expr()->eq('IDENTITY(o.campaign)', (int) $campaignId), + $q->expr()->eq('o.isScheduled', ':true'), + $q->expr()->lte('o.triggerDate', ':now') + ) + ) + ->setParameter('now', $date) + ->setParameter('true', true, 'boolean'); + + if ($count) { + $q->select('COUNT(o) as event_count'); + + $results = $results = $q->getQuery()->getArrayResult(); + $count = $results[0]['event_count']; + + return $count; + } + + $q->select('o, IDENTITY(o.lead) as lead_id, IDENTITY(o.event) AS event_id') + ->orderBy('o.triggerDate', 'DESC'); + + if ($limit) { + $q->setFirstResult(0) + ->setMaxResults($limit); + } + + $results = $q->getQuery()->getArrayResult(); + + // Organize by lead + $logs = []; + foreach ($results as $e) { + $logs[$e['lead_id']][$e['event_id']] = array_merge($e[0], ['lead_id' => $e['lead_id'], 'event_id' => $e['event_id']]); + } + unset($results); + + return $logs; + } } diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index b00d1fecada..2ede93f156a 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -11,6 +11,7 @@ namespace Mautic\CampaignBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Mautic\CoreBundle\Entity\CommonRepository; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\LeadBundle\Entity\TimelineTrait; @@ -64,9 +65,6 @@ public function getTableAlias() * @param array $options * * @return array - * - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException */ public function getLeadLogs($leadId = null, array $options = []) { @@ -140,9 +138,6 @@ public function getLeadLogs($leadId = null, array $options = []) * @param array $options * * @return array - * - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException */ public function getUpcomingEvents(array $options = null) { @@ -388,4 +383,86 @@ public function getChartQuery($options) return $chartQuery->fetchTimeData('('.$query.')', 'date_triggered'); } + + /** + * Get a list of scheduled events. + * + * @param $eventId + * @param null $limit + * @param null $contactId + * + * @return ArrayCollection + * + * @throws \Doctrine\ORM\Query\QueryException + */ + public function getScheduled($eventId, $limit = null, $contactId = null) + { + $date = new \Datetime(); + $q = $this->createQueryBuilder('o'); + + $q->select('o, e, c') + ->indexBy('o', 'o.id') + ->innerJoin('o.event', 'e') + ->innerJoin('o.campaign', 'c') + ->where( + $q->expr()->andX( + $q->expr()->eq('IDENTITY(o.event)', ':eventId'), + $q->expr()->eq('o.isScheduled', ':true'), + $q->expr()->lte('o.triggerDate', ':now') + ) + ) + ->setParameter('eventId', (int) $eventId) + ->setParameter('now', $date) + ->setParameter('true', true, 'boolean'); + + if ($contactId) { + $q->andWhere( + $q->expr()->eq('IDENTITY(o.lead)', ':contactId') + ) + ->setParameter('contactId', (int) $contactId); + } + + if ($limit) { + $q->setFirstResult(0) + ->setMaxResults($limit); + } + + return new ArrayCollection($q->getQuery()->getResult()); + } + + /** + * @param $campaignId + * + * @return array + */ + public function getScheduledCounts($campaignId) + { + $date = new \Datetime('now', new \DateTimeZone('UTC')); + + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $results = $q->select('COUNT(*) as event_count, l.event_id') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'l') + ->where( + $q->expr()->andX( + $q->expr()->eq('l.campaign_id', ':campaignId'), + $q->expr()->eq('l.is_scheduled', ':true'), + $q->expr()->lte('l.trigger_date', ':now') + ) + ) + ->setParameter('campaignId', $campaignId) + ->setParameter('now', $date->format('Y-m-d H:i:s')) + ->setParameter('true', true, \PDO::PARAM_BOOL) + ->groupBy('l.event_id') + ->execute() + ->fetchAll(); + + $events = []; + + foreach ($results as $result) { + $events[$result['event_id']] = (int) $result['event_count']; + } + + return $events; + } } diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php new file mode 100644 index 00000000000..bcf0c6d8783 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -0,0 +1,127 @@ +config = $config; + $this->event = $event; + $this->logs = $logs; + $this->extractContacts(); + } + + /** + * @return AbstractEventAccessor + */ + public function getConfig() + { + return $this->config; + } + + /** + * @return Event + */ + public function getEvent() + { + return $this->event; + } + + /** + * Return an array of Lead entities keyed by LeadEventLog ID. + * + * @return Lead[] + */ + public function getContacts() + { + return $this->contacts; + } + + /** + * Get the IDs of all contacts affected by this event. + * + * @return array + */ + public function getContactIds() + { + return array_keys($this->logContactXref); + } + + /** + * @param int $id + * + * @return mixed|null + */ + public function findLogByContactId($id) + { + return $this->logs->get($this->logContactXref[$id]); + } + + /** + * Check if an event is applicable. + * + * @param $eventType + */ + public function checkContext($eventType) + { + return strtolower($eventType) === strtolower($this->event->getType()); + } + + private function extractContacts() + { + /** @var LeadEventLog $log */ + foreach ($this->logs as $log) { + $contact = $log->getLead(); + $this->contacts[$log->getId()] = $contact; + $this->logContactXref[$contact->getId()] = $log->getId(); + } + } +} diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php index d9a143be694..b4823f32f3f 100644 --- a/app/bundles/CampaignBundle/Event/PendingEvent.php +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -17,23 +17,8 @@ use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; -class PendingEvent extends \Symfony\Component\EventDispatcher\Event +class PendingEvent extends AbstractLogCollectionEvent { - /** - * @var AbstractEventAccessor - */ - private $config; - - /** - * @var Event - */ - private $event; - - /** - * @var ArrayCollection - */ - private $pending; - /** * @var ArrayCollection */ @@ -54,37 +39,25 @@ class PendingEvent extends \Symfony\Component\EventDispatcher\Event */ private $channelId; + /** + * @var \DateTime + */ + private $now; + /** * PendingEvent constructor. * * @param AbstractEventAccessor $config * @param Event $event - * @param ArrayCollection $pending + * @param ArrayCollection $logs */ - public function __construct(AbstractEventAccessor $config, Event $event, ArrayCollection $pending) + public function __construct(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) { - $this->config = $config; - $this->event = $event; - $this->pending = $pending; - $this->failures = new ArrayCollection(); $this->successful = new ArrayCollection(); - } - - /** - * @return AbstractEventAccessor - */ - public function getConfig() - { - return $this->config; - } + $this->now = new \DateTime(); - /** - * @return Event - */ - public function getEvent() - { - return $this->event; + parent::__construct($config, $event, $logs); } /** @@ -92,7 +65,7 @@ public function getEvent() */ public function getPending() { - return $this->pending; + return $this->logs; } /** @@ -130,7 +103,7 @@ public function fail(LeadEventLog $log, $reason) */ public function failAll($reason) { - foreach ($this->pending as $log) { + foreach ($this->logs as $log) { $this->fail($log, $reason); } } @@ -150,9 +123,23 @@ public function pass(LeadEventLog $log) $log->setMetadata($metadata); } $this->logChannel($log); + $log->setIsScheduled(false) + ->setDateTriggered($this->now); + $this->successful->add($log); } + /** + * Pass all pending. + */ + public function passAll() + { + /** @var LeadEventLog $log */ + foreach ($this->logs as $log) { + $this->pass($log); + } + } + /** * @return ArrayCollection */ @@ -179,16 +166,6 @@ public function setChannel($channel, $channelId = null) $this->channelId = $channelId; } - /** - * Check if an event is applicable. - * - * @param $eventType - */ - public function checkContext($eventType) - { - return strtolower($eventType) === strtolower($this->event->getType()); - } - /** * @param LeadEventLog $log */ diff --git a/app/bundles/CampaignBundle/Event/ScheduledBatchEvent.php b/app/bundles/CampaignBundle/Event/ScheduledBatchEvent.php new file mode 100644 index 00000000000..74f14611fca --- /dev/null +++ b/app/bundles/CampaignBundle/Event/ScheduledBatchEvent.php @@ -0,0 +1,25 @@ +logs; + } +} diff --git a/app/bundles/CampaignBundle/Event/ScheduledEvent.php b/app/bundles/CampaignBundle/Event/ScheduledEvent.php index 4fecf468828..cd4e5d162f7 100644 --- a/app/bundles/CampaignBundle/Event/ScheduledEvent.php +++ b/app/bundles/CampaignBundle/Event/ScheduledEvent.php @@ -26,12 +26,12 @@ class ScheduledEvent extends CampaignScheduledEvent /** * @var AbstractEventAccessor */ - private $config; + private $eventConfig; /** * @var ArrayCollection */ - private $log; + private $eventLog; /** * PendingEvent constructor. @@ -42,8 +42,8 @@ class ScheduledEvent extends CampaignScheduledEvent */ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) { - $this->config = $config; - $this->log = $log; + $this->eventConfig = $config; + $this->eventLog = $log; // @deprecated support for pre 2.13.0; to be removed in 3.0 parent::__construct( @@ -64,7 +64,7 @@ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) */ public function getConfig() { - return $this->config; + return $this->eventConfig; } /** @@ -72,6 +72,6 @@ public function getConfig() */ public function getLog() { - return $this->log; + return $this->eventLog; } } diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php new file mode 100644 index 00000000000..d32ec4169ae --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php @@ -0,0 +1,64 @@ +leadRepository = $leadRepository; + } + + /** + * Hydrate contacts with custom field value, companies, etc. + * + * @param ArrayCollection $logs + * + * @return ArrayCollection + */ + public function hydrateContacts(ArrayCollection $logs) + { + $contactIds = []; + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $contactIds[] = $log->getLead()->getId(); + } + + $contacts = $this->leadRepository->getContactCollection($contactIds); + + foreach ($logs as $log) { + $contactId = $log->getLead()->getId(); + $contact = $contacts->get($contactId); + + $log->setLead($contact); + } + } + + public function clear() + { + $this->leadRepository->clear(); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index 9c1be706e13..38226d23927 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -19,6 +19,7 @@ use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; +use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php index d0bc7509849..8d4d8422170 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/Exception/LogPassedAndFailedException.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\CampaignBundle\Executioner\Dispatcher; +namespace Mautic\CampaignBundle\Executioner\Dispatcher\Exception; use Mautic\CampaignBundle\Entity\LeadEventLog; diff --git a/app/bundles/CampaignBundle/Executioner/Event/Action.php b/app/bundles/CampaignBundle/Executioner/Event/Action.php index 83616136ec2..76cf40d73d1 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Action.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Action.php @@ -51,19 +51,19 @@ public function __construct(EventLogger $eventLogger, EventDispatcher $dispatche * * @return mixed|void * - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\LogNotProcessedException - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException */ - public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) { // Ensure each contact has a log entry to prevent them from being picked up again prematurely foreach ($contacts as $contact) { $log = $this->getLogEntry($event, $contact); ChannelExtractor::setChannel($log, $event, $config); - $this->eventLogger->addToQueue($log, false); + $this->eventLogger->addToQueue($log); } - $this->eventLogger->persistQueued(false); + $this->eventLogger->persistQueued(); // Execute to process the batch of contacts $this->dispatcher->executeEvent($config, $event, $this->eventLogger->getLogs()); @@ -72,6 +72,23 @@ public function execute(AbstractEventAccessor $config, Event $event, ArrayCollec $this->eventLogger->persist(); } + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + */ + public function executeLogs(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + { + // Execute to process the batch of contacts + $this->dispatcher->executeEvent($config, $event, $logs); + + // Update log entries or persist failed entries + $this->eventLogger->persistCollection($logs); + } + /** * @param Event $event * @param Lead $contact diff --git a/app/bundles/CampaignBundle/Executioner/Event/Condition.php b/app/bundles/CampaignBundle/Executioner/Event/Condition.php index d3f3020e650..c501dd60267 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Condition.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Condition.php @@ -49,7 +49,11 @@ public function __construct(EventLogger $eventLogger, EventDispatcher $dispatche * * @return mixed|void */ - public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + { + } + + public function executeLogs(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) { } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Decision.php b/app/bundles/CampaignBundle/Executioner/Event/Decision.php index 722f0c63178..2a10573265c 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Decision.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Decision.php @@ -49,7 +49,11 @@ public function __construct(EventLogger $eventLogger, EventDispatcher $dispatche * * @return mixed|void */ - public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + { + } + + public function executeLogs(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) { } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php index c13c1ea1c2a..acb90c3422b 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php +++ b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php @@ -24,5 +24,5 @@ interface EventInterface * * @return mixed */ - public function execute(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts); + public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts); } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 058f5366c74..76294a90efc 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -74,24 +74,64 @@ public function __construct( * @param Event $event * @param ArrayCollection $contacts * - * @throws Dispatcher\LogNotProcessedException - * @throws Dispatcher\LogPassedAndFailedException + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException */ - public function execute(Event $event, ArrayCollection $contacts) + public function executeForContacts(Event $event, ArrayCollection $contacts) { $this->logger->debug('CAMPAIGN: Executing event ID '.$event->getId()); + if ($contacts->count()) { + $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); + + return; + } + + $config = $this->collector->getEventConfig($event); + + switch ($event->getEventType()) { + case Event::TYPE_ACTION: + $this->actionExecutioner->executeForContacts($config, $event, $contacts); + break; + case Event::TYPE_CONDITION: + $this->conditionExecutioner->executeForContacts($config, $event, $contacts); + break; + case Event::TYPE_DECISION: + $this->decisionExecutioner->executeForContacts($config, $event, $contacts); + break; + default: + throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); + } + } + + /** + * @param Event $event + * @param ArrayCollection $contacts + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + */ + public function executeLogs(Event $event, ArrayCollection $logs) + { + $this->logger->debug('CAMPAIGN: Executing event ID '.$event->getId()); + + if (!$logs->count()) { + $this->logger->debug('CAMPAIGN: No logs to process for event ID '.$event->getId()); + + return; + } + $config = $this->collector->getEventConfig($event); switch ($event->getEventType()) { case Event::TYPE_ACTION: - $this->actionExecutioner->execute($config, $event, $contacts); + $this->actionExecutioner->executeLogs($config, $event, $logs); break; case Event::TYPE_CONDITION: - $this->conditionExecutioner->execute($config, $event, $contacts); + $this->conditionExecutioner->executeLogs($config, $event, $logs); break; case Event::TYPE_DECISION: - $this->decisionExecutioner->execute($config, $event, $contacts); + $this->decisionExecutioner->executeLogs($config, $event, $logs); break; default: throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); diff --git a/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php b/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php new file mode 100644 index 00000000000..2222fcd8908 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php @@ -0,0 +1,36 @@ +campaign = $campaign; - $this->contactId = null; - $this->eventLimit = $eventLimit; - $this->maxEventsToExecute = $maxEventsToExecute; - $this->output = ($output) ? $output : new NullOutput(); - - $this->prepareForExecution(); - $this->executeOrSchedule(); + $this->campaign = $campaign; + $this->contactId = null; + $this->batchLimit = $batchLimit; + $this->output = ($output) ? $output : new NullOutput(); + + $this->execute(); } /** @@ -143,22 +140,40 @@ public function executeForCampaign(Campaign $campaign, $eventLimit = 100, $maxEv * @param $contactId * @param OutputInterface|null $output * - * @throws NoContactsFound - * @throws NoEventsFound + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws NotSchedulableException */ public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) { - $this->campaign = $campaign; - $this->contactId = $contactId; - $this->output = ($output) ? $output : new NullOutput(); + $this->campaign = $campaign; + $this->contactId = $contactId; + $this->output = ($output) ? $output : new NullOutput(); + $this->batchLimit = null; - // Process all events for this contact - $this->eventLimit = null; - $this->maxEventsToExecute = null; + $this->execute(); + } - $this->prepareForExecution(); - $this->executeOrSchedule(); + /** + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws NotSchedulableException + */ + private function execute() + { + try { + $this->prepareForExecution(); + $this->executeOrScheduleEvent(); + } catch (NoContactsFound $exception) { + $this->logger->debug('CAMPAIGN: No more contacts to process'); + } catch (NoEventsFound $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } } /** @@ -168,69 +183,72 @@ private function prepareForExecution() { $this->logger->debug('CAMPAIGN: Triggering kickoff events'); - defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - $this->batchCounter = 0; - $this->rootEvents = $this->campaign->getRootEvents(); - $totalRootEvents = $this->rootEvents->count(); + + $this->rootEvents = $this->campaign->getRootEvents(); + $totalRootEvents = $this->rootEvents->count(); $this->logger->debug('CAMPAIGN: Processing the following events: '.implode(', ', $this->rootEvents->getKeys())); $totalContacts = $this->kickoffContacts->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->contactId); $totalKickoffEvents = $totalRootEvents * $totalContacts; + $this->output->writeln( $this->translator->trans( 'mautic.campaign.trigger.event_count', [ '%events%' => $totalKickoffEvents, - '%batch%' => $this->eventLimit, + '%batch%' => $this->batchLimit, ] ) ); - if (!$totalKickoffEvents) { - $this->logger->debug('CAMPAIGN: No contacts/events to process'); + $this->progressBar = ProgressBarHelper::init($this->output, $totalKickoffEvents); + $this->progressBar->start(); + if (!$totalKickoffEvents) { throw new NoEventsFound(); } - - if (!$this->maxEventsToExecute) { - $this->maxEventsToExecute = $totalKickoffEvents; - } - - $this->progressBar = ProgressBarHelper::init($this->output, $this->maxEventsToExecute); - $this->progressBar->start(); } /** - * @throws Dispatcher\LogNotProcessedException - * @throws Dispatcher\LogPassedAndFailedException + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws NoContactsFound * @throws NotSchedulableException */ - private function executeOrSchedule() + private function executeOrScheduleEvent() { // Use the same timestamp across all contacts processed $now = new \DateTime(); // Loop over contacts until the entire campaign is executed - try { - while ($contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->eventLimit, $this->contactId)) { - /** @var Event $event */ - foreach ($this->rootEvents as $event) { - // Check if the event should be scheduled (let the scheduler's do the debug logging) - $executionDate = $this->scheduler->getExecutionDateTime($event, $now); - if ($executionDate > $now) { - $this->scheduler->schedule($event, $executionDate, $contacts); - continue; - } - - // Execute the event for the batch of contacts - $this->executioner->execute($event, $contacts); + $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->batchLimit, $this->contactId); + while ($contacts->count()) { + /** @var Event $event */ + foreach ($this->rootEvents as $event) { + $this->progressBar->advance($contacts->count()); + + // Check if the event should be scheduled (let the schedulers do the debug logging) + $executionDate = $this->scheduler->getExecutionDateTime($event, $now); + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$event->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s'). + ' compared to '.$now->format('Y-m-d H:i:s') + ); + + if ($executionDate > $now) { + $this->scheduler->schedule($event, $executionDate, $contacts); + continue; } - $this->kickoffContacts->clear(); + // Execute the event for the batch of contacts + $this->executioner->executeForContacts($event, $contacts); } - } catch (NoContactsFound $exception) { - // We're done here + + $this->kickoffContacts->clear(); + + // Get the next batch + $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->batchLimit, $this->contactId); } } } diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index bf9613cdf27..68749dd6523 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -65,12 +65,12 @@ public function __construct(IpLookupHelper $ipLookupHelper, LeadModel $leadModel /** * @param LeadEventLog $log */ - public function addToQueue(LeadEventLog $log, $clearPostPersist = true) + public function addToQueue(LeadEventLog $log) { $this->queued->add($log); if ($this->queued->count() >= 20) { - $this->persistQueued($clearPostPersist); + $this->persistQueued(); } } @@ -111,21 +111,16 @@ public function buildLogEntry(Event $event, $lead = null, $systemTriggered = fal /** * Persist the queue, clear the entities from memory, and reset the queue. */ - public function persistQueued($clearPostPersist = true) + public function persistQueued() { - if ($this->queued) { + if ($this->queued->count()) { $this->repo->saveEntities($this->queued->getValues()); - - if ($clearPostPersist) { - $this->repo->clear(); - } } - if (!$clearPostPersist) { - // The logs are needed so don't clear - foreach ($this->queued as $log) { - $this->processed->add($log); - } + // Push them into the processed ArrayCollection to be used later. + /** @var LeadEventLog $log */ + foreach ($this->queued as $log) { + $this->processed->set($log->getId(), $log); } $this->queued->clear(); @@ -139,12 +134,25 @@ public function getLogs() return $this->processed; } + /** + * @param ArrayCollection $collection + */ + public function persistCollection(ArrayCollection $collection) + { + if (!$collection->count()) { + return; + } + + $this->repo->saveEntities($collection->getValues()); + $this->repo->clear(); + } + /** * Persist processed entities after they've been updated. */ public function persist() { - if (!$this->processed) { + if (!$this->processed->count()) { return; } diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index 54d0a31a71c..def51805918 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -11,6 +11,279 @@ namespace Mautic\CampaignBundle\Executioner; -class ScheduledExecutioner +use Doctrine\Common\Collections\ArrayCollection; +use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\Entity\LeadEventLogRepository; +use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; +use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; +use Mautic\CoreBundle\Helper\ProgressBarHelper; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Translation\TranslatorInterface; + +class ScheduledExecutioner implements ExecutionerInterface { + /** + * @var LeadEventLogRepository + */ + private $repo; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var EventExecutioner + */ + private $executioner; + + /** + * @var EventScheduler + */ + private $scheduler; + + /** + * @var ScheduledContacts + */ + private $scheduledContacts; + + /** + * @var Campaign + */ + private $campaign; + + /** + * @var + */ + private $contactId; + + /** + * @var int + */ + private $batchLimit; + + /** + * @var OutputInterface + */ + private $output; + + /** + * @var ProgressBar + */ + private $progressBar; + + /** + * @var array + */ + private $scheduledEvents; + + /** + * ScheduledExecutioner constructor. + * + * @param LeadEventLogRepository $repository + * @param LoggerInterface $logger + * @param TranslatorInterface $translator + * @param EventExecutioner $executioner + * @param EventScheduler $scheduler + * @param ScheduledContacts $scheduledContacts + */ + public function __construct( + LeadEventLogRepository $repository, + LoggerInterface $logger, + TranslatorInterface $translator, + EventExecutioner $executioner, + EventScheduler $scheduler, + ScheduledContacts $scheduledContacts + ) { + $this->repo = $repository; + $this->logger = $logger; + $this->translator = $translator; + $this->executioner = $executioner; + $this->scheduler = $scheduler; + $this->scheduledContacts = $scheduledContacts; + } + + /** + * @param Campaign $campaign + * @param int $batchLimit + * @param OutputInterface|null $output + * + * @return mixed|void + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + public function executeForCampaign(Campaign $campaign, $batchLimit = 100, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->batchLimit = $batchLimit; + $this->output = ($output) ? $output : new NullOutput(); + + $this->logger->debug('CAMPAIGN: Triggering scheduled events'); + + $this->execute(); + } + + /** + * @param Campaign $campaign + * @param $contactId + * @param OutputInterface|null $output + * + * @return mixed|void + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->contactId = $contactId; + $this->output = ($output) ? $output : new NullOutput(); + $this->batchLimit = null; + + $this->execute(); + } + + /** + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + private function execute() + { + try { + $this->prepareForExecution(); + $this->executeOrRecheduleEvent(); + } catch (NoEventsFound $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } + } + + /** + * @throws NoEventsFound + */ + private function prepareForExecution() + { + // Get counts by event + $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId()); + $totalScheduledCount = array_sum($scheduledEvents); + $this->scheduledEvents = array_keys($scheduledEvents); + $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); + + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalScheduledCount, + '%batch%' => $this->batchLimit, + ] + ) + ); + + $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); + $this->progressBar->start(); + + if (!$totalScheduledCount) { + throw new NoEventsFound(); + } + } + + /** + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + private function executeOrRecheduleEvent() + { + // Use the same timestamp across all contacts processed + $now = new \DateTime(); + + foreach ($this->scheduledEvents as $eventId) { + // Loop over contacts until the entire campaign is executed + $this->executeScheduled($eventId, $now); + } + } + + /** + * @param $eventId + * @param \DateTime $now + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + private function executeScheduled($eventId, \DateTime $now) + { + $logs = $this->repo->getScheduled($eventId, $this->batchLimit, $this->contactId); + $this->scheduledContacts->hydrateContacts($logs); + + while ($logs->count()) { + $event = $logs->first()->getEvent(); + $this->progressBar->advance($logs->count()); + $this->validateSchedule($logs, $event, $now); + + // Execute if there are any that did not get rescheduled + $this->executioner->executeLogs($event, $logs); + + // Get next batch + $this->scheduledContacts->clear(); + $logs = $this->repo->getScheduled($eventId, $this->batchLimit, $this->contactId); + } + } + + /** + * @param ArrayCollection $logs + * @param Event $event + * @param \DateTime $now + * + * @throws Scheduler\Exception\NotSchedulableException + */ + private function validateSchedule(ArrayCollection $logs, Event $event, \DateTime $now) + { + // Check if the event should be scheduled (let the schedulers do the debug logging) + /** @var LeadEventLog $log */ + foreach ($logs as $key => $log) { + if ($createdDate = $log->getDateTriggered()) { + // Date Triggered will be when the log entry was first created so use it to compare to ensure that the event's schedule + // hasn't been changed since this even was first scheduled + $executionDate = $this->scheduler->getExecutionDateTime($event, $now, $createdDate); + $this->logger->debug( + 'CAMPAIGN: Log ID# '.$log->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s'). + ' compared to '.$now->format('Y-m-d H:i:s') + ); + + if ($executionDate > $now) { + // The schedule has changed for this event since first scheduled + $this->scheduler->reschedule($log, $executionDate); + $logs->remove($key); + + continue; + } + } + } + } } diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index 165adf1e80b..ce04509fcbe 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -15,6 +15,7 @@ use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\Event\ScheduledBatchEvent; use Mautic\CampaignBundle\Event\ScheduledEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; @@ -120,6 +121,12 @@ public function schedule(Event $event, \DateTime $executionDate, ArrayCollection // Persist any pending in the queue $this->eventLogger->persistQueued(); + + // Send out a batch event + $this->dispatchBatchScheduledEvent($config, $event, $this->eventLogger->getLogs()); + + // Update log entries and clear from memory + $this->eventLogger->persist(); } /** @@ -128,6 +135,7 @@ public function schedule(Event $event, \DateTime $executionDate, ArrayCollection public function reschedule(LeadEventLog $log, \DateTime $toBeExecutedOn) { $log->setTriggerDate($toBeExecutedOn); + $this->eventLogger->persistLog($log); $event = $log->getEvent(); $config = $this->collector->getEventConfig($event); @@ -161,19 +169,26 @@ public function rescheduleFailure(LeadEventLog $log) * * @throws NotSchedulableException */ - public function getExecutionDateTime(Event $event, \DateTime $now = null) + public function getExecutionDateTime(Event $event, \DateTime $now = null, \DateTime $comparedToDateTime = null) { if (null === $now) { $now = new \DateTime(); } + if (null === $comparedToDateTime) { + $comparedToDateTime = clone $now; + } else { + // Prevent comparisons from modifying original object + $comparedToDateTime = clone $comparedToDateTime; + } + switch ($event->getTriggerMode()) { case Event::TRIGGER_MODE_IMMEDIATE: return $now; case Event::TRIGGER_MODE_INTERVAL: - return $this->intervalScheduler->getExecutionDateTime($event, $now, $now); + return $this->intervalScheduler->getExecutionDateTime($event, $now, $comparedToDateTime); case Event::TRIGGER_MODE_DATE: - return $this->dateTimeScheduler->getExecutionDateTime($event, $now, $now); + return $this->dateTimeScheduler->getExecutionDateTime($event, $now, $comparedToDateTime); } throw new NotSchedulableException(); @@ -190,4 +205,16 @@ private function dispatchScheduledEvent(AbstractEventAccessor $config, LeadEvent new ScheduledEvent($config, $log) ); } + + /** + * @param AbstractEventAccessor $config + * @param ArrayCollection $logs + */ + private function dispatchBatchScheduledEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_SCHEDULED_BATCH, + new ScheduledBatchEvent($config, $event, $logs) + ); + } } diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php index 8df143e50bb..fafb4f57fec 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -45,7 +45,6 @@ public function __construct(LoggerInterface $logger) public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime) { // $triggerOn = $negate ? clone $parentTriggeredDate : new \DateTime(); - $interval = $event->getTriggerInterval(); $unit = $event->getTriggerIntervalUnit(); diff --git a/app/bundles/CampaignBundle/Translations/en_US/messages.ini b/app/bundles/CampaignBundle/Translations/en_US/messages.ini index f32c95a3935..92c27a1d9b5 100644 --- a/app/bundles/CampaignBundle/Translations/en_US/messages.ini +++ b/app/bundles/CampaignBundle/Translations/en_US/messages.ini @@ -102,7 +102,7 @@ mautic.campaign.rebuild.not_found="Campaign #%id% does not exist" mautic.campaign.rebuild.to_be_added="%leads% total contact(s) to be added in batches of %batch%" mautic.campaign.rebuild.to_be_removed="%leads% total contact(s) to be removed in batches of %batch%" mautic.campaign.scheduled="Campaign event scheduled" -mautic.campaign.trigger.event_count="%events% total events(s) to be processed in batches of %batch%" +mautic.campaign.trigger.event_count="%events% total events(s) to be processed in batches of %batch% contacts" mautic.campaign.trigger.events_executed="%events% event(s) executed" mautic.campaign.trigger.lead_count_processed="%leads% total contact(s) to be processed in batches of %batch%" mautic.campaign.trigger.lead_count_analyzed="%leads% total contact(s) to be analyzed in batches of %batch%" diff --git a/app/bundles/CoreBundle/IpLookup/MaxmindDownloadLookup.php b/app/bundles/CoreBundle/IpLookup/MaxmindDownloadLookup.php index 729d5048948..69baed83b7a 100644 --- a/app/bundles/CoreBundle/IpLookup/MaxmindDownloadLookup.php +++ b/app/bundles/CoreBundle/IpLookup/MaxmindDownloadLookup.php @@ -66,9 +66,6 @@ protected function lookup() $this->timezone = $record->location->timeZone; $this->zipcode = $record->location->postalCode; } catch (\Exception $exception) { - if ($this->logger) { - $this->logger->warn('IP LOOKUP: '.$exception->getMessage()); - } } } } diff --git a/app/bundles/CoreBundle/Model/AbstractCommonModel.php b/app/bundles/CoreBundle/Model/AbstractCommonModel.php index cf11be6269e..a1619d3583b 100644 --- a/app/bundles/CoreBundle/Model/AbstractCommonModel.php +++ b/app/bundles/CoreBundle/Model/AbstractCommonModel.php @@ -24,6 +24,7 @@ use Symfony\Bundle\FrameworkBundle\Routing\Router; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Intl\Intl; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Translation\TranslatorInterface; /** @@ -285,7 +286,8 @@ public function decodeArrayFromUrl($string, $urlDecode = true) */ public function buildUrl($route, $routeParams = [], $absolute = true, $clickthrough = [], $utmTags = []) { - $url = $this->router->generate($route, $routeParams, $absolute); + $referenceType = ($absolute) ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::RELATIVE_PATH; + $url = $this->router->generate($route, $routeParams, $referenceType); $url .= (!empty($clickthrough)) ? '?ct='.$this->encodeArrayForUrl($clickthrough) : ''; return $url; diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index de342c9163d..d0892f6131d 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -144,13 +144,14 @@ 'class' => 'Mautic\EmailBundle\EventListener\TokenSubscriber', ], 'mautic.email.campaignbundle.subscriber' => [ - 'class' => 'Mautic\EmailBundle\EventListener\CampaignSubscriber', + 'class' => \Mautic\EmailBundle\EventListener\CampaignSubscriber::class, 'arguments' => [ 'mautic.lead.model.lead', 'mautic.email.model.email', 'mautic.campaign.model.event', 'mautic.channel.model.queue', 'mautic.email.model.send_email_to_user', + 'translator', ], ], 'mautic.email.campaignbundle.condition_subscriber' => [ diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 99c33b6c246..d3483113295 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -18,7 +18,6 @@ use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\Model\EventModel; use Mautic\ChannelBundle\Model\MessageQueueModel; -use Mautic\CoreBundle\EventListener\CommonSubscriber; use Mautic\EmailBundle\EmailEvents; use Mautic\EmailBundle\Entity\Email; use Mautic\EmailBundle\Event\EmailOpenEvent; @@ -27,13 +26,16 @@ use Mautic\EmailBundle\Helper\UrlMatcher; use Mautic\EmailBundle\Model\EmailModel; use Mautic\EmailBundle\Model\SendEmailToUser; +use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; use Mautic\PageBundle\Entity\Hit; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Translation\TranslatorInterface; /** * Class CampaignSubscriber. */ -class CampaignSubscriber extends CommonSubscriber +class CampaignSubscriber implements EventSubscriberInterface { /** * @var LeadModel @@ -60,6 +62,11 @@ class CampaignSubscriber extends CommonSubscriber */ private $sendEmailToUser; + /** + * @var TranslatorInterface + */ + private $translator; + /** * @param LeadModel $leadModel * @param EmailModel $emailModel @@ -72,13 +79,15 @@ public function __construct( EmailModel $emailModel, EventModel $eventModel, MessageQueueModel $messageQueueModel, - SendEmailToUser $sendEmailToUser + SendEmailToUser $sendEmailToUser, + TranslatorInterface $translator ) { $this->leadModel = $leadModel; $this->emailModel = $emailModel; $this->campaignEventModel = $eventModel; $this->messageQueueModel = $messageQueueModel; $this->sendEmailToUser = $sendEmailToUser; + $this->translator = $translator; } /** @@ -253,11 +262,11 @@ public function onCampaignTriggerDecision(CampaignExecutionEvent $event) } /** - * Triggers the action which sends email to contact. + * Triggers the action which sends email to contacts. * * @param PendingEvent $event * - * @return PendingEvent|null + * @throws \Doctrine\ORM\ORMException */ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) { @@ -286,45 +295,56 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) ]; // Determine if this email is transactional/marketing - $contacts = []; - $logAssignments = []; - $pending = $event->getPending(); - /** @var LeadEventLog $log */ - foreach ($pending as $id => $log) { - $lead = $log->getLead(); - $leadCredentials = $lead->getProfileFields(); + $logAssignments = []; + $pending = $event->getPending(); + $contacts = $event->getContacts(); + $credentialArray = []; + + /** + * @var int + * @var Lead $contact + */ + foreach ($contacts as $logId => $contact) { + $leadCredentials = $contact->getProfileFields(); if (empty($leadCredentials['email'])) { - $event->fail($log, $lead->getPrimaryIdentifier().' does not have an email'); + $event->fail( + $pending->get($logId), + $this->translator->trans('mautic.email.contact_has_no_email', ['%contact%' => $contact->getPrimaryIdentifier()]) + ); continue; } - $contacts[$id] = $leadCredentials; - $logAssignments[$lead->getId()] = $id; + $credentialArray[$logId] = $leadCredentials; } if ('marketing' == $type) { - // Determine if this lead has received the email before + // Determine if this lead has received the email before and if so, don't send it again $stats = $this->emailModel->getStatRepository()->checkContactsSentEmail(array_keys($logAssignments), $emailId, true); foreach ($stats as $contactId => $sent) { /** @var LeadEventLog $log */ - $log = $pending->get($id); - $event->fail($log, 'Already received email'); - unset($contacts[$id]); + $log = $event->findLogByContactId($contactId); + $event->fail( + $log, + $this->translator->trans('mautic.email.contact_already_received_marketing_email', ['%contact%' => $contact->getPrimaryIdentifier()]) + ); + unset($credentialArray[$log->getId()]); } } - if (count($contacts)) { - $errors = $this->emailModel->sendEmail($email, $contacts, $options); + if (count($credentialArray)) { + $errors = $this->emailModel->sendEmail($email, $credentialArray, $options); - if (empty($errors)) { - foreach ($contacts as $logId => $contactId) { - $event->pass($pending->get($logId)); - } - } else { - foreach ($errors as $failedContactId => $reason) { - $event->fail($pending->get($logAssignments[$failedContactId]), $reason); - } + // Fail those that failed to send + foreach ($errors as $failedContactId => $reason) { + $log = $pending->get($logAssignments[$failedContactId]); + $event->fail($log, $reason); + unset($credentialArray[$log->getId()]); + } + + // Pass everyone else + foreach (array_keys($credentialArray) as $logId) { + $event->pass($pending->get($logId)); } } } diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index d6d59e0c1dd..8919b6cc339 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -23,6 +23,7 @@ use Mautic\EmailBundle\Swiftmailer\Message\MauticMessage; use Mautic\EmailBundle\Swiftmailer\Transport\TokenTransportInterface; use Mautic\LeadBundle\Entity\Lead; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; /** * Class MailHelper. @@ -1499,7 +1500,7 @@ public function getCustomHeaders() private function getUnsubscribeHeader() { if ($this->idHash) { - $url = $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], true); + $url = $this->factory->getRouter()->generate('mautic_email_unsubscribe', ['idHash' => $this->idHash], UrlGeneratorInterface::ABSOLUTE_URL); return "<$url>"; } @@ -1547,7 +1548,7 @@ public function getTokens() [ 'idHash' => $this->idHash, ], - true + UrlGeneratorInterface::ABSOLUTE_URL ); } else { $tokens['{tracking_pixel}'] = self::getBlankPixel(); diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index 37c8738de64..dccb147e148 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -54,6 +54,8 @@ mautic.email.complaint.reason.unknown="Unknown complaint from mail provider" mautic.email.complaint.reason.abuse="Mail provider indicated unsolicited email or some other kind of email abuse" mautic.email.complaint.reason.fraud="Mail provider indicated some kind of fraud or phishing activity" mautic.email.complaint.reason.virus="Mail provider reports that a virus is found in the originating message" +mautic.email.contact_already_received_marketing_email="%contact% has already received this marketing email." +mautic.email.contact_has_no_email="%contact% has no email address." mautic.email.builder.addcontent="Click to add content" mautic.email.builder.index="Extras" mautic.email.campaign.event.open="Opens email" From 39f460d866c43cd4dce4b81558e92f6fb5ace9c3 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Sun, 4 Feb 2018 23:50:52 -0600 Subject: [PATCH 429/778] Execute decisions, refactor parts of consuming contacts as logs --- app/bundles/CampaignBundle/CampaignEvents.php | 15 +- .../Command/TriggerCampaignCommand.php | 181 +++++------ app/bundles/CampaignBundle/Config/config.php | 32 +- .../Entity/CampaignRepository.php | 8 +- app/bundles/CampaignBundle/Entity/Event.php | 48 ++- .../CampaignBundle/Entity/EventRepository.php | 280 +++--------------- .../Entity/LegacyEventRepository.php | 264 +++++++++++++++++ .../Event/AbstractLogCollectionEvent.php | 10 - .../Event/CampaignExecutionEvent.php | 23 +- .../Event/CampaignScheduledEvent.php | 12 +- .../CampaignBundle/Event/ConditionEvent.php | 135 +++++++++ .../CampaignBundle/Event/ContextTrait.php | 31 ++ .../CampaignBundle/Event/DecisionEvent.php | 129 +++++++- .../CampaignBundle/Event/EventArrayTrait.php | 72 ++++- .../CampaignBundle/Event/PendingEvent.php | 2 + .../CampaignBundle/Event/ScheduledEvent.php | 20 +- .../Accessor/Event/AbstractEventAccessor.php | 9 - .../Accessor/Event/ActionAccessor.php | 19 ++ .../Accessor/Event/ConditionAccessor.php | 17 ++ .../Accessor/Event/DecisionAccessor.php | 14 + .../Executioner/DecisionExecutioner.php | 186 ++++++++++++ .../Dispatcher/EventDispatcher.php | 43 ++- .../Dispatcher/LegacyEventDispatcher.php | 71 ++++- .../Executioner/Event/Action.php | 74 +---- .../Executioner/Event/Condition.php | 60 +++- .../Executioner/Event/Decision.php | 72 ++++- .../Executioner/Event/EventInterface.php | 6 +- .../Executioner/EventExecutioner.php | 260 ++++++++++++++-- .../CampaignNotExecutableException.php | 16 + .../Exception/CannotProcessEventException.php | 16 + .../Exception/ConditionFailedException.php | 16 + .../DecisionNotApplicableException.php | 16 + .../Executioner/KickoffExecutioner.php | 30 +- .../Executioner/Logger/EventLogger.php | 11 +- .../Executioner/Result/Counter.php | 179 +++++++++++ .../Executioner/Result/EvaluatedContacts.php | 69 +++++ .../Executioner/ScheduledExecutioner.php | 27 +- .../CampaignBundle/Model/EventModel.php | 59 +--- .../LeadBundle/Tracker/ContactTracker.php | 9 + 39 files changed, 1931 insertions(+), 610 deletions(-) create mode 100644 app/bundles/CampaignBundle/Entity/LegacyEventRepository.php create mode 100644 app/bundles/CampaignBundle/Event/ConditionEvent.php create mode 100644 app/bundles/CampaignBundle/Event/ContextTrait.php create mode 100644 app/bundles/CampaignBundle/Executioner/Exception/CampaignNotExecutableException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Exception/CannotProcessEventException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Exception/ConditionFailedException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Exception/DecisionNotApplicableException.php create mode 100644 app/bundles/CampaignBundle/Executioner/Result/Counter.php create mode 100644 app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php diff --git a/app/bundles/CampaignBundle/CampaignEvents.php b/app/bundles/CampaignBundle/CampaignEvents.php index 2ff1819cf08..d10b2b50bec 100644 --- a/app/bundles/CampaignBundle/CampaignEvents.php +++ b/app/bundles/CampaignBundle/CampaignEvents.php @@ -134,13 +134,22 @@ final class CampaignEvents const ON_EVENT_FAILED = 'matuic.campaign_on_event_failed'; /** - * The mautic.campaign_on_event_decision event is dispatched when a campaign event is executed. + * The mautic.campaign_on_event_decision_evaluation event is dispatched when a campaign decision is to be evaluated. * * The event listener receives a Mautic\CampaignBundle\Event\DecisionEvent instance. * * @var string */ - const ON_EVENT_DECISION = 'mautic.campaign_on_event_decision'; + const ON_EVENT_DECISION_EVALUATION = 'mautic.campaign_on_event_decision_evaluation'; + + /** + * The mautic.campaign_on_event_decision_evaluation event is dispatched when a campaign decision is to be evaluated. + * + * The event listener receives a Mautic\CampaignBundle\Event\DecisionEvent instance. + * + * @var string + */ + const ON_EVENT_CONDITION_EVALUATION = 'mautic.campaign_on_event_decision_evaluation'; /** * @deprecated 2.13.0; to be removed in 3.0. Listen to ON_EVENT_EXECUTED and ON_EVENT_FAILED @@ -154,7 +163,7 @@ final class CampaignEvents const ON_EVENT_EXECUTION = 'mautic.campaign_on_event_execution'; /** - * @deprecated 2.13.0; to be removed in 3.0; Listen to ON_EVENT_DECISION instead + * @deprecated 2.13.0; to be removed in 3.0; Listen to ON_EVENT_DECISION_EVALUATION instead * * The mautic.campaign_on_event_decision_trigger event is dispatched after a lead decision triggers a set of actions or if the decision is set * as a root level event. diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 35b9f2beb6a..f5ac5c99c6f 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -68,23 +68,21 @@ protected function execute(InputInterface $input, OutputInterface $output) { $container = $this->getContainer(); - /** @var \Mautic\CampaignBundle\Model\EventModel $model */ - $model = $container->get('mautic.campaign.model.event'); /** @var \Mautic\CampaignBundle\Model\CampaignModel $campaignModel */ - $campaignModel = $container->get('mautic.campaign.model.campaign'); - $this->dispatcher = $container->get('event_dispatcher'); - $translator = $container->get('translator'); - $em = $container->get('doctrine')->getManager(); - $id = $input->getOption('campaign-id'); - $scheduleOnly = $input->getOption('scheduled-only'); - $negativeOnly = $input->getOption('negative-only'); - $batch = $input->getOption('batch-limit'); - $max = $input->getOption('max-events'); - - /** @var KickoffExecutioner $kickoff */ - $kickoff = $container->get('mautic.campaign.executioner.kickoff'); - /** @var ScheduledExecutioner $scheduled */ - $scheduled = $container->get('mautic.campaign.executioner.scheduled'); + $campaignModel = $container->get('mautic.campaign.model.campaign'); + $this->dispatcher = $container->get('event_dispatcher'); + $this->translator = $container->get('translator'); + $this->em = $container->get('doctrine')->getManager(); + $this->output = $output; + $id = $input->getOption('campaign-id'); + $scheduleOnly = $input->getOption('scheduled-only'); + $negativeOnly = $input->getOption('negative-only'); + $batchLimit = $input->getOption('batch-limit'); + + /* @var KickoffExecutioner $kickoff */ + $this->kickoff = $container->get('mautic.campaign.executioner.kickoff'); + /* @var ScheduledExecutioner $scheduled */ + $this->scheduled = $container->get('mautic.campaign.executioner.scheduled'); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); @@ -94,45 +92,13 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($id) { /** @var \Mautic\CampaignBundle\Entity\Campaign $campaign */ - $campaign = $campaignModel->getEntity($id); - - if ($campaign !== null && $campaign->isPublished()) { - if (!$this->dispatchTriggerEvent($campaign)) { - return 0; - } - - $totalProcessed = 0; - $output->writeln(''.$translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $id]).''); - - if (!$negativeOnly && !$scheduleOnly) { - //trigger starting action events for newly added contacts - $output->writeln(''.$translator->trans('mautic.campaign.trigger.starting').''); - $processed = $model->triggerStartingEvents($campaign, $totalProcessed, $batch, $max, $output); - $output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).''."\n" - ); - } - - if ((!$max || $totalProcessed < $max) && !$negativeOnly) { - //trigger scheduled events - $output->writeln(''.$translator->trans('mautic.campaign.trigger.scheduled').''); - $processed = $model->triggerScheduledEvents($campaign, $totalProcessed, $batch, $max, $output); - $output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).''."\n" - ); - } - - if ((!$max || $totalProcessed < $max) && !$scheduleOnly) { - //find and trigger "no" path events - $output->writeln(''.$translator->trans('mautic.campaign.trigger.negative').''); - $processed = $model->triggerNegativeEvents($campaign, $totalProcessed, $batch, $max, $output); - $output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).''."\n" - ); - } - } else { - $output->writeln(''.$translator->trans('mautic.campaign.rebuild.not_found', ['%id%' => $id]).''); + if (!$campaign = $campaignModel->getEntity($id)) { + $output->writeln(''.$this->translator->trans('mautic.campaign.rebuild.not_found', ['%id%' => $id]).''); + + return 0; } + + $this->triggerCampaign($campaign, $negativeOnly, $scheduleOnly, $batchLimit); } else { $campaigns = $campaignModel->getEntities( [ @@ -140,67 +106,11 @@ protected function execute(InputInterface $input, OutputInterface $output) ] ); - while (($c = $campaigns->next()) !== false) { - $totalProcessed = 0; - + while (($next = $campaigns->next()) !== false) { // Key is ID and not 0 - $c = reset($c); - - if ($c->isPublished()) { - if (!$this->dispatchTriggerEvent($c)) { - continue; - } - - $output->writeln(''.$translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $c->getId()]).''); - if (!$negativeOnly && !$scheduleOnly) { - //trigger starting action events for newly added contacts - $output->writeln(''.$translator->trans('mautic.campaign.trigger.starting').''); - $kickoff->executeForCampaign($c, $batch, $output); - $output->writeln(''.$translator->trans('mautic.campaign.trigger.scheduled').''); - $scheduled->executeForCampaign($c, $batch, $output); - - /*$output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' - ."\n" - );*/ - } - - /* - if ($max && $totalProcessed >= $max) { - continue; - } - - if (!$negativeOnly) { - //trigger scheduled events - $output->writeln(''.$translator->trans('mautic.campaign.trigger.scheduled').''); - $processed = $model->triggerScheduledEvents($c, $totalProcessed, $batch, $max, $output); - $output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' - ."\n" - ); - } - - if ($max && $totalProcessed >= $max) { - continue; - } - - if (!$scheduleOnly) { - //find and trigger "no" path events - $output->writeln(''.$translator->trans('mautic.campaign.trigger.negative').''); - $processed = $model->triggerNegativeEvents($c, $totalProcessed, $batch, $max, $output); - $output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' - ."\n" - ); - } - */ - } - - $em->detach($c); - unset($c); + $campaign = reset($next); + $this->triggerCampaign($campaign, $negativeOnly, $scheduleOnly, $batchLimit); } - - unset($campaigns); } $this->completeRun(); @@ -208,6 +118,51 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } + private function triggerCampaign(Campaign $campaign, $negativeOnly, $scheduleOnly, $batchLimit) + { + if (!$campaign->isPublished()) { + return; + } + + if (!$this->dispatchTriggerEvent($campaign)) { + return; + } + + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $campaign->getId()]).''); + if (!$negativeOnly && !$scheduleOnly) { + //trigger starting action events for newly added contacts + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.starting').''); + $counter = $this->kickoff->executeForCampaign($campaign, $batchLimit, $this->output); + $this->output->writeln( + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]).'' + ."\n" + ); + } + + if (!$negativeOnly) { + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.scheduled').''); + $counter = $this->scheduled->executeForCampaign($campaign, $batchLimit, $this->output); + $this->output->writeln( + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]).'' + ."\n" + ); + } + + /* + if (!$scheduleOnly) { + //find and trigger "no" path events + $output->writeln(''.$translator->trans('mautic.campaign.trigger.negative').''); + $processed = $model->triggerNegativeEvents($c, $totalProcessed, $batch, $max, $output); + $output->writeln( + ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' + ."\n" + ); + } + */ + + $this->em->detach($campaign); + } + /** * @param Campaign $campaign * diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 399be78b998..1dda4b244a9 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -222,12 +222,11 @@ 'class' => 'Mautic\CampaignBundle\Model\EventModel', 'arguments' => [ 'mautic.helper.ip_lookup', - 'mautic.helper.core_parameters', 'mautic.lead.model.lead', 'mautic.campaign.model.campaign', 'mautic.user.model.user', 'mautic.core.model.notification', - 'mautic.factory', + 'mautic.campaign.executioner.active_decision', ], ], 'mautic.campaign.model.event_log' => [ @@ -247,6 +246,20 @@ \Mautic\CampaignBundle\Entity\Campaign::class, ], ], + 'mautic.campaign.repository.lead' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\CampaignBundle\Entity\Lead::class, + ], + ], + 'mautic.campaign.repository.event' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\CampaignBundle\Entity\Event::class, + ], + ], 'mautic.campaign.repository.lead_event_log' => [ 'class' => Doctrine\ORM\EntityRepository::class, 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], @@ -321,14 +334,12 @@ 'mautic.campaign.executioner.action' => [ 'class' => \Mautic\CampaignBundle\Executioner\Event\Action::class, 'arguments' => [ - 'mautic.campaign.event_logger', 'mautic.campaign.event_dispatcher', ], ], 'mautic.campaign.executioner.condition' => [ 'class' => \Mautic\CampaignBundle\Executioner\Event\Condition::class, 'arguments' => [ - 'mautic.campaign.event_logger', 'mautic.campaign.event_dispatcher', ], ], @@ -343,6 +354,7 @@ 'class' => \Mautic\CampaignBundle\Executioner\EventExecutioner::class, 'arguments' => [ 'mautic.campaign.event_collector', + 'mautic.campaign.event_logger', 'mautic.campaign.executioner.action', 'mautic.campaign.executioner.condition', 'mautic.campaign.executioner.decision', @@ -370,6 +382,17 @@ 'mautic.campaign.contact_finder.scheduled', ], ], + 'mautic.campaign.executioner.active_decision' => [ + 'class' => \Mautic\CampaignBundle\Executioner\DecisionExecutioner::class, + 'arguments' => [ + 'monolog.logger.mautic', + 'mautic.lead.model.lead', + 'mautic.campaign.repository.event', + 'mautic.campaign.executioner', + 'mautic.campaign.executioner.decision', + 'mautic.campaign.event_collector', + ], + ], // @deprecated 2.13.0 for BC support; to be removed in 3.0 'mautic.campaign.legacy_event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher::class, @@ -377,6 +400,7 @@ 'event_dispatcher', 'mautic.campaign.scheduler', 'monolog.logger.mautic', + 'mautic.lead.model.lead', 'mautic.factory', ], ], diff --git a/app/bundles/CampaignBundle/Entity/CampaignRepository.php b/app/bundles/CampaignBundle/Entity/CampaignRepository.php index d54dc222397..3aabfd251a8 100644 --- a/app/bundles/CampaignBundle/Entity/CampaignRepository.php +++ b/app/bundles/CampaignBundle/Entity/CampaignRepository.php @@ -609,14 +609,16 @@ public function getCampaignLeadIds($campaignId, $start = 0, $limit = false, $pen ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') ->where( $sq->expr()->andX( - $sq->expr()->eq('cl.lead_id', 'e.lead_id'), - $sq->expr()->eq('e.campaign_id', (int) $campaignId) + $sq->expr()->eq('e.lead_id', 'cl.lead_id'), + $sq->expr()->eq('e.campaign_id', ':campaignId'), + $sq->expr()->eq('e.rotation', 'cl.rotation') ) ); $q->andWhere( sprintf('NOT EXISTS (%s)', $sq->getSQL()) - ); + ) + ->setParameter('campaignId', (int) $campaignId); } if (!empty($limit)) { diff --git a/app/bundles/CampaignBundle/Entity/Event.php b/app/bundles/CampaignBundle/Entity/Event.php index c5620cabda2..7b9aeb6153e 100644 --- a/app/bundles/CampaignBundle/Entity/Event.php +++ b/app/bundles/CampaignBundle/Entity/Event.php @@ -604,15 +604,57 @@ public function removeChild(Event $children) } /** - * Get children. - * - * @return \Doctrine\Common\Collections\Collection + * @return ArrayCollection */ public function getChildren() { return $this->children; } + /** + * @return ArrayCollection + */ + public function getPositiveChildren() + { + $criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', self::PATH_ACTION)); + + return $this->getChildren()->matching($criteria); + } + + /** + * @return ArrayCollection + */ + public function getNegativeChildren() + { + $criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', self::PATH_ACTION)); + + return $this->getChildren()->matching($criteria); + } + + /** + * @param $type + * + * @return ArrayCollection + */ + public function getChildrenByType($type) + { + $criteria = Criteria::create()->where(Criteria::expr()->eq('type', $type)); + + return $this->getChildren()->matching($criteria); + } + + /** + * @param $type + * + * @return ArrayCollection + */ + public function getChildrenByEventType($type) + { + $criteria = Criteria::create()->where(Criteria::expr()->eq('eventType', $type)); + + return $this->getChildren()->matching($criteria); + } + /** * Set parent. * diff --git a/app/bundles/CampaignBundle/Entity/EventRepository.php b/app/bundles/CampaignBundle/Entity/EventRepository.php index cf1732612e4..dc61fae25fb 100644 --- a/app/bundles/CampaignBundle/Entity/EventRepository.php +++ b/app/bundles/CampaignBundle/Entity/EventRepository.php @@ -11,12 +11,10 @@ namespace Mautic\CampaignBundle\Entity; -use Mautic\CoreBundle\Entity\CommonRepository; - /** * EventRepository. */ -class EventRepository extends CommonRepository +class EventRepository extends LegacyEventRepository { /** * Get a list of entities. @@ -52,77 +50,55 @@ public function getEntities(array $args = []) } /** - * Get array of published events based on type. - * - * @param $type - * @param array $campaigns - * @param null $leadId If included, only events that have not been triggered by the lead yet will be included - * @param bool $positivePathOnly If negative, all events including those with a negative path will be returned + * @param $contactId * * @return array */ - public function getPublishedByType($type, array $campaigns = null, $leadId = null, $positivePathOnly = true) + public function getContactPendingEvents($contactId, $type) { - $q = $this->createQueryBuilder('e') - ->select('c, e, ec, ep, ecc') - ->join('e.campaign', 'c') - ->leftJoin('e.children', 'ec') - ->leftJoin('e.parent', 'ep') - ->leftJoin('ec.campaign', 'ecc') - ->orderBy('e.order'); - - //make sure the published up and down dates are good - $expr = $this->getPublishedByDateExpression($q, 'c'); - - $expr->add( - $q->expr()->eq('e.type', ':type') - ); - - $q->where($expr) - ->setParameter('type', $type); - - if (!empty($campaigns)) { - $q->andWhere($q->expr()->in('c.id', ':campaigns')) - ->setParameter('campaigns', $campaigns); - } - - if ($leadId != null) { - // Events that aren't fired yet - $dq = $this->getEntityManager()->createQueryBuilder(); - $dq->select('ellev.id') - ->from('MauticCampaignBundle:LeadEventLog', 'ell') - ->leftJoin('ell.event', 'ellev') - ->leftJoin('ell.lead', 'el') - ->where('ellev.id = e.id') - ->andWhere( - $dq->expr()->eq('el.id', ':leadId') - ); - - $q->andWhere('e.id NOT IN('.$dq->getDQL().')') - ->setParameter('leadId', $leadId); - } - - if ($positivePathOnly) { - $q->andWhere( - $q->expr()->orX( - $q->expr()->neq( - 'e.decisionPath', - $q->expr()->literal('no') - ), - $q->expr()->isNull('e.decisionPath') + // Limit to events that aren't been executed or scheduled yet + $eventQb = $this->getEntityManager()->createQueryBuilder(); + $eventQb->select('IDENTITY(log_event.event)') + ->from(LeadEventLog::class, 'log_event') + ->where( + $eventQb->expr()->andX( + $eventQb->expr()->eq('IDENTITY(log_event.event)', 'IDENTITY(e.parent)'), + $eventQb->expr()->eq('IDENTITY(log_event.lead)', 'IDENTITY(l.lead)'), + $eventQb->expr()->eq('log_event.rotation', 'l.rotation') ) ); - } - $results = $q->getQuery()->getArrayResult(); + // Limit to events that have no parent or who's parent has already been executed + $parentQb = $this->getEntityManager()->createQueryBuilder(); + $parentQb->select('parent_log_event.id') + ->from(LeadEventLog::class, 'parent_log_event') + ->where( + $parentQb->expr()->eq('IDENTITY(parent_log_event.event)', 'IDENTITY(e.parent)'), + $parentQb->expr()->eq('IDENTITY(parent_log_event.lead)', 'IDENTITY(l.lead)'), + $parentQb->expr()->eq('parent_log_event.rotation', 'l.rotation') + ); - //group them by campaign - $events = []; - foreach ($results as $r) { - $events[$r['campaign']['id']][$r['id']] = $r; - } + $q = $this->createQueryBuilder('e', 'e.id'); + $q->select('e,c') + ->innerJoin('e.campaign', 'c') + ->innerJoin('c.leads', 'l') + ->where( + $q->expr()->andX( + $q->expr()->eq('c.isPublished', 1), + $q->expr()->eq('e.type', ':type'), + $q->expr()->eq('IDENTITY(l.lead)', ':contactId'), + $q->expr()->eq('l.manuallyRemoved', 0), + $q->expr()->notIn('e.id', $eventQb->getDQL()), + $q->expr()->orX( + $q->expr()->isNull('e.parent'), + $q->expr()->exists($parentQb->getDQL()) + ) + ) + ) + ->setParameter('type', $type) + ->setParameter('contactId', (int) $contactId); - return $events; + return $q->getQuery()->getResult(); } /** @@ -161,103 +137,6 @@ public function getEventsByParent($parentId, $decisionPath = null, $eventType = return $q->getQuery()->getArrayResult(); } - /** - * Get the top level events for a campaign. - * - * @param $id - * @param $includeDecisions - * - * @return array - */ - public function getRootLevelEvents($id, $includeDecisions = false) - { - $q = $this->getEntityManager()->createQueryBuilder(); - - $q->select('e') - ->from('MauticCampaignBundle:Event', 'e', 'e.id') - ->where( - $q->expr()->andX( - $q->expr()->eq('IDENTITY(e.campaign)', (int) $id), - $q->expr()->isNull('e.parent') - ) - ); - - if (!$includeDecisions) { - $q->andWhere( - $q->expr()->neq('e.eventType', $q->expr()->literal('decision')) - ); - } - - $results = $q->getQuery()->getArrayResult(); - - return $results; - } - - /** - * Gets ids of leads who have already triggered the event. - * - * @param $events - * @param $leadId - * - * @return array - */ - public function getEventLogLeads($events, $leadId = null) - { - $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); - - $q->select('distinct(e.lead_id)') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') - ->where( - $q->expr()->in('e.event_id', $events) - ) - ->setParameter('false', false, 'boolean'); - - if ($leadId) { - $q->andWhere( - $q->expr()->eq('e.lead_id', (int) $leadId) - ); - } - - $results = $q->execute()->fetchAll(); - - $log = []; - foreach ($results as $r) { - $log[] = $r['lead_id']; - } - - unset($results); - - return $log; - } - - /** - * Get an array of events that have been triggered by this lead. - * - * @param $leadId - * - * @return array - */ - public function getLeadTriggeredEvents($leadId) - { - $q = $this->getEntityManager()->createQueryBuilder() - ->select('e, c, l') - ->from('MauticCampaignBundle:Event', 'e') - ->join('e.campaign', 'c') - ->join('e.log', 'l'); - - //make sure the published up and down dates are good - $q->where($q->expr()->eq('IDENTITY(l.lead)', (int) $leadId)); - - $results = $q->getQuery()->getArrayResult(); - - $return = []; - foreach ($results as $r) { - $return[$r['id']] = $r; - } - - return $return; - } - /** * @param $campaignId * @@ -324,24 +203,6 @@ public function getEvents($args = []) return $events; } - /** - * @param $campaignId - * - * @return array - */ - public function getCampaignActionAndConditionEvents($campaignId) - { - $q = $this->getEntityManager()->createQueryBuilder(); - $q->select('e') - ->from('MauticCampaignBundle:Event', 'e', 'e.id') - ->where($q->expr()->eq('IDENTITY(e.campaign)', (int) $campaignId)) - ->andWhere($q->expr()->in('e.eventType', ['action', 'condition'])); - - $events = $q->getQuery()->getArrayResult(); - - return $events; - } - /** * Get the non-action log. * @@ -435,7 +296,7 @@ public function getEventLog($campaignId, $leads = [], $havingEvents = [], $exclu } /** - * Null event parents in preparation for deleting a campaign. + * Null event parents in preparation for deleI'lting a campaign. * * @param $campaignId */ @@ -548,61 +409,4 @@ protected function addSearchCommandWhereClause($q, $filter) { return $this->addStandardSearchCommandWhereClause($q, $filter); } - - /** - * @deprecated 2.13.0 to be removed in 3.0; use LeadEventLogRepository::getScheduled() instead - * - * Get a list of scheduled events. - * - * @param $campaignId - * @param bool $count - * @param int $limit - * - * @return array|bool - */ - public function getScheduledEvents($campaignId, $count = false, $limit = 0) - { - $date = new \Datetime(); - - $q = $this->getEntityManager()->createQueryBuilder() - ->from('MauticCampaignBundle:LeadEventLog', 'o'); - - $q->where( - $q->expr()->andX( - $q->expr()->eq('IDENTITY(o.campaign)', (int) $campaignId), - $q->expr()->eq('o.isScheduled', ':true'), - $q->expr()->lte('o.triggerDate', ':now') - ) - ) - ->setParameter('now', $date) - ->setParameter('true', true, 'boolean'); - - if ($count) { - $q->select('COUNT(o) as event_count'); - - $results = $results = $q->getQuery()->getArrayResult(); - $count = $results[0]['event_count']; - - return $count; - } - - $q->select('o, IDENTITY(o.lead) as lead_id, IDENTITY(o.event) AS event_id') - ->orderBy('o.triggerDate', 'DESC'); - - if ($limit) { - $q->setFirstResult(0) - ->setMaxResults($limit); - } - - $results = $q->getQuery()->getArrayResult(); - - // Organize by lead - $logs = []; - foreach ($results as $e) { - $logs[$e['lead_id']][$e['event_id']] = array_merge($e[0], ['lead_id' => $e['lead_id'], 'event_id' => $e['event_id']]); - } - unset($results); - - return $logs; - } } diff --git a/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php b/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php new file mode 100644 index 00000000000..76dd5aa9913 --- /dev/null +++ b/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php @@ -0,0 +1,264 @@ +getEntityManager()->createQueryBuilder() + ->from('MauticCampaignBundle:LeadEventLog', 'o'); + + $q->where( + $q->expr()->andX( + $q->expr()->eq('IDENTITY(o.campaign)', (int) $campaignId), + $q->expr()->eq('o.isScheduled', ':true'), + $q->expr()->lte('o.triggerDate', ':now') + ) + ) + ->setParameter('now', $date) + ->setParameter('true', true, 'boolean'); + + if ($count) { + $q->select('COUNT(o) as event_count'); + + $results = $results = $q->getQuery()->getArrayResult(); + $count = $results[0]['event_count']; + + return $count; + } + + $q->select('o, IDENTITY(o.lead) as lead_id, IDENTITY(o.event) AS event_id') + ->orderBy('o.triggerDate', 'DESC'); + + if ($limit) { + $q->setFirstResult(0) + ->setMaxResults($limit); + } + + $results = $q->getQuery()->getArrayResult(); + + // Organize by lead + $logs = []; + foreach ($results as $e) { + $logs[$e['lead_id']][$e['event_id']] = array_merge($e[0], ['lead_id' => $e['lead_id'], 'event_id' => $e['event_id']]); + } + unset($results); + + return $logs; + } + + /** + * Get array of published events based on type. + * + * @param $type + * @param array $campaigns + * @param null $leadId If included, only events that have not been triggered by the lead yet will be included + * @param bool $positivePathOnly If negative, all events including those with a negative path will be returned + * + * @return array + */ + public function getPublishedByType($type, array $campaigns = null, $leadId = null, $positivePathOnly = true) + { + $q = $this->createQueryBuilder('e') + ->select('c, e, ec, ep, ecc') + ->join('e.campaign', 'c') + ->leftJoin('e.children', 'ec') + ->leftJoin('e.parent', 'ep') + ->leftJoin('ec.campaign', 'ecc') + ->orderBy('e.order'); + + //make sure the published up and down dates are good + $expr = $this->getPublishedByDateExpression($q, 'c'); + + $expr->add( + $q->expr()->eq('e.type', ':type') + ); + + $q->where($expr) + ->setParameter('type', $type); + + if (!empty($campaigns)) { + $q->andWhere($q->expr()->in('c.id', ':campaigns')) + ->setParameter('campaigns', $campaigns); + } + + if ($leadId != null) { + // Events that aren't fired yet + $dq = $this->getEntityManager()->createQueryBuilder(); + $dq->select('ellev.id') + ->from('MauticCampaignBundle:LeadEventLog', 'ell') + ->leftJoin('ell.event', 'ellev') + ->leftJoin('ell.lead', 'el') + ->where('ellev.id = e.id') + ->andWhere( + $dq->expr()->eq('el.id', ':leadId') + ); + + $q->andWhere('e.id NOT IN('.$dq->getDQL().')') + ->setParameter('leadId', $leadId); + } + + if ($positivePathOnly) { + $q->andWhere( + $q->expr()->orX( + $q->expr()->neq( + 'e.decisionPath', + $q->expr()->literal('no') + ), + $q->expr()->isNull('e.decisionPath') + ) + ); + } + + $results = $q->getQuery()->getArrayResult(); + + //group them by campaign + $events = []; + foreach ($results as $r) { + $events[$r['campaign']['id']][$r['id']] = $r; + } + + return $events; + } + + /** + * Get the top level events for a campaign. + * + * @param $id + * @param $includeDecisions + * + * @return array + */ + public function getRootLevelEvents($id, $includeDecisions = false) + { + $q = $this->getEntityManager()->createQueryBuilder(); + + $q->select('e') + ->from('MauticCampaignBundle:Event', 'e', 'e.id') + ->where( + $q->expr()->andX( + $q->expr()->eq('IDENTITY(e.campaign)', (int) $id), + $q->expr()->isNull('e.parent') + ) + ); + + if (!$includeDecisions) { + $q->andWhere( + $q->expr()->neq('e.eventType', $q->expr()->literal('decision')) + ); + } + + $results = $q->getQuery()->getArrayResult(); + + return $results; + } + + /** + * Gets ids of leads who have already triggered the event. + * + * @param $events + * @param $leadId + * + * @return array + */ + public function getEventLogLeads($events, $leadId = null) + { + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $q->select('distinct(e.lead_id)') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') + ->where( + $q->expr()->in('e.event_id', $events) + ) + ->setParameter('false', false, 'boolean'); + + if ($leadId) { + $q->andWhere( + $q->expr()->eq('e.lead_id', (int) $leadId) + ); + } + + $results = $q->execute()->fetchAll(); + + $log = []; + foreach ($results as $r) { + $log[] = $r['lead_id']; + } + + unset($results); + + return $log; + } + + /** + * Get an array of events that have been triggered by this lead. + * + * @param $leadId + * + * @return array + */ + public function getLeadTriggeredEvents($leadId) + { + $q = $this->getEntityManager()->createQueryBuilder() + ->select('e, c, l') + ->from('MauticCampaignBundle:Event', 'e') + ->join('e.campaign', 'c') + ->join('e.log', 'l'); + + //make sure the published up and down dates are good + $q->where($q->expr()->eq('IDENTITY(l.lead)', (int) $leadId)); + + $results = $q->getQuery()->getArrayResult(); + + $return = []; + foreach ($results as $r) { + $return[$r['id']] = $r; + } + + return $return; + } + + /** + * @param $campaignId + * + * @return array + */ + public function getCampaignActionAndConditionEvents($campaignId) + { + $q = $this->getEntityManager()->createQueryBuilder(); + $q->select('e') + ->from('MauticCampaignBundle:Event', 'e', 'e.id') + ->where($q->expr()->eq('IDENTITY(e.campaign)', (int) $campaignId)) + ->andWhere($q->expr()->in('e.eventType', ['action', 'condition'])); + + $events = $q->getQuery()->getArrayResult(); + + return $events; + } +} diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index bcf0c6d8783..e08b94a0c52 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -105,16 +105,6 @@ public function findLogByContactId($id) return $this->logs->get($this->logContactXref[$id]); } - /** - * Check if an event is applicable. - * - * @param $eventType - */ - public function checkContext($eventType) - { - return strtolower($eventType) === strtolower($this->event->getType()); - } - private function extractContacts() { /** @var LeadEventLog $log */ diff --git a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php index e3877cd6198..3525bda3b3c 100644 --- a/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignExecutionEvent.php @@ -22,6 +22,9 @@ */ class CampaignExecutionEvent extends Event { + use EventArrayTrait; + use ContextTrait; + /** * @var Lead */ @@ -32,11 +35,6 @@ class CampaignExecutionEvent extends Event */ protected $event; - /** - * @var array - */ - protected $config; - /** * @var array */ @@ -88,7 +86,6 @@ public function __construct(array $args, $result, LeadEventLog $log = null) { $this->lead = $args['lead']; $this->event = $args['event']; - $this->config = $args['event']['properties']; $this->eventDetails = $args['eventDetails']; $this->systemTriggered = $args['systemTriggered']; $this->eventSettings = $args['eventSettings']; @@ -130,7 +127,7 @@ public function getLeadFields() */ public function getEvent() { - return $this->event; + return ($this->event instanceof \Mautic\CampaignBundle\Entity\Event) ? $this->getEventArray($this->event) : $this->event; } /** @@ -138,7 +135,7 @@ public function getEvent() */ public function getConfig() { - return $this->config; + return $this->getEvent()['properties']; } /** @@ -235,16 +232,6 @@ public function wasLogUpdatedByListener() return $this->logUpdatedByListener; } - /** - * Check if an event is applicable. - * - * @param $eventType - */ - public function checkContext($eventType) - { - return strtolower($eventType) == strtolower($this->event['type']); - } - /** * @param $channel * @param null $channelId diff --git a/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php b/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php index b4ee6e5426e..7132d398bfe 100644 --- a/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignScheduledEvent.php @@ -21,6 +21,8 @@ */ class CampaignScheduledEvent extends Event { + use EventArrayTrait; + /** * @var \Mautic\LeadBundle\Entity\Lead */ @@ -31,11 +33,6 @@ class CampaignScheduledEvent extends Event */ protected $event; - /** - * @var array - */ - protected $config; - /** * @var array */ @@ -71,7 +68,6 @@ public function __construct(array $args, LeadEventLog $log = null) { $this->lead = $args['lead']; $this->event = $args['event']; - $this->config = $args['event']['properties']; $this->eventDetails = $args['eventDetails']; $this->systemTriggered = $args['systemTriggered']; $this->dateScheduled = $args['dateScheduled']; @@ -93,7 +89,7 @@ public function getLead() */ public function getEvent() { - return $this->event; + return ($this->event instanceof \Mautic\CampaignBundle\Entity\Event) ? $this->getEventArray($this->event) : $this->event; } /** @@ -101,7 +97,7 @@ public function getEvent() */ public function getConfig() { - return $this->config; + return $this->getEvent()['properties']; } /** diff --git a/app/bundles/CampaignBundle/Event/ConditionEvent.php b/app/bundles/CampaignBundle/Event/ConditionEvent.php new file mode 100644 index 00000000000..43a4e6e936d --- /dev/null +++ b/app/bundles/CampaignBundle/Event/ConditionEvent.php @@ -0,0 +1,135 @@ +eventConfig = $config; + $this->eventLog = $log; + + // @deprecated support for pre 2.13.0; to be removed in 3.0 + parent::__construct( + [ + 'eventSettings' => $config->getConfig(), + 'eventDetails' => null, + 'event' => $log->getEvent(), + 'lead' => $log->getLead(), + 'systemTriggered' => $log->getSystemTriggered(), + ], + null, + $log + ); + } + + /** + * @return AbstractEventAccessor + */ + public function getEventConfig() + { + return $this->eventConfig; + } + + /** + * @return LeadEventLog + */ + public function getLog() + { + return $this->eventLog; + } + + /** + * Pass this condition. + */ + public function pass() + { + $this->passed = true; + } + + /** + * Fail this condition. + */ + public function fail() + { + $this->passed = false; + } + + /** + * @return bool + */ + public function wasConditionSatisfied() + { + return $this->passed; + } + + /** + * @param $channel + * @param null $channelId + */ + public function setChannel($channel, $channelId = null) + { + $this->log->setChannel($this->channel) + ->setChannelId($this->channelId); + } + + /** + * @deprecated 2.13.0 to be removed in 3.0; BC support + * + * @return bool + */ + public function getResult() + { + return $this->passed; + } + + /** + * @deprecated 2.13.0 to be removed in 3.0; BC support + * + * @param $result + * + * @return $this + */ + public function setResult($result) + { + $this->passed = (bool) $result; + + return $this; + } +} diff --git a/app/bundles/CampaignBundle/Event/ContextTrait.php b/app/bundles/CampaignBundle/Event/ContextTrait.php new file mode 100644 index 00000000000..dc2c724fa65 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/ContextTrait.php @@ -0,0 +1,31 @@ +event) { + return false; + } + + $type = ($this->event instanceof \Mautic\CampaignBundle\Entity\Event) ? $this->event->getType() : $this->event['type']; + + return strtolower($eventType) == strtolower($type); + } +} diff --git a/app/bundles/CampaignBundle/Event/DecisionEvent.php b/app/bundles/CampaignBundle/Event/DecisionEvent.php index c572604fae7..e45095bf631 100644 --- a/app/bundles/CampaignBundle/Event/DecisionEvent.php +++ b/app/bundles/CampaignBundle/Event/DecisionEvent.php @@ -11,6 +11,133 @@ namespace Mautic\CampaignBundle\Event; -class DecisionEvent +use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Symfony\Component\EventDispatcher\Event; + +class DecisionEvent extends CampaignExecutionEvent { + use ContextTrait; + + /** + * @var AbstractEventAccessor + */ + private $eventConfig; + + /** + * @var LeadEventLog + */ + private $eventLog; + + /** + * @var mixed + */ + private $passthrough; + + /** + * @var bool + */ + private $applicable = false; + + /** + * DecisionEvent constructor. + * + * @param AbstractEventAccessor $config + * @param LeadEventLog $log + * @param $passthrough + */ + public function __construct(AbstractEventAccessor $config, LeadEventLog $log, $passthrough) + { + $this->eventConfig = $config; + $this->eventLog = $log; + $this->passthrough = $passthrough; + + // @deprecated support for pre 2.13.0; to be removed in 3.0 + parent::__construct( + [ + 'eventSettings' => $config->getConfig(), + 'eventDetails' => $passthrough, + 'event' => $log->getEvent(), + 'lead' => $log->getLead(), + 'systemTriggered' => defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED'), + 'dateScheduled' => $log->getTriggerDate(), + ], + null, + $log + ); + } + + /** + * @return AbstractEventAccessor + */ + public function getEventConfig() + { + return $this->eventConfig; + } + + /** + * @return LeadEventLog + */ + public function getLog() + { + return $this->eventLog; + } + + /** + * @return mixed + */ + public function getPassthrough() + { + return $this->passthrough; + } + + /** + * Note that this decision is a match and the child events should be executed. + */ + public function setAsApplicable() + { + $this->applicable = true; + } + + /** + * @return bool + */ + public function wasDecisionApplicable() + { + return $this->applicable; + } + + /** + * @param $channel + * @param null $channelId + */ + public function setChannel($channel, $channelId = null) + { + $this->log->setChannel($this->channel) + ->setChannelId($this->channelId); + } + + /** + * @deprecated 2.13.0 to be removed in 3.0; BC support + * + * @return bool + */ + public function getResult() + { + return $this->applicable; + } + + /** + * @deprecated 2.13.0 to be removed in 3.0; BC support + * + * @param $result + * + * @return $this + */ + public function setResult($result) + { + $this->applicable = $result; + + return $this; + } } diff --git a/app/bundles/CampaignBundle/Event/EventArrayTrait.php b/app/bundles/CampaignBundle/Event/EventArrayTrait.php index ed7cd086218..e1fc43aaf70 100644 --- a/app/bundles/CampaignBundle/Event/EventArrayTrait.php +++ b/app/bundles/CampaignBundle/Event/EventArrayTrait.php @@ -12,6 +12,8 @@ namespace Mautic\CampaignBundle\Event; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; /** * Trait EventArrayTrait. @@ -21,12 +23,24 @@ trait EventArrayTrait { /** + * @var array + */ + protected $eventArray = []; + + /** + * Used to convert entities to the old array format; tried to minimize the need for this except where needed. + * * @param Event $event * * @return array */ - private function getEventArray(Event $event) + protected function getEventArray(Event $event) { + $eventId = $event->getId(); + if (isset($this->eventArray[$eventId])) { + return $this->eventArray[$eventId]; + } + $eventArray = $event->convertToArray(); $campaign = $event->getCampaign(); @@ -36,6 +50,60 @@ private function getEventArray(Event $event) 'createdBy' => $campaign->getCreatedBy(), ]; - return $eventArray; + $eventArray['parent'] = null; + if ($parent = $event->getParent()) { + $eventArray['parent'] = $parent->convertToArray(); + $eventArray['parent']['campaign'] = $eventArray['campaign']; + } + + $eventArray['children'] = []; + if ($children = $event->getChildren()) { + /** @var Event $child */ + foreach ($children as $child) { + $childArray = $child->convertToArray(); + $childArray['parent'] =&$eventArray; + $childArray['campaign'] =&$eventArray['campaign']; + unset($childArray['children']); + + $eventArray['children'] = $childArray; + } + } + + $this->eventArray[$eventId] = $eventArray; + + return $this->eventArray[$eventId]; + } + + /** + * @param LeadEventLog $log + * + * @return array + */ + protected function getLegacyEventsArray(LeadEventLog $log) + { + $event = $log->getEvent(); + + return [ + $event->getCampaign()->getId() => [ + $this->getEventArray($event), + ], + ]; + } + + /** + * @param Event $event + * @param AbstractEventAccessor $config + * + * @return array + */ + protected function getLegacyEventsConfigArray(Event $event, AbstractEventAccessor $config) + { + return [ + $event->getEventType() => [ + $event->getType() => [ + $config->getConfig(), + ], + ], + ]; } } diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php index b4823f32f3f..fd3e8affb63 100644 --- a/app/bundles/CampaignBundle/Event/PendingEvent.php +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -19,6 +19,8 @@ class PendingEvent extends AbstractLogCollectionEvent { + use ContextTrait; + /** * @var ArrayCollection */ diff --git a/app/bundles/CampaignBundle/Event/ScheduledEvent.php b/app/bundles/CampaignBundle/Event/ScheduledEvent.php index cd4e5d162f7..6561e4579e6 100644 --- a/app/bundles/CampaignBundle/Event/ScheduledEvent.php +++ b/app/bundles/CampaignBundle/Event/ScheduledEvent.php @@ -11,17 +11,12 @@ namespace Mautic\CampaignBundle\Event; -use Doctrine\Common\Collections\ArrayCollection; -use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; class ScheduledEvent extends CampaignScheduledEvent { - /* - * @deprecated support for pre 2.13.0; to be removed in 3.0 - */ - use EventArrayTrait; + use ContextTrait; /** * @var AbstractEventAccessor @@ -29,16 +24,15 @@ class ScheduledEvent extends CampaignScheduledEvent private $eventConfig; /** - * @var ArrayCollection + * @var LeadEventLog */ private $eventLog; /** - * PendingEvent constructor. + * ScheduledEvent constructor. * * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $log + * @param LeadEventLog $log */ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) { @@ -50,7 +44,7 @@ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) [ 'eventSettings' => $config->getConfig(), 'eventDetails' => null, - 'event' => $this->getEventArray($log->getEvent()), + 'event' => $log->getEvent(), 'lead' => $log->getLead(), 'systemTriggered' => true, 'dateScheduled' => $log->getTriggerDate(), @@ -62,13 +56,13 @@ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) /** * @return AbstractEventAccessor */ - public function getConfig() + public function getEventConfig() { return $this->eventConfig; } /** - * @return ArrayCollection + * @return LeadEventLog */ public function getLog() { diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php index be3ff415c79..71303186605 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php @@ -24,7 +24,6 @@ abstract class AbstractEventAccessor protected $systemProperties = [ 'label', 'description', - 'batchEventName', 'formType', 'formTypeOptions', 'formTheme', @@ -67,14 +66,6 @@ public function getDescription() return $this->getProperty('description'); } - /** - * @return mixed - */ - public function getBatchEventName() - { - return $this->getProperty('batchEventName'); - } - /** * @return string */ diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php index f16ef421fc0..17bcc4a2a7b 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php @@ -13,4 +13,23 @@ class ActionAccessor extends AbstractEventAccessor { + /** + * ActionAccessor constructor. + * + * @param array $config + */ + public function __construct(array $config) + { + parent::__construct($config); + + $this->systemProperties[] = 'batchEventName'; + } + + /** + * @return mixed + */ + public function getBatchEventName() + { + return $this->getProperty('batchEventName'); + } } diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php index 396e454cdc7..dd3d938aace 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php @@ -11,6 +11,23 @@ namespace Mautic\CampaignBundle\EventCollector\Accessor\Event; +/** + * Class ConditionAccessor. + */ class ConditionAccessor extends AbstractEventAccessor { + public function __construct(array $config) + { + parent::__construct($config); + + $this->systemProperties[] = 'eventName'; + } + + /** + * @return mixed + */ + public function getEventName() + { + return $this->getProperty('eventName'); + } } diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php index 11d2b330da6..d5b3b421a61 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php @@ -13,4 +13,18 @@ class DecisionAccessor extends AbstractEventAccessor { + public function __construct(array $config) + { + parent::__construct($config); + + $this->systemProperties[] = 'eventName'; + } + + /** + * @return mixed + */ + public function getEventName() + { + return $this->getProperty('eventName'); + } } diff --git a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php index b1affb8088b..102427e7c1f 100644 --- a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php @@ -11,7 +11,193 @@ namespace Mautic\CampaignBundle\Executioner; +use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\EventRepository; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; +use Mautic\CampaignBundle\EventCollector\EventCollector; +use Mautic\CampaignBundle\Executioner\Event\Decision; +use Mautic\CampaignBundle\Executioner\Exception\CampaignNotExecutableException; +use Mautic\CampaignBundle\Executioner\Exception\DecisionNotApplicableException; +use Mautic\LeadBundle\Entity\Lead; +use Mautic\LeadBundle\Model\LeadModel; +use Psr\Log\LoggerInterface; + class DecisionExecutioner { //if (Event::PATH_INACTION === $event->getDecisionPath() && Event::TYPE_CONDITION !== $event->getType() && $inactionPathProhibted) { + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var LeadModel + */ + private $leadModel; + + /** + * @var Lead + */ + private $contact; + + /** + * @var array + */ + private $events; + + /** + * @var EventRepository + */ + private $eventRepository; + + /** + * @var EventExecutioner + */ + private $executioner; + + /** + * @var Decision + */ + private $decisionExecutioner; + + /** + * @var EventCollector + */ + private $collector; + + /** + * DecisionExecutioner constructor. + * + * @param LoggerInterface $logger + * @param LeadModel $leadModel + * @param EventRepository $eventRepository + * @param EventExecutioner $executioner + * @param Decision $decisionExecutioner + * @param EventCollector $collector + */ + public function __construct( + LoggerInterface $logger, + LeadModel $leadModel, + EventRepository $eventRepository, + EventExecutioner $executioner, + Decision $decisionExecutioner, + EventCollector $collector + ) { + $this->logger = $logger; + $this->leadModel = $leadModel; + $this->eventRepository = $eventRepository; + $this->executioner = $executioner; + $this->decisionExecutioner = $decisionExecutioner; + $this->collector = $collector; + } + + /** + * @param $type + * @param null $passthrough + * @param null $channel + * @param null $channelId + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + */ + public function execute($type, $passthrough = null, $channel = null, $channelId = null) + { + $this->logger->debug('CAMPAIGN: Campaign triggered for event type '.$type.'('.$channel.' / '.$channelId.')'); + + // Kept for BC support although not sure we need this + defined('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED', 1); + + try { + $this->fetchCurrentContact(); + } catch (CampaignNotExecutableException $exception) { + $this->logger->debug('CAMPAIGN: '.$exception->getMessage()); + + return; + } + + try { + $this->fetchCampaignData($type); + } catch (CampaignNotExecutableException $exception) { + $this->logger->debug('CAMPAIGN: '.$exception->getMessage()); + + return; + } + + /** @var Event $event */ + foreach ($this->events as $event) { + try { + $this->evaluateDecisionForContact($event, $passthrough, $channel, $channelId); + } catch (DecisionNotApplicableException $exception) { + $this->logger->debug('CAMPAIGN: Event ID '.$event->getId().' is not applicable ('.$exception->getMessage().')'); + + continue; + } + + $children = $event->getPositiveChildren(); + if (!$children->count()) { + $this->logger->debug('CAMPAIGN: Event ID '.$event->getId().' has no positive children'); + + continue; + } + + foreach ($children as $child) { + $this->executioner->executeForContact($child, $this->contact); + } + } + } + + /** + * @param Event $event + * @param null $passthrough + * @param null $channel + * @param null $channelId + * + * @throws DecisionNotApplicableException + * @throws Exception\CannotProcessEventException + */ + private function evaluateDecisionForContact(Event $event, $passthrough = null, $channel = null, $channelId = null) + { + $this->logger->debug('CAMPAIGN: Executing '.$event->getType().' ID '.$event->getId().' for contact ID '.$this->contact->getId()); + + // If channels do not match up, there's no need to go further + if ($channel && $event->getChannel() && $channel !== $event->getChannel()) { + throw new DecisionNotApplicableException('channels do not match'); + } + + if ($channel && $channelId && $event->getChannelId() && $channelId !== $event->getChannelId()) { + throw new DecisionNotApplicableException('channel IDs do not match for channel '.$channel); + } + + /** @var DecisionAccessor $config */ + $config = $this->collector->getEventConfig($event); + $this->decisionExecutioner->evaluateForContact($config, $event, $this->contact, $passthrough, $channel, $channelId); + } + + /** + * @throws CampaignNotExecutableException + */ + private function fetchCurrentContact() + { + $this->contact = $this->leadModel->getCurrentLead(); + if (!$this->contact instanceof Lead || !$this->contact->getId()) { + throw new CampaignNotExecutableException('Unidentifiable contact'); + } + + $this->logger->debug('CAMPAIGN: Current contact ID# '.$this->contact->getId()); + } + + /** + * @param $type + * + * @throws CampaignNotExecutableException + */ + private function fetchCampaignData($type) + { + if (!$this->events = $this->eventRepository->getContactPendingEvents($this->contact->getId(), $type)) { + throw new CampaignNotExecutableException('Contact does not have any applicable '.$type.' associations.'); + } + + $this->logger->debug('CAMPAIGN: Found '.count($this->events).' events to analyize for contact ID '.$this->contact->getId()); + } } diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index 38226d23927..4300eef64f4 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -14,10 +14,16 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\Event\ConditionEvent; +use Mautic\CampaignBundle\Event\DecisionEvent; use Mautic\CampaignBundle\Event\ExecutedEvent; use Mautic\CampaignBundle\Event\FailedEvent; use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; @@ -76,7 +82,7 @@ public function __construct( * @throws LogNotProcessedException * @throws LogPassedAndFailedException */ - public function executeEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + public function executeActionEvent(ActionAccessor $config, Event $event, ArrayCollection $logs) { // this if statement can be removed when legacy dispatcher is removed if ($customEvent = $config->getBatchEventName()) { @@ -97,7 +103,40 @@ public function executeEvent(AbstractEventAccessor $config, Event $event, ArrayC // Execute BC eventName or callback. Or support case where the listener has been converted to batchEventName but still wants to execute // eventName for BC support for plugins that could be listening to it's own custom event. - $this->legacyDispatcher->dispatchCustomEvent($config, $event, $logs, ($customEvent)); + $this->legacyDispatcher->dispatchCustomEvent($config, $logs, ($customEvent)); + } + + /** + * @param DecisionAccessor $config + * @param LeadEventLog $log + * @param $passthrough + * + * @return DecisionEvent + */ + public function dispatchDecisionEvent(DecisionAccessor $config, LeadEventLog $log, $passthrough) + { + $event = new DecisionEvent($config, $log, $passthrough); + $this->dispatcher->dispatch($config->getEventName(), $event); + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $event); + + $this->legacyDispatcher->dispatchDecisionEvent($event); + + return $event; + } + + /** + * @param ConditionAccessor $config + * @param LeadEventLog $log + * + * @return ConditionEvent + */ + public function dispatchConditionEvent(ConditionAccessor $config, LeadEventLog $log) + { + $event = new ConditionEvent($config, $log); + $this->dispatcher->dispatch($config->getEventName(), $event); + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $event); + + return $event; } /** diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index d203b383a94..47becc132c5 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -16,13 +16,16 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\FailedLeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\Event\CampaignDecisionEvent; use Mautic\CampaignBundle\Event\CampaignExecutionEvent; +use Mautic\CampaignBundle\Event\DecisionEvent; use Mautic\CampaignBundle\Event\EventArrayTrait; use Mautic\CampaignBundle\Event\ExecutedEvent; use Mautic\CampaignBundle\Event\FailedEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Factory\MauticFactory; +use Mautic\LeadBundle\Model\LeadModel; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -50,6 +53,11 @@ class LegacyEventDispatcher */ private $logger; + /** + * @var LeadModel + */ + private $leadModel; + /** * @var MauticFactory */ @@ -67,21 +75,24 @@ public function __construct( EventDispatcherInterface $dispatcher, EventScheduler $scheduler, LoggerInterface $logger, + LeadModel $leadModel, MauticFactory $factory ) { $this->dispatcher = $dispatcher; $this->scheduler = $scheduler; $this->logger = $logger; + $this->leadModel = $leadModel; $this->factory = $factory; } /** * @param AbstractEventAccessor $config - * @param Event $event * @param ArrayCollection $logs - * @param bool $wasBatchProcessed + * @param $wasBatchProcessed + * + * @throws \ReflectionException */ - public function dispatchCustomEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, $wasBatchProcessed) + public function dispatchCustomEvent(AbstractEventAccessor $config, ArrayCollection $logs, $wasBatchProcessed) { $settings = $config->getConfig(); @@ -89,19 +100,19 @@ public function dispatchCustomEvent(AbstractEventAccessor $config, Event $event, return; } - $eventArray = $this->getEventArray($event); - /** @var LeadEventLog $log */ foreach ($logs as $log) { + $this->leadModel->setSystemCurrentLead($log->getLead()); + if (isset($settings['eventName'])) { - $result = $this->dispatchEventName($settings['eventName'], $settings, $eventArray, $log); + $result = $this->dispatchEventName($settings['eventName'], $settings, $log); } else { if (!is_callable($settings['callback'])) { // No use to keep trying for the other logs as it won't ever work break; } - $result = $this->dispatchCallback($settings, $eventArray, $log); + $result = $this->dispatchCallback($settings, $log); } if (!$wasBatchProcessed) { @@ -114,12 +125,14 @@ public function dispatchCustomEvent(AbstractEventAccessor $config, Event $event, $this->dispatchFailedEvent($config, $log); - return; + continue; } $this->dispatchExecutedEvent($config, $log); } } + + $this->leadModel->setSystemCurrentLead(null); } /** @@ -140,27 +153,52 @@ public function dispatchExecutionEvents(AbstractEventAccessor $config, ArrayColl } } + /** + * @param DecisionEvent $decisionEvent + */ + public function dispatchDecisionEvent(DecisionEvent $decisionEvent) + { + if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_DECISION_TRIGGER)) { + $log = $decisionEvent->getLog(); + $event = $log->getEvent(); + + $legacyDecisionEvent = $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_DECISION_TRIGGER, + new CampaignDecisionEvent( + $log->getLead(), + $event->getType(), + $decisionEvent->getEventConfig()->getConfig(), + $this->getLegacyEventsArray($log), + $this->getLegacyEventsConfigArray($event, $decisionEvent->getEventConfig()), + 0 === $event->getOrder(), + [$log] + ) + ); + + if ($legacyDecisionEvent->wasDecisionTriggered()) { + $decisionEvent->setAsApplicable(); + } + } + } + /** * @param $eventName * @param array $settings - * @param array $eventArray * @param LeadEventLog $log * * @return bool */ - private function dispatchEventName($eventName, array $settings, array $eventArray, LeadEventLog $log) + private function dispatchEventName($eventName, array $settings, LeadEventLog $log) { @trigger_error('eventName is deprecated. Convert to using batchEventName.', E_USER_DEPRECATED); - // Create a campaign event with a default successful result $campaignEvent = new CampaignExecutionEvent( [ 'eventSettings' => $settings, - 'eventDetails' => null, // @todo fix when procesing decisions, - 'event' => $eventArray, + 'eventDetails' => null, + 'event' => $log->getEvent(), 'lead' => $log->getLead(), 'systemTriggered' => $log->getSystemTriggered(), - 'config' => $eventArray['properties'], ], null, $log @@ -185,11 +223,12 @@ private function dispatchEventName($eventName, array $settings, array $eventArra * * @throws \ReflectionException */ - private function dispatchCallback(array $settings, array $eventArray, LeadEventLog $log) + private function dispatchCallback(array $settings, LeadEventLog $log) { @trigger_error('callback is deprecated. Convert to using batchEventName.', E_USER_DEPRECATED); - $args = [ + $eventArray = $this->getEventArray($log->getEvent()); + $args = [ 'eventSettings' => $settings, 'eventDetails' => null, // @todo fix when procesing decisions, 'event' => $eventArray, diff --git a/app/bundles/CampaignBundle/Executioner/Event/Action.php b/app/bundles/CampaignBundle/Executioner/Event/Action.php index 76cf40d73d1..1076148ffac 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Action.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Action.php @@ -13,21 +13,15 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; -use Mautic\CampaignBundle\Executioner\Logger\EventLogger; -use Mautic\CampaignBundle\Helper\ChannelExtractor; -use Mautic\LeadBundle\Entity\Lead; +use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; class Action implements EventInterface { const TYPE = 'action'; - /** - * @var EventLogger - */ - private $eventLogger; - /** * @var EventDispatcher */ @@ -36,75 +30,37 @@ class Action implements EventInterface /** * Action constructor. * - * @param EventLogger $eventLogger + * @param EventDispatcher $dispatcher */ - public function __construct(EventLogger $eventLogger, EventDispatcher $dispatcher) + public function __construct(EventDispatcher $dispatcher) { - $this->eventLogger = $eventLogger; $this->dispatcher = $dispatcher; } /** * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $contacts + * @param ArrayCollection $logs * * @return mixed|void * + * @throws CannotProcessEventException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException */ - public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) { - // Ensure each contact has a log entry to prevent them from being picked up again prematurely - foreach ($contacts as $contact) { - $log = $this->getLogEntry($event, $contact); - ChannelExtractor::setChannel($log, $event, $config); - - $this->eventLogger->addToQueue($log); + /** @var LeadEventLog $firstLog */ + if (!$firstLog = $logs->first()) { + return; } - $this->eventLogger->persistQueued(); - // Execute to process the batch of contacts - $this->dispatcher->executeEvent($config, $event, $this->eventLogger->getLogs()); + $event = $firstLog->getEvent(); - // Update log entries or persist failed entries - $this->eventLogger->persist(); - } + if (Event::TYPE_ACTION !== $event->getEventType()) { + throw new CannotProcessEventException('Cannot process event ID '.$event->getId().' as an action.'); + } - /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException - */ - public function executeLogs(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) - { // Execute to process the batch of contacts - $this->dispatcher->executeEvent($config, $event, $logs); - - // Update log entries or persist failed entries - $this->eventLogger->persistCollection($logs); - } - - /** - * @param Event $event - * @param Lead $contact - * - * @return \Mautic\CampaignBundle\Entity\LeadEventLog - */ - private function getLogEntry(Event $event, Lead $contact) - { - // Create the entry - $log = $this->eventLogger->buildLogEntry($event, $contact); - - $log->setIsScheduled(false); - $log->setDateTriggered(new \DateTime()); - - $this->eventLogger->persistLog($log); - - return $log; + $this->dispatcher->executeActionEvent($config, $event, $logs); } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Condition.php b/app/bundles/CampaignBundle/Executioner/Event/Condition.php index c501dd60267..75a7ec7eac6 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Condition.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Condition.php @@ -13,47 +13,77 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; -use Mautic\CampaignBundle\Executioner\Logger\EventLogger; +use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; +use Mautic\CampaignBundle\Executioner\Exception\ConditionFailedException; +use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; class Condition implements EventInterface { const TYPE = 'condition'; - /** - * @var EventLogger - */ - private $eventLogger; - /** * @var EventDispatcher */ private $dispatcher; /** - * Action constructor. + * Condition constructor. * - * @param EventLogger $eventLogger + * @param EventDispatcher $dispatcher */ - public function __construct(EventLogger $eventLogger, EventDispatcher $dispatcher) + public function __construct(EventDispatcher $dispatcher) { - $this->eventLogger = $eventLogger; $this->dispatcher = $dispatcher; } /** * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $contacts + * @param ArrayCollection $logs * - * @return mixed|void + * @return EvaluatedContacts|mixed + * + * @throws CannotProcessEventException */ - public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) { + $evaluatedContacts = new EvaluatedContacts(); + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + try { + /* @var ConditionAccessor $config */ + $this->execute($config, $log); + $evaluatedContacts->pass($log->getLead()); + } catch (ConditionFailedException $exception) { + $evaluatedContacts->fail($log->getLead()); + $log->setNonActionPathTaken(true); + } + } + + return $evaluatedContacts; } - public function executeLogs(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + /** + * @param ConditionAccessor $config + * @param LeadEventLog $log + * + * @throws CannotProcessEventException + * @throws ConditionFailedException + */ + private function execute(ConditionAccessor $config, LeadEventLog $log) { + if (Event::TYPE_CONDITION !== $log->getEvent()->getEventType()) { + throw new CannotProcessEventException('Cannot process event ID '.$log->getEvent()->getId().' as a condition.'); + } + + $conditionEvent = $this->dispatcher->dispatchConditionEvent($config, $log); + + if (!$conditionEvent->wasConditionSatisfied()) { + throw new ConditionFailedException('evaluation failed'); + } } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Decision.php b/app/bundles/CampaignBundle/Executioner/Event/Decision.php index 2a10573265c..bc0dca556b8 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Decision.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Decision.php @@ -13,9 +13,15 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; +use Mautic\CampaignBundle\Executioner\Exception\DecisionNotApplicableException; use Mautic\CampaignBundle\Executioner\Logger\EventLogger; +use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; +use Mautic\LeadBundle\Entity\Lead; class Decision implements EventInterface { @@ -44,16 +50,72 @@ public function __construct(EventLogger $eventLogger, EventDispatcher $dispatche /** * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $contacts + * @param ArrayCollection $logs * - * @return mixed|void + * @return EvaluatedContacts|mixed + * + * @throws CannotProcessEventException + */ + public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) + { + $evaluatedContacts = new EvaluatedContacts(); + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + if (Event::TYPE_DECISION !== $log->getEvent()->getEventType()) { + throw new CannotProcessEventException('Event ID '.$log->getEvent()->getId().' is not a decision'); + } + + try { + /* @var DecisionAccessor $config */ + $this->execute($config, $log); + $evaluatedContacts->pass($log->getLead()); + } catch (DecisionNotApplicableException $exception) { + $evaluatedContacts->fail($log->getLead()); + } + } + + return $evaluatedContacts; + } + + /** + * @param DecisionAccessor $config + * @param Event $event + * @param Lead $contact + * @param null $passthrough + * @param null $channel + * @param null $channelId + * + * @throws CannotProcessEventException + * @throws DecisionNotApplicableException */ - public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts) + public function evaluateForContact(DecisionAccessor $config, Event $event, Lead $contact, $passthrough = null, $channel = null, $channelId = null) { + if (Event::TYPE_DECISION !== $event->getEventType()) { + throw new CannotProcessEventException('Cannot process event ID '.$event->getId().' as a decision.'); + } + + $log = $this->eventLogger->buildLogEntry($event, $contact); + $log->setChannel($channel) + ->setChannelId($channelId); + + $this->execute($config, $log, $passthrough); + $this->eventLogger->persistLog($log); } - public function executeLogs(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + /** + * @param DecisionAccessor $config + * @param LeadEventLog $log + * @param null $passthrough + * + * @throws DecisionNotApplicableException + */ + private function execute(DecisionAccessor $config, LeadEventLog $log, $passthrough = null) { + $decisionEvent = $this->dispatcher->dispatchDecisionEvent($config, $log, $passthrough); + + if (!$decisionEvent->wasDecisionApplicable()) { + throw new DecisionNotApplicableException('evaluation failed'); + } } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php index acb90c3422b..4953f5dd2b2 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php +++ b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php @@ -12,17 +12,15 @@ namespace Mautic\CampaignBundle\Executioner\Event; use Doctrine\Common\Collections\ArrayCollection; -use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; interface EventInterface { /** * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $contacts + * @param ArrayCollection $logs * * @return mixed */ - public function executeForContacts(AbstractEventAccessor $config, Event $event, ArrayCollection $contacts); + public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs); } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 76294a90efc..1a4a61a6647 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -13,11 +13,18 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\TypeNotFoundException; use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\Event\Action; use Mautic\CampaignBundle\Executioner\Event\Condition; use Mautic\CampaignBundle\Executioner\Event\Decision; +use Mautic\CampaignBundle\Executioner\Logger\EventLogger; +use Mautic\CampaignBundle\Executioner\Result\Counter; +use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; +use Mautic\CampaignBundle\Helper\ChannelExtractor; +use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; class EventExecutioner @@ -42,6 +49,11 @@ class EventExecutioner */ private $collector; + /** + * @var EventLogger + */ + private $eventLogger; + /** * @var LoggerInterface */ @@ -58,6 +70,7 @@ class EventExecutioner */ public function __construct( EventCollector $eventCollector, + EventLogger $eventLogger, Action $actionExecutioner, Condition $conditionExecutioner, Decision $decisionExecutioner, @@ -67,53 +80,59 @@ public function __construct( $this->conditionExecutioner = $conditionExecutioner; $this->decisionExecutioner = $decisionExecutioner; $this->collector = $eventCollector; + $this->eventLogger = $eventLogger; $this->logger = $logger; } + /** + * @param Event $event + * @param Lead $contact + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + */ + public function executeForContact(Event $event, Lead $contact) + { + $contacts = new ArrayCollection([$contact->getId() => $contact]); + + $this->executeForContacts($event, $contacts); + } + /** * @param Event $event * @param ArrayCollection $contacts + * @param Counter|null $counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException */ - public function executeForContacts(Event $event, ArrayCollection $contacts) + public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null) { - $this->logger->debug('CAMPAIGN: Executing event ID '.$event->getId()); - - if ($contacts->count()) { + if (!$contacts->count()) { $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); return; } $config = $this->collector->getEventConfig($event); + $logs = $this->generateLogsFromContacts($event, $config, $contacts); - switch ($event->getEventType()) { - case Event::TYPE_ACTION: - $this->actionExecutioner->executeForContacts($config, $event, $contacts); - break; - case Event::TYPE_CONDITION: - $this->conditionExecutioner->executeForContacts($config, $event, $contacts); - break; - case Event::TYPE_DECISION: - $this->decisionExecutioner->executeForContacts($config, $event, $contacts); - break; - default: - throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); - } + $this->executeLogs($event, $logs, $counter); } /** * @param Event $event - * @param ArrayCollection $contacts + * @param ArrayCollection $logs + * @param Counter|null $counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException */ - public function executeLogs(Event $event, ArrayCollection $logs) + public function executeLogs(Event $event, ArrayCollection $logs, Counter $counter = null) { - $this->logger->debug('CAMPAIGN: Executing event ID '.$event->getId()); + $this->logger->debug('CAMPAIGN: Executing '.$event->getType().' ID '.$event->getId()); if (!$logs->count()) { $this->logger->debug('CAMPAIGN: No logs to process for event ID '.$event->getId()); @@ -123,18 +142,213 @@ public function executeLogs(Event $event, ArrayCollection $logs) $config = $this->collector->getEventConfig($event); + if ($counter) { + $counter->advanceExecuted($logs->count()); + } + switch ($event->getEventType()) { case Event::TYPE_ACTION: - $this->actionExecutioner->executeLogs($config, $event, $logs); + $this->executeAction($config, $event, $logs, $counter); break; case Event::TYPE_CONDITION: - $this->conditionExecutioner->executeLogs($config, $event, $logs); + $this->executeCondition($config, $event, $logs, $counter); break; case Event::TYPE_DECISION: - $this->decisionExecutioner->executeLogs($config, $event, $logs); + $this->executeDecision($config, $event, $logs, $counter); break; default: throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); } } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + */ + private function executeAction(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) + { + $this->actionExecutioner->executeLogs($config, $logs); + + /** @var ArrayCollection $contacts */ + $contacts = $this->extractContactsFromLogs($logs); + + // Update and clear any pending logs + $this->eventLogger->persistCollection($logs); + + // Process conditions that are attached to this action + $this->executeContactsForConditionChildren($event, $contacts, $counter); + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + */ + private function executeCondition(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) + { + $evaluatedContacts = $this->conditionExecutioner->executeLogs($config, $logs); + + // Update and clear any pending logs + $this->eventLogger->persistCollection($logs); + + $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + */ + private function executeDecision(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) + { + $evaluatedContacts = $this->decisionExecutioner->executeLogs($config, $logs); + + // Update and clear any pending logs + $this->eventLogger->persistCollection($logs); + + $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); + } + + /** + * @param Event $event + * @param EvaluatedContacts $contacts + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + */ + private function executeContactsForDecisionPathChildren(Event $event, EvaluatedContacts $contacts, Counter $counter = null) + { + $childrenCounter = new Counter(); + $positive = $contacts->getPassed(); + if ($positive->count()) { + $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $positive->getKeys()).' passed evaluation for event ID '.$event->getId()); + + $children = $event->getPositiveChildren(); + $childrenCounter->advanceEvaluated($children->count()); + $this->executeContactsForChildren($children, $positive, $childrenCounter); + } + + $negative = $contacts->getFailed(); + if ($negative->count()) { + $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $negative->getKeys()).' failed evaluation for event ID '.$event->getId()); + + $children = $event->getNegativeChildren(); + $childrenCounter->advanceEvaluated($children->count()); + $this->executeContactsForChildren($children, $negative, $childrenCounter); + } + + if ($counter) { + $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); + $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); + } + } + + /** + * @param Event $event + * @param ArrayCollection $contacts + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + */ + private function executeContactsForConditionChildren(Event $event, ArrayCollection $contacts, Counter $counter = null) + { + $childrenCounter = new Counter(); + $conditions = $event->getChildrenByEventType(Event::TYPE_CONDITION); + $childrenCounter->advanceEvaluated($conditions->count()); + + $this->logger->debug('CAMPAIGN: Evaluating '.$conditions->count().' conditions for action ID '.$event->getId()); + + $this->executeContactsForChildren($conditions, $contacts, $childrenCounter); + + if ($counter) { + $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); + $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); + } + } + + /** + * @param ArrayCollection $children + * @param ArrayCollection $contacts + * @param Counter $childrenCounter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + */ + private function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter) + { + /** @var Event $child */ + foreach ($children as $child) { + // Ignore decisions + if (Event::TYPE_DECISION == $child->getEventType()) { + $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$child->getId().' as a decision'); + continue; + } + + $this->executeForContacts($child, $contacts, $childrenCounter); + } + } + + /** + * @param ArrayCollection $logs + * + * @return ArrayCollection + */ + private function extractContactsFromLogs(ArrayCollection $logs) + { + $contacts = new ArrayCollection(); + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $contact = $log->getLead(); + $contacts->set($contact->getId(), $contact); + } + + return $contacts; + } + + /** + * @param Event $event + * @param AbstractEventAccessor $config + * @param ArrayCollection $contacts + * + * @return ArrayCollection + */ + private function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts) + { + // Ensure each contact has a log entry to prevent them from being picked up again prematurely + foreach ($contacts as $contact) { + $log = $this->eventLogger->buildLogEntry($event, $contact); + $log->setIsScheduled(false); + $log->setDateTriggered(new \DateTime()); + + ChannelExtractor::setChannel($log, $event, $config); + + $this->eventLogger->addToQueue($log); + } + + $this->eventLogger->persistQueued(); + + return $this->eventLogger->getLogs(); + } } diff --git a/app/bundles/CampaignBundle/Executioner/Exception/CampaignNotExecutableException.php b/app/bundles/CampaignBundle/Executioner/Exception/CampaignNotExecutableException.php new file mode 100644 index 00000000000..6a8c05c4320 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Exception/CampaignNotExecutableException.php @@ -0,0 +1,16 @@ +batchLimit = $batchLimit; $this->output = ($output) ? $output : new NullOutput(); - $this->execute(); + return $this->execute(); } /** @@ -140,6 +143,8 @@ public function executeForCampaign(Campaign $campaign, $batchLimit = 100, Output * @param $contactId * @param OutputInterface|null $output * + * @return Counter + * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws NotSchedulableException @@ -151,16 +156,20 @@ public function executeForContact(Campaign $campaign, $contactId, OutputInterfac $this->output = ($output) ? $output : new NullOutput(); $this->batchLimit = null; - $this->execute(); + return $this->execute(); } /** + * @return Counter + * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws NotSchedulableException */ private function execute() { + $this->counter = new Counter(); + try { $this->prepareForExecution(); $this->executeOrScheduleEvent(); @@ -174,6 +183,8 @@ private function execute() $this->output->writeln("\n"); } } + + return $this->counter; } /** @@ -213,6 +224,7 @@ private function prepareForExecution() /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws NoContactsFound * @throws NotSchedulableException */ @@ -220,6 +232,7 @@ private function executeOrScheduleEvent() { // Use the same timestamp across all contacts processed $now = new \DateTime(); + $this->counter->advanceEventCount($this->rootEvents->count()); // Loop over contacts until the entire campaign is executed $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->batchLimit, $this->contactId); @@ -227,6 +240,7 @@ private function executeOrScheduleEvent() /** @var Event $event */ foreach ($this->rootEvents as $event) { $this->progressBar->advance($contacts->count()); + $this->counter->advanceEvaluated($contacts->count()); // Check if the event should be scheduled (let the schedulers do the debug logging) $executionDate = $this->scheduler->getExecutionDateTime($event, $now); @@ -242,7 +256,7 @@ private function executeOrScheduleEvent() } // Execute the event for the batch of contacts - $this->executioner->executeForContacts($event, $contacts); + $this->executioner->executeForContacts($event, $contacts, $this->counter); } $this->kickoffContacts->clear(); diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index 68749dd6523..02e0857d6c5 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -85,11 +85,10 @@ public function persistLog(LeadEventLog $log) /** * @param Event $event * @param null $lead - * @param bool $systemTriggered * * @return LeadEventLog */ - public function buildLogEntry(Event $event, $lead = null, $systemTriggered = false) + public function buildLogEntry(Event $event, $lead = null) { $log = new LeadEventLog(); @@ -103,7 +102,7 @@ public function buildLogEntry(Event $event, $lead = null, $systemTriggered = fal $log->setLead($lead); $log->setDateTriggered(new \DateTime()); - $log->setSystemTriggered($systemTriggered); + $log->setSystemTriggered(defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED')); return $log; } @@ -145,6 +144,10 @@ public function persistCollection(ArrayCollection $collection) $this->repo->saveEntities($collection->getValues()); $this->repo->clear(); + + // Clear queued and processed + $this->processed->clear(); + $this->queued->clear(); } /** @@ -157,7 +160,7 @@ public function persist() } $this->repo->saveEntities($this->processed->getValues()); - $this->repo->clear(); $this->processed->clear(); + $this->repo->clear(); } } diff --git a/app/bundles/CampaignBundle/Executioner/Result/Counter.php b/app/bundles/CampaignBundle/Executioner/Result/Counter.php new file mode 100644 index 00000000000..252e344e1fa --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Result/Counter.php @@ -0,0 +1,179 @@ +eventCount = $eventCount; + $this->evaluated = $evaluated; + $this->executed = $executed; + $this->totalEvaluated = $totalEvaluated; + $this->totalExecuted = $totalExecuted; + } + + /** + * @return int + */ + public function getEventCount() + { + return $this->eventCount; + } + + /** + * @param int $step + */ + public function advanceEventCount($step = 1) + { + $this->eventCount += $step; + } + + /** + * @return int + */ + public function getEvaluated() + { + return $this->evaluated; + } + + /** + * @param int $step + */ + public function advanceEvaluated($step = 1) + { + $this->evaluated += $step; + $this->totalEvaluated += $step; + } + + /** + * @return int + */ + public function getExecuted() + { + return $this->executed; + } + + /** + * @param int $step + */ + public function advanceExecuted($step = 1) + { + $this->executed += $step; + $this->totalExecuted += $step; + } + + /** + * Includes all child events (conditions, etc) evaluated. + * + * @return int + */ + public function getTotalEvaluated() + { + return $this->totalEvaluated; + } + + /** + * @param int $step + */ + public function advanceTotalEvaluated($step = 1) + { + $this->totalEvaluated += $step; + } + + /** + * Includes all child events (conditions, etc) executed. + * + * @return int + */ + public function getTotalExecuted() + { + return $this->totalExecuted; + } + + /** + * @param int $step + */ + public function advanceTotalExecuted($step = 1) + { + $this->totalExecuted += $step; + } + + /** + * BC support for pre 2.13.0 array based counts. + * + * @param mixed $offset + * + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->$$offset); + } + + /** + * BC support for pre 2.13.0 array based counts. + * + * @param mixed $offset + * + * @return mixed|null + */ + public function offsetGet($offset) + { + return (isset($this->$$offset)) ? $this->$$offset : null; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + // ignore + } + + /** + * @param mixed $offset + */ + public function offsetUnset($offset) + { + // ignore + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php b/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php new file mode 100644 index 00000000000..75d271b487d --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php @@ -0,0 +1,69 @@ +passed = new ArrayCollection(); + $this->failed = new ArrayCollection(); + } + + /** + * @param Lead $contact + */ + public function pass(Lead $contact) + { + $this->passed->set($contact->getId(), $contact); + } + + /** + * @param Lead $contact + */ + public function fail(Lead $contact) + { + $this->failed->set($contact->getId(), $contact); + } + + /** + * @return ArrayCollection|Lead[] + */ + public function getPassed() + { + return $this->passed; + } + + /** + * @return ArrayCollection|Lead[] + */ + public function getFailed() + { + return $this->failed; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index def51805918..0358ce33fe2 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -18,6 +18,7 @@ use Mautic\CampaignBundle\Entity\LeadEventLogRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Helper\ProgressBarHelper; use Psr\Log\LoggerInterface; @@ -88,6 +89,11 @@ class ScheduledExecutioner implements ExecutionerInterface */ private $scheduledEvents; + /** + * @var Counter + */ + private $counter; + /** * ScheduledExecutioner constructor. * @@ -119,7 +125,7 @@ public function __construct( * @param int $batchLimit * @param OutputInterface|null $output * - * @return mixed|void + * @return Counter|mixed * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException @@ -134,7 +140,7 @@ public function executeForCampaign(Campaign $campaign, $batchLimit = 100, Output $this->logger->debug('CAMPAIGN: Triggering scheduled events'); - $this->execute(); + return $this->execute(); } /** @@ -142,7 +148,7 @@ public function executeForCampaign(Campaign $campaign, $batchLimit = 100, Output * @param $contactId * @param OutputInterface|null $output * - * @return mixed|void + * @return Counter|mixed * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException @@ -156,10 +162,12 @@ public function executeForContact(Campaign $campaign, $contactId, OutputInterfac $this->output = ($output) ? $output : new NullOutput(); $this->batchLimit = null; - $this->execute(); + return $this->execute(); } /** + * @return Counter + * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Scheduler\Exception\NotSchedulableException @@ -167,6 +175,8 @@ public function executeForContact(Campaign $campaign, $contactId, OutputInterfac */ private function execute() { + $this->counter = new Counter(); + try { $this->prepareForExecution(); $this->executeOrRecheduleEvent(); @@ -178,6 +188,8 @@ private function execute() $this->output->writeln("\n"); } } + + return $this->counter; } /** @@ -221,6 +233,8 @@ private function executeOrRecheduleEvent() $now = new \DateTime(); foreach ($this->scheduledEvents as $eventId) { + $this->counter->advanceEventCount(); + // Loop over contacts until the entire campaign is executed $this->executeScheduled($eventId, $now); } @@ -243,10 +257,13 @@ private function executeScheduled($eventId, \DateTime $now) while ($logs->count()) { $event = $logs->first()->getEvent(); $this->progressBar->advance($logs->count()); + $this->counter->advanceEvaluated($logs->count()); + + // Validate that the schedule is still appropriate $this->validateSchedule($logs, $event, $now); // Execute if there are any that did not get rescheduled - $this->executioner->executeLogs($event, $logs); + $this->executioner->executeLogs($event, $logs, $this->counter); // Get next batch $this->scheduledContacts->clear(); diff --git a/app/bundles/CampaignBundle/Model/EventModel.php b/app/bundles/CampaignBundle/Model/EventModel.php index 9ff90c11add..f8d534de510 100644 --- a/app/bundles/CampaignBundle/Model/EventModel.php +++ b/app/bundles/CampaignBundle/Model/EventModel.php @@ -22,10 +22,9 @@ use Mautic\CampaignBundle\Event\CampaignDecisionEvent; use Mautic\CampaignBundle\Event\CampaignExecutionEvent; use Mautic\CampaignBundle\Event\CampaignScheduledEvent; -use Mautic\CoreBundle\Factory\MauticFactory; +use Mautic\CampaignBundle\Executioner\DecisionExecutioner; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; -use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Helper\ProgressBarHelper; @@ -42,16 +41,6 @@ */ class EventModel extends CommonFormModel { - /** - * @var mixed - */ - protected $batchSleepTime; - - /** - * @var mixed - */ - protected $batchCampaignSleepTime; - /** * Used in triggerEvent so that responses from recursive events are saved. * @@ -85,14 +74,9 @@ class EventModel extends CommonFormModel protected $notificationModel; /** - * @var mixed + * @var DecisionExecutioner */ - protected $scheduleTimeForFailedEvents; - - /** - * @var MauticFactory - */ - protected $factory; + private $decisionExecutioner; /** * Track triggered events to check for conditions that may be attached. @@ -104,32 +88,27 @@ class EventModel extends CommonFormModel /** * EventModel constructor. * - * @param IpLookupHelper $ipLookupHelper - * @param CoreParametersHelper $coreParametersHelper - * @param LeadModel $leadModel - * @param CampaignModel $campaignModel - * @param UserModel $userModel - * @param NotificationModel $notificationModel - * @param MauticFactory $factory + * @param IpLookupHelper $ipLookupHelper + * @param LeadModel $leadModel + * @param CampaignModel $campaignModel + * @param UserModel $userModel + * @param NotificationModel $notificationModel + * @param DecisionExecutioner $decisionExecutioner */ public function __construct( IpLookupHelper $ipLookupHelper, - CoreParametersHelper $coreParametersHelper, LeadModel $leadModel, CampaignModel $campaignModel, UserModel $userModel, NotificationModel $notificationModel, - MauticFactory $factory + DecisionExecutioner $decisionExecutioner ) { $this->ipLookupHelper = $ipLookupHelper; $this->leadModel = $leadModel; $this->campaignModel = $campaignModel; $this->userModel = $userModel; $this->notificationModel = $notificationModel; - $this->batchSleepTime = $coreParametersHelper->getParameter('mautic.batch_sleep_time'); - $this->batchCampaignSleepTime = $coreParametersHelper->getParameter('mautic.batch_campaign_sleep_time'); - $this->scheduleTimeForFailedEvents = $coreParametersHelper->getParameter('campaign_time_wait_on_event_false'); - $this->factory = $factory; + $this->decisionExecutioner = $decisionExecutioner; } /** @@ -236,7 +215,9 @@ public function deleteEvents($currentEvents, $deletedEvents) */ public function triggerEvent($type, $eventDetails = null, $channel = null, $channelId = null) { - static $leadCampaigns = [], $eventList = [], $availableEventSettings = [], $leadsEvents = [], $examinedEvents = []; + $this->decisionExecutioner->execute($type, $eventDetails, $channel, $channelId); + + return; $this->logger->debug('CAMPAIGN: Campaign triggered for event type '.$type.'('.$channel.' / '.$channelId.')'); @@ -2221,16 +2202,6 @@ public function handleCondition( */ protected function batchSleep() { - $eventSleepTime = $this->batchCampaignSleepTime ? $this->batchCampaignSleepTime : ($this->batchSleepTime ? $this->batchSleepTime : 1); - - if (empty($eventSleepTime)) { - return; - } - - if ($eventSleepTime < 1) { - usleep($eventSleepTime * 1000000); - } else { - sleep($eventSleepTime); - } + // No longer used } } diff --git a/app/bundles/LeadBundle/Tracker/ContactTracker.php b/app/bundles/LeadBundle/Tracker/ContactTracker.php index e0ea6edd959..326ec764e85 100644 --- a/app/bundles/LeadBundle/Tracker/ContactTracker.php +++ b/app/bundles/LeadBundle/Tracker/ContactTracker.php @@ -199,6 +199,15 @@ public function setTrackedContact(Lead $trackedContact) */ public function setSystemContact(Lead $lead = null) { + if (null !== $lead) { + $this->logger->addDebug("LEAD: {$lead->getId()} set as system lead."); + + $fields = $lead->getFields(); + if (empty($fields)) { + $this->hydrateCustomFieldData($lead); + } + } + $this->systemContact = $lead; } From 41d32b0a40cacb6de6d561c12d9d0d55be4e2940 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 5 Feb 2018 07:42:29 -0600 Subject: [PATCH 430/778] Schedule children of conditions and decisions --- app/bundles/CampaignBundle/Config/config.php | 1 + .../Executioner/EventExecutioner.php | 181 +++++++++--------- .../Executioner/Logger/EventLogger.php | 45 +++++ .../Executioner/ScheduledExecutioner.php | 3 +- 4 files changed, 134 insertions(+), 96 deletions(-) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 1dda4b244a9..d78be54a53f 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -359,6 +359,7 @@ 'mautic.campaign.executioner.condition', 'mautic.campaign.executioner.decision', 'monolog.logger.mautic', + 'mautic.campaign.scheduler', ], ], 'mautic.campaign.executioner.kickoff' => [ diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 1a4a61a6647..3e6ecc65c10 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -13,7 +13,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\TypeNotFoundException; use Mautic\CampaignBundle\EventCollector\EventCollector; @@ -23,7 +22,7 @@ use Mautic\CampaignBundle\Executioner\Logger\EventLogger; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; -use Mautic\CampaignBundle\Helper\ChannelExtractor; +use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; @@ -59,14 +58,26 @@ class EventExecutioner */ private $logger; + /** + * @var EventScheduler + */ + private $scheduler; + + /** + * @var \DateTime + */ + private $now; + /** * EventExecutioner constructor. * * @param EventCollector $eventCollector + * @param EventLogger $eventLogger * @param Action $actionExecutioner * @param Condition $conditionExecutioner * @param Decision $decisionExecutioner * @param LoggerInterface $logger + * @param EventScheduler $scheduler */ public function __construct( EventCollector $eventCollector, @@ -74,7 +85,8 @@ public function __construct( Action $actionExecutioner, Condition $conditionExecutioner, Decision $decisionExecutioner, - LoggerInterface $logger + LoggerInterface $logger, + EventScheduler $scheduler ) { $this->actionExecutioner = $actionExecutioner; $this->conditionExecutioner = $conditionExecutioner; @@ -82,6 +94,8 @@ public function __construct( $this->collector = $eventCollector; $this->eventLogger = $eventLogger; $this->logger = $logger; + $this->scheduler = $scheduler; + $this->now = new \DateTime(); } /** @@ -106,6 +120,7 @@ public function executeForContact(Event $event, Lead $contact) * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null) { @@ -116,7 +131,7 @@ public function executeForContacts(Event $event, ArrayCollection $contacts, Coun } $config = $this->collector->getEventConfig($event); - $logs = $this->generateLogsFromContacts($event, $config, $contacts); + $logs = $this->logger->generateLogsFromContacts($event, $config, $contacts); $this->executeLogs($event, $logs, $counter); } @@ -129,6 +144,7 @@ public function executeForContacts(Event $event, ArrayCollection $contacts, Coun * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counter = null) { @@ -161,70 +177,6 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte } } - /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - */ - private function executeAction(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) - { - $this->actionExecutioner->executeLogs($config, $logs); - - /** @var ArrayCollection $contacts */ - $contacts = $this->extractContactsFromLogs($logs); - - // Update and clear any pending logs - $this->eventLogger->persistCollection($logs); - - // Process conditions that are attached to this action - $this->executeContactsForConditionChildren($event, $contacts, $counter); - } - - /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - */ - private function executeCondition(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) - { - $evaluatedContacts = $this->conditionExecutioner->executeLogs($config, $logs); - - // Update and clear any pending logs - $this->eventLogger->persistCollection($logs); - - $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); - } - - /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - */ - private function executeDecision(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) - { - $evaluatedContacts = $this->decisionExecutioner->executeLogs($config, $logs); - - // Update and clear any pending logs - $this->eventLogger->persistCollection($logs); - - $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); - } - /** * @param Event $event * @param EvaluatedContacts $contacts @@ -233,8 +185,9 @@ private function executeDecision(AbstractEventAccessor $config, Event $event, Ar * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - private function executeContactsForDecisionPathChildren(Event $event, EvaluatedContacts $contacts, Counter $counter = null) + public function executeContactsForDecisionPathChildren(Event $event, EvaluatedContacts $contacts, Counter $counter = null) { $childrenCounter = new Counter(); $positive = $contacts->getPassed(); @@ -269,8 +222,9 @@ private function executeContactsForDecisionPathChildren(Event $event, EvaluatedC * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - private function executeContactsForConditionChildren(Event $event, ArrayCollection $contacts, Counter $counter = null) + public function executeContactsForConditionChildren(Event $event, ArrayCollection $contacts, Counter $counter = null) { $childrenCounter = new Counter(); $conditions = $event->getChildrenByEventType(Event::TYPE_CONDITION); @@ -287,6 +241,7 @@ private function executeContactsForConditionChildren(Event $event, ArrayCollecti } /** + * @param Event $event * @param ArrayCollection $children * @param ArrayCollection $contacts * @param Counter $childrenCounter @@ -294,8 +249,9 @@ private function executeContactsForConditionChildren(Event $event, ArrayCollecti * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - private function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter) + public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter) { /** @var Event $child */ foreach ($children as $child) { @@ -305,50 +261,85 @@ private function executeContactsForChildren(ArrayCollection $children, ArrayColl continue; } + $executionDate = $this->scheduler->getExecutionDateTime($child, $this->now); + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$child->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s') + ); + + if ($executionDate > $this->now) { + $this->scheduler->schedule($child, $executionDate, $contacts); + continue; + } + $this->executeForContacts($child, $contacts, $childrenCounter); } } /** - * @param ArrayCollection $logs + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param Counter|null $counter * - * @return ArrayCollection + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - private function extractContactsFromLogs(ArrayCollection $logs) + private function executeAction(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) { - $contacts = new ArrayCollection(); + $this->actionExecutioner->executeLogs($config, $logs); - /** @var LeadEventLog $log */ - foreach ($logs as $log) { - $contact = $log->getLead(); - $contacts->set($contact->getId(), $contact); - } + /** @var ArrayCollection $contacts */ + $contacts = $this->logger->extractContactsFromLogs($logs); + + // Update and clear any pending logs + $this->eventLogger->persistCollection($logs); - return $contacts; + // Process conditions that are attached to this action + $this->executeContactsForConditionChildren($event, $contacts, $counter); } /** - * @param Event $event * @param AbstractEventAccessor $config - * @param ArrayCollection $contacts + * @param Event $event + * @param ArrayCollection $logs + * @param Counter|null $counter * - * @return ArrayCollection + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - private function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts) + private function executeCondition(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) { - // Ensure each contact has a log entry to prevent them from being picked up again prematurely - foreach ($contacts as $contact) { - $log = $this->eventLogger->buildLogEntry($event, $contact); - $log->setIsScheduled(false); - $log->setDateTriggered(new \DateTime()); + $evaluatedContacts = $this->conditionExecutioner->executeLogs($config, $logs); - ChannelExtractor::setChannel($log, $event, $config); + // Update and clear any pending logs + $this->eventLogger->persistCollection($logs); - $this->eventLogger->addToQueue($log); - } + $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); + } + + /** + * @param AbstractEventAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + private function executeDecision(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) + { + $evaluatedContacts = $this->decisionExecutioner->executeLogs($config, $logs); - $this->eventLogger->persistQueued(); + // Update and clear any pending logs + $this->eventLogger->persistCollection($logs); - return $this->eventLogger->getLogs(); + $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); } } diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index 02e0857d6c5..5af75b64ad5 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -15,6 +15,8 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\Helper\ChannelExtractor; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\LeadBundle\Model\LeadModel; @@ -163,4 +165,47 @@ public function persist() $this->processed->clear(); $this->repo->clear(); } + + /** + * @param ArrayCollection $logs + * + * @return ArrayCollection + */ + public function extractContactsFromLogs(ArrayCollection $logs) + { + $contacts = new ArrayCollection(); + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $contact = $log->getLead(); + $contacts->set($contact->getId(), $contact); + } + + return $contacts; + } + + /** + * @param Event $event + * @param AbstractEventAccessor $config + * @param ArrayCollection $contacts + * + * @return ArrayCollection + */ + public function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts) + { + // Ensure each contact has a log entry to prevent them from being picked up again prematurely + foreach ($contacts as $contact) { + $log = $this->buildLogEntry($event, $contact); + $log->setIsScheduled(false); + $log->setDateTriggered(new \DateTime()); + + ChannelExtractor::setChannel($log, $event, $config); + + $this->addToQueue($log); + } + + $this->persistQueued(); + + return $this->getLogs(); + } } diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index 0358ce33fe2..eef46d5ac30 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -246,6 +246,7 @@ private function executeOrRecheduleEvent() * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException * @throws \Doctrine\ORM\Query\QueryException */ @@ -285,7 +286,7 @@ private function validateSchedule(ArrayCollection $logs, Event $event, \DateTime foreach ($logs as $key => $log) { if ($createdDate = $log->getDateTriggered()) { // Date Triggered will be when the log entry was first created so use it to compare to ensure that the event's schedule - // hasn't been changed since this even was first scheduled + // hasn't been changed since this event was first scheduled $executionDate = $this->scheduler->getExecutionDateTime($event, $now, $createdDate); $this->logger->debug( 'CAMPAIGN: Log ID# '.$log->getId(). From 2f0d42274b3f5372d9cbc618a0984fd5ecf7c606 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 5 Feb 2018 12:40:49 -0600 Subject: [PATCH 431/778] Bubble up responses to decision --- app/bundles/CampaignBundle/Config/config.php | 1 + .../Executioner/DecisionExecutioner.php | 68 +- .../Executioner/EventExecutioner.php | 24 +- .../Executioner/Logger/EventLogger.php | 36 +- .../Executioner/Result/Counter.php | 43 +- .../Executioner/Result/Responses.php | 102 + .../Executioner/Scheduler/EventScheduler.php | 16 +- .../CampaignBundle/Model/EventModel.php | 2113 ++--------------- .../Model/LegacyEventModelTrait.php | 964 ++++++++ .../CoreBundle/Entity/CommonRepository.php | 10 + 10 files changed, 1466 insertions(+), 1911 deletions(-) create mode 100644 app/bundles/CampaignBundle/Executioner/Result/Responses.php create mode 100644 app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index d78be54a53f..689edda9ec4 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -392,6 +392,7 @@ 'mautic.campaign.executioner', 'mautic.campaign.executioner.decision', 'mautic.campaign.event_collector', + 'mautic.campaign.scheduler', ], ], // @deprecated 2.13.0 for BC support; to be removed in 3.0 diff --git a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php index 102427e7c1f..1c383b0535a 100644 --- a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php @@ -11,6 +11,7 @@ namespace Mautic\CampaignBundle\Executioner; +use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\EventRepository; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; @@ -18,14 +19,14 @@ use Mautic\CampaignBundle\Executioner\Event\Decision; use Mautic\CampaignBundle\Executioner\Exception\CampaignNotExecutableException; use Mautic\CampaignBundle\Executioner\Exception\DecisionNotApplicableException; +use Mautic\CampaignBundle\Executioner\Result\Responses; +use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; use Psr\Log\LoggerInterface; class DecisionExecutioner { - //if (Event::PATH_INACTION === $event->getDecisionPath() && Event::TYPE_CONDITION !== $event->getType() && $inactionPathProhibted) { - /** * @var LoggerInterface */ @@ -66,6 +67,16 @@ class DecisionExecutioner */ private $collector; + /** + * @var EventScheduler + */ + private $scheduler; + + /** + * @var DecisionResponses + */ + private $responses; + /** * DecisionExecutioner constructor. * @@ -75,6 +86,7 @@ class DecisionExecutioner * @param EventExecutioner $executioner * @param Decision $decisionExecutioner * @param EventCollector $collector + * @param EventScheduler $scheduler */ public function __construct( LoggerInterface $logger, @@ -82,7 +94,8 @@ public function __construct( EventRepository $eventRepository, EventExecutioner $executioner, Decision $decisionExecutioner, - EventCollector $collector + EventCollector $collector, + EventScheduler $scheduler ) { $this->logger = $logger; $this->leadModel = $leadModel; @@ -90,6 +103,7 @@ public function __construct( $this->executioner = $executioner; $this->decisionExecutioner = $decisionExecutioner; $this->collector = $collector; + $this->scheduler = $scheduler; } /** @@ -98,11 +112,18 @@ public function __construct( * @param null $channel * @param null $channelId * + * @return Responses + * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ public function execute($type, $passthrough = null, $channel = null, $channelId = null) { + $this->responses = new Responses(); + $now = new \DateTime(); + $this->logger->debug('CAMPAIGN: Campaign triggered for event type '.$type.'('.$channel.' / '.$channelId.')'); // Kept for BC support although not sure we need this @@ -113,7 +134,7 @@ public function execute($type, $passthrough = null, $channel = null, $channelId } catch (CampaignNotExecutableException $exception) { $this->logger->debug('CAMPAIGN: '.$exception->getMessage()); - return; + return $this->responses; } try { @@ -121,7 +142,7 @@ public function execute($type, $passthrough = null, $channel = null, $channelId } catch (CampaignNotExecutableException $exception) { $this->logger->debug('CAMPAIGN: '.$exception->getMessage()); - return; + return $this->responses; } /** @var Event $event */ @@ -141,9 +162,42 @@ public function execute($type, $passthrough = null, $channel = null, $channelId continue; } - foreach ($children as $child) { - $this->executioner->executeForContact($child, $this->contact); + $this->executeAssociatedEvents($children, $now); + } + + // Save any changes to the contact done by the listeners + if ($this->contact->getChanges()) { + $this->leadModel->saveEntity($this->contact, false); + } + + return $this->responses; + } + + /** + * @param ArrayCollection $children + * @param \DateTime $now + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + private function executeAssociatedEvents(ArrayCollection $children, \DateTime $now) + { + /** @var Event $child */ + foreach ($children as $child) { + $executionDate = $this->scheduler->getExecutionDateTime($child, $now); + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$child->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s') + ); + + if ($executionDate > $now) { + $this->scheduler->scheduleForContact($child, $executionDate, $this->contact); + continue; } + + $this->executioner->executeForContact($child, $this->contact, $this->responses); } } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 3e6ecc65c10..58785fcb319 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -22,6 +22,7 @@ use Mautic\CampaignBundle\Executioner\Logger\EventLogger; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; +use Mautic\CampaignBundle\Executioner\Result\Responses; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; @@ -102,14 +103,18 @@ public function __construct( * @param Event $event * @param Lead $contact * + * @return ArrayCollection + * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - public function executeForContact(Event $event, Lead $contact) + public function executeForContact(Event $event, Lead $contact, Responses $responses = null) { $contacts = new ArrayCollection([$contact->getId() => $contact]); - $this->executeForContacts($event, $contacts); + $this->executeForContacts($event, $contacts, null, $responses); } /** @@ -122,7 +127,7 @@ public function executeForContact(Event $event, Lead $contact) * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null) + public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, Responses $responses = null) { if (!$contacts->count()) { $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); @@ -131,9 +136,18 @@ public function executeForContacts(Event $event, ArrayCollection $contacts, Coun } $config = $this->collector->getEventConfig($event); - $logs = $this->logger->generateLogsFromContacts($event, $config, $contacts); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts); $this->executeLogs($event, $logs, $counter); + + if ($responses) { + // Extract responses + $responses->setFromLogs($logs); + } + + // Save updated log entries and clear from memory + $this->eventLogger->persistCollection($logs) + ->clear(); } /** @@ -292,7 +306,7 @@ private function executeAction(AbstractEventAccessor $config, Event $event, Arra $this->actionExecutioner->executeLogs($config, $logs); /** @var ArrayCollection $contacts */ - $contacts = $this->logger->extractContactsFromLogs($logs); + $contacts = $this->eventLogger->extractContactsFromLogs($logs); // Update and clear any pending logs $this->eventLogger->persistCollection($logs); diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index 5af75b64ad5..b052a2584db 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -137,33 +137,57 @@ public function getLogs() /** * @param ArrayCollection $collection + * + * @return $this */ public function persistCollection(ArrayCollection $collection) { if (!$collection->count()) { - return; + return $this; } $this->repo->saveEntities($collection->getValues()); - $this->repo->clear(); - // Clear queued and processed - $this->processed->clear(); - $this->queued->clear(); + return $this; + } + + /** + * @param ArrayCollection $collection + * + * @return $this + */ + public function clearCollection(ArrayCollection $collection) + { + $this->repo->detachEntities($collection->getValues()); + + return $this; } /** * Persist processed entities after they've been updated. + * + * @return $this */ public function persist() { if (!$this->processed->count()) { - return; + return $this; } $this->repo->saveEntities($this->processed->getValues()); + + return $this; + } + + /** + * @return $this + */ + public function clear() + { $this->processed->clear(); $this->repo->clear(); + + return $this; } /** diff --git a/app/bundles/CampaignBundle/Executioner/Result/Counter.php b/app/bundles/CampaignBundle/Executioner/Result/Counter.php index 252e344e1fa..20845fe3cca 100644 --- a/app/bundles/CampaignBundle/Executioner/Result/Counter.php +++ b/app/bundles/CampaignBundle/Executioner/Result/Counter.php @@ -11,7 +11,7 @@ namespace Mautic\CampaignBundle\Executioner\Result; -class Counter implements \ArrayAccess +class Counter { /** * @var int @@ -135,45 +135,4 @@ public function advanceTotalExecuted($step = 1) { $this->totalExecuted += $step; } - - /** - * BC support for pre 2.13.0 array based counts. - * - * @param mixed $offset - * - * @return bool - */ - public function offsetExists($offset) - { - return isset($this->$$offset); - } - - /** - * BC support for pre 2.13.0 array based counts. - * - * @param mixed $offset - * - * @return mixed|null - */ - public function offsetGet($offset) - { - return (isset($this->$$offset)) ? $this->$$offset : null; - } - - /** - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) - { - // ignore - } - - /** - * @param mixed $offset - */ - public function offsetUnset($offset) - { - // ignore - } } diff --git a/app/bundles/CampaignBundle/Executioner/Result/Responses.php b/app/bundles/CampaignBundle/Executioner/Result/Responses.php new file mode 100644 index 00000000000..bf91cd573f0 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Result/Responses.php @@ -0,0 +1,102 @@ +setResponse($log->getEvent(), $log->getMetadata()); + } + } + + /** + * @param Event $event + * @param $response + */ + public function setResponse(Event $event, $response) + { + switch ($event->getEventType()) { + case Event::TYPE_ACTION: + if (!isset($this->actionResponses[$event->getType()])) { + $this->actionResponses[$event->getType()] = []; + } + $this->actionResponses[$event->getType()][$event->getId()] = $response; + break; + case Event::TYPE_CONDITION: + if (!isset($this->conditionResponses[$event->getType()])) { + $this->conditionResponses[$event->getType()] = []; + } + $this->conditionResponses[$event->getType()][$event->getId()] = $response; + break; + } + } + + /** + * @param null $type + * + * @return array|mixed + */ + public function getActionResponses($type = null) + { + if ($type) { + return (isset($this->actionResponses[$type])) ? $this->actionResponses[$type] : []; + } + + return $this->actionResponses; + } + + /** + * @param null $type + * + * @return array|mixed + */ + public function getConditionResponses($type = null) + { + if ($type) { + return (isset($this->conditionResponses[$type])) ? $this->conditionResponses[$type] : []; + } + + return $this->conditionResponses; + } + + /** + * @deprecated 2.13.0 to be removed in 3.0; used for BC EventModel::triggerEvent() + * + * @return array + */ + public function getResponseArray() + { + return array_merge($this->actionResponses, $this->conditionResponses); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index ce04509fcbe..b8c190c8510 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -24,6 +24,7 @@ use Mautic\CampaignBundle\Executioner\Scheduler\Mode\DateTime; use Mautic\CampaignBundle\Executioner\Scheduler\Mode\Interval; use Mautic\CoreBundle\Helper\CoreParametersHelper; +use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -92,6 +93,18 @@ public function __construct( $this->coreParametersHelper = $coreParametersHelper; } + /** + * @param Event $event + * @param \DateTime $executionDate + * @param Lead $contact + */ + public function scheduleForContact(Event $event, \DateTime $executionDate, Lead $contact) + { + $contacts = new ArrayCollection([$contact]); + + $this->schedule($event, $executionDate, $contacts); + } + /** * @param Event $event * @param ArrayCollection $contacts @@ -126,7 +139,8 @@ public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $this->dispatchBatchScheduledEvent($config, $event, $this->eventLogger->getLogs()); // Update log entries and clear from memory - $this->eventLogger->persist(); + $this->eventLogger->persist() + ->clear(); } /** diff --git a/app/bundles/CampaignBundle/Model/EventModel.php b/app/bundles/CampaignBundle/Model/EventModel.php index f8d534de510..3e5da406e25 100644 --- a/app/bundles/CampaignBundle/Model/EventModel.php +++ b/app/bundles/CampaignBundle/Model/EventModel.php @@ -11,18 +11,12 @@ namespace Mautic\CampaignBundle\Model; -use Doctrine\DBAL\DBALException; -use Doctrine\ORM\EntityNotFoundException; -use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Entity\FailedLeadEventLog; -use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; -use Mautic\CampaignBundle\Event\CampaignDecisionEvent; -use Mautic\CampaignBundle\Event\CampaignExecutionEvent; -use Mautic\CampaignBundle\Event\CampaignScheduledEvent; use Mautic\CampaignBundle\Executioner\DecisionExecutioner; +use Mautic\CampaignBundle\Executioner\KickoffExecutioner; +use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\DateTimeHelper; @@ -41,12 +35,7 @@ */ class EventModel extends CommonFormModel { - /** - * Used in triggerEvent so that responses from recursive events are saved. - * - * @var bool - */ - private $triggeredResponses = false; + use LegacyEventModelTrait; /** * @var IpLookupHelper @@ -73,27 +62,17 @@ class EventModel extends CommonFormModel */ protected $notificationModel; - /** - * @var DecisionExecutioner - */ - private $decisionExecutioner; - - /** - * Track triggered events to check for conditions that may be attached. - * - * @var array - */ - protected $triggeredEvents = []; - /** * EventModel constructor. * - * @param IpLookupHelper $ipLookupHelper - * @param LeadModel $leadModel - * @param CampaignModel $campaignModel - * @param UserModel $userModel - * @param NotificationModel $notificationModel - * @param DecisionExecutioner $decisionExecutioner + * @param IpLookupHelper $ipLookupHelper + * @param LeadModel $leadModel + * @param CampaignModel $campaignModel + * @param UserModel $userModel + * @param NotificationModel $notificationModel + * @param DecisionExecutioner $decisionExecutioner + * @param KickoffExecutioner $kickoffExecutioner + * @param ScheduledExecutioner $scheduledExecutioner */ public function __construct( IpLookupHelper $ipLookupHelper, @@ -101,14 +80,18 @@ public function __construct( CampaignModel $campaignModel, UserModel $userModel, NotificationModel $notificationModel, - DecisionExecutioner $decisionExecutioner + DecisionExecutioner $decisionExecutioner, + KickoffExecutioner $kickoffExecutioner, + ScheduledExecutioner $scheduledExecutioner ) { - $this->ipLookupHelper = $ipLookupHelper; - $this->leadModel = $leadModel; - $this->campaignModel = $campaignModel; - $this->userModel = $userModel; - $this->notificationModel = $notificationModel; - $this->decisionExecutioner = $decisionExecutioner; + $this->ipLookupHelper = $ipLookupHelper; + $this->leadModel = $leadModel; + $this->campaignModel = $campaignModel; + $this->userModel = $userModel; + $this->notificationModel = $notificationModel; + $this->decisionExecutioner = $decisionExecutioner; + $this->kickoffExecutioner = $kickoffExecutioner; + $this->scheduledExecutioner = $scheduledExecutioner; } /** @@ -204,983 +187,198 @@ public function deleteEvents($currentEvents, $deletedEvents) } /** - * Triggers an event. - * - * @param $type - * @param null $eventDetails - * @param null $channel - * @param null $channelId - * - * @return array|bool - */ - public function triggerEvent($type, $eventDetails = null, $channel = null, $channelId = null) - { - $this->decisionExecutioner->execute($type, $eventDetails, $channel, $channelId); - - return; - - $this->logger->debug('CAMPAIGN: Campaign triggered for event type '.$type.'('.$channel.' / '.$channelId.')'); - - // Skip the anonymous check to force actions to fire for subsequent triggers - $systemTriggered = defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED'); - - if (!$systemTriggered) { - defined('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED', 1); - } - - //only trigger events for anonymous users (to prevent populating full of user/company data) - /*if (!$systemTriggered && !$this->security->isAnonymous()) { - $this->logger->debug('CAMPAIGN: contact not anonymous; abort'); - - return false; - }*/ - - // get the current lead - $lead = $this->leadModel->getCurrentLead(); - - if (!$lead instanceof Lead) { - $this->logger->debug('CAMPAIGN: unidentifiable contact; abort'); - - return false; - } - - $leadId = $lead->getId(); - $this->logger->debug('CAMPAIGN: Current Lead ID# '.$leadId); - - //get the lead's campaigns so we have when the lead was added - if (empty($leadCampaigns[$leadId])) { - $leadCampaigns[$leadId] = $this->campaignModel->getLeadCampaigns($lead, true); - } - - if (empty($leadCampaigns[$leadId])) { - $this->logger->debug('CAMPAIGN: no campaigns found so abort'); - - return false; - } - - //get the list of events that match the triggering event and is in the campaigns this lead belongs to - /** @var \Mautic\CampaignBundle\Entity\EventRepository $eventRepo */ - $eventRepo = $this->getRepository(); - if (empty($eventList[$leadId][$type])) { - $eventList[$leadId][$type] = $eventRepo->getPublishedByType($type, $leadCampaigns[$leadId], $lead->getId()); - } - $events = $eventList[$leadId][$type]; - //get event settings from the bundles - if (empty($availableEventSettings)) { - $availableEventSettings = $this->campaignModel->getEvents(); - } - //make sure there are events before continuing - if (!count($availableEventSettings) || empty($events)) { - $this->logger->debug('CAMPAIGN: no events found so abort'); - - return false; - } - - //get campaign list - $campaigns = $this->campaignModel->getEntities( - [ - 'force' => [ - 'filter' => [ - [ - 'column' => 'c.id', - 'expr' => 'in', - 'value' => array_keys($events), - ], - ], - ], - 'ignore_paginator' => true, - ] - ); - - //get a list of events that has already been executed for this lead - if (empty($leadsEvents[$leadId])) { - $leadsEvents[$leadId] = $eventRepo->getLeadTriggeredEvents($leadId); - } - - if (!isset($examinedEvents[$leadId])) { - $examinedEvents[$leadId] = []; - } - - $this->triggeredResponses = []; - $logs = []; - foreach ($events as $campaignId => $campaignEvents) { - if (empty($campaigns[$campaignId])) { - $this->logger->debug('CAMPAIGN: Campaign entity for ID# '.$campaignId.' not found'); - - continue; - } - - foreach ($campaignEvents as $k => $event) { - //has this event already been examined via a parent's children? - //all events of this triggering type has to be queried since this particular event could be anywhere in the dripflow - if (in_array($event['id'], $examinedEvents[$leadId])) { - $this->logger->debug('CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' already processed this round'); - continue; - } - $examinedEvents[$leadId][] = $event['id']; - - //check to see if this has been fired sequentially - if (!empty($event['parent'])) { - if (!isset($leadsEvents[$leadId][$event['parent']['id']])) { - //this event has a parent that has not been triggered for this lead so break out - $this->logger->debug( - 'CAMPAIGN: parent (ID# '.$event['parent']['id'].') for ID# '.$event['id'] - .' has not been triggered yet or was triggered with this batch' - ); - continue; - } - $parentLog = $leadsEvents[$leadId][$event['parent']['id']]['log'][0]; - - if ($parentLog['isScheduled']) { - //this event has a parent that is scheduled and thus not triggered - $this->logger->debug( - 'CAMPAIGN: parent (ID# '.$event['parent']['id'].') for ID# '.$event['id'] - .' has not been triggered yet because it\'s scheduled' - ); - continue; - } else { - $parentTriggeredDate = $parentLog['dateTriggered']; - } - } else { - $parentTriggeredDate = new \DateTime(); - } - - if (isset($availableEventSettings[$event['eventType']][$type])) { - $decisionEventSettings = $availableEventSettings[$event['eventType']][$type]; - } else { - // Not found maybe it's no longer available? - $this->logger->debug('CAMPAIGN: '.$type.' does not exist. (#'.$event['id'].')'); - - continue; - } - - //check the callback function for the event to make sure it even applies based on its settings - if (!$response = $this->invokeEventCallback($event, $decisionEventSettings, $lead, $eventDetails, $systemTriggered)) { - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' callback check failed with a response of '.var_export( - $response, - true - ) - ); - - continue; - } - - if (!empty($event['children'])) { - $this->logger->debug('CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' has children'); - - $childrenTriggered = false; - foreach ($event['children'] as $child) { - if (isset($leadsEvents[$leadId][$child['id']])) { - //this child event has already been fired for this lead so move on to the next event - $this->logger->debug('CAMPAIGN: '.ucfirst($child['eventType']).' ID# '.$child['id'].' already triggered'); - continue; - } elseif ($child['eventType'] == 'decision') { - //hit a triggering type event so move on - $this->logger->debug('CAMPAIGN: ID# '.$child['id'].' is a decision'); - - continue; - } elseif ($child['decisionPath'] == 'no') { - // non-action paths should not be processed by this because the contact already took action in order to get here - $childrenTriggered = true; - $this->logger->debug('CAMPAIGN: '.ucfirst($child['eventType']).' ID# '.$child['id'].' has a decision path of no'); - - continue; - } else { - $this->logger->debug('CAMPAIGN: '.ucfirst($child['eventType']).' ID# '.$child['id'].' is being processed'); - } - - //store in case a child was pulled with events - $examinedEvents[$leadId][] = $child['id']; - - if ($this->executeEvent($child, $campaigns[$campaignId], $lead, $availableEventSettings, false, $parentTriggeredDate)) { - $childrenTriggered = true; - } - } - - if ($childrenTriggered) { - $this->logger->debug('CAMPAIGN: Decision ID# '.$event['id'].' successfully executed and logged.'); - - //a child of this event was triggered or scheduled so make not of the triggering event in the log - $log = $this->getLogEntity($event['id'], $campaigns[$campaignId], $lead, null, $systemTriggered); - $log->setChannel($channel) - ->setChannelId($channelId); - $logs[] = $log; - } else { - $this->logger->debug('CAMPAIGN: Decision not logged'); - } - } else { - $this->logger->debug('CAMPAIGN: No children for this event.'); - } - } - - $this->triggerConditions($campaigns[$campaignId]); - } - - if (count($logs)) { - $this->getLeadEventLogRepository()->saveEntities($logs); - } - - if ($lead->getChanges()) { - $this->leadModel->saveEntity($lead, false); - } - - if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_DECISION_TRIGGER)) { - $this->dispatcher->dispatch( - CampaignEvents::ON_EVENT_DECISION_TRIGGER, - new CampaignDecisionEvent($lead, $type, $eventDetails, $events, $availableEventSettings, false, $logs) - ); - } - - $actionResponses = $this->triggeredResponses; - $this->triggeredResponses = false; - - return $actionResponses; - } - - /** - * Trigger the root level action(s) in campaign(s). + * Find and trigger the negative events, i.e. the events with a no decision path. * * @param Campaign $campaign - * @param $totalEventCount + * @param int $totalEventCount * @param int $limit * @param bool $max * @param OutputInterface $output - * @param int|null $leadId * @param bool|false $returnCounts If true, returns array of counters * * @return int */ - public function triggerStartingEvents( - Campaign $campaign, - &$totalEventCount, + public function triggerNegativeEvents( + $campaign, + &$totalEventCount = 0, $limit = 100, $max = false, OutputInterface $output = null, - $leadId = null, $returnCounts = false ) { defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - $campaignId = $campaign->getId(); - - $this->logger->debug('CAMPAIGN: Triggering starting events'); + $this->logger->debug('CAMPAIGN: Triggering negative events'); + $campaignId = $campaign->getId(); $repo = $this->getRepository(); $campaignRepo = $this->getCampaignRepository(); $logRepo = $this->getLeadEventLogRepository(); - if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_DECISION_TRIGGER)) { - // Include decisions if there are listeners - $events = $repo->getRootLevelEvents($campaignId, true, true); + // Get events to avoid large number of joins + $campaignEvents = $repo->getCampaignEvents($campaignId); - // Filter out decisions - $decisionChildren = []; - foreach ($events as $event) { - if ($event['eventType'] == 'decision') { - $decisionChildren[$event['id']] = $repo->getEventsByParent($event['id']); + // Get an array of events that are non-action based + $nonActionEvents = []; + $actionEvents = []; + foreach ($campaignEvents as $id => $e) { + if (!empty($e['decisionPath']) && !empty($e['parent_id']) && $campaignEvents[$e['parent_id']]['eventType'] != 'condition') { + if ($e['decisionPath'] == 'no') { + $nonActionEvents[$e['parent_id']][$id] = $e; + } elseif ($e['decisionPath'] == 'yes') { + $actionEvents[$e['parent_id']][] = $id; } } - } else { - $events = $repo->getRootLevelEvents($campaignId); } - $rootEventCount = count($events); + $this->logger->debug('CAMPAIGN: Processing the children of the following events: '.implode(', ', array_keys($nonActionEvents))); - if (empty($rootEventCount)) { - $this->logger->debug('CAMPAIGN: No events to trigger'); + if (empty($nonActionEvents)) { + // No non-action events associated with this campaign + unset($campaignEvents); - return ($returnCounts) ? [ - 'events' => 0, - 'evaluated' => 0, - 'executed' => 0, - 'totalEvaluated' => 0, - 'totalExecuted' => 0, - ] : 0; + return 0; } - // Event settings - $eventSettings = $this->campaignModel->getEvents(); - - // Get a lead count; if $leadId, then use this as a check to ensure lead is part of the campaign - $leadCount = $campaignRepo->getCampaignLeadCount($campaignId, $leadId, array_keys($events)); - - // Get a total number of events that will be processed - $totalStartingEvents = $leadCount * $rootEventCount; + // Get a count + $leadCount = $campaignRepo->getCampaignLeadCount($campaignId); if ($output) { $output->writeln( $this->translator->trans( - 'mautic.campaign.trigger.event_count', - ['%events%' => $totalStartingEvents, '%batch%' => $limit] + 'mautic.campaign.trigger.lead_count_analyzed', + ['%leads%' => $leadCount, '%batch%' => $limit] ) ); } - if (empty($leadCount)) { - $this->logger->debug('CAMPAIGN: No contacts to process'); - - unset($events); - - return ($returnCounts) ? [ - 'events' => 0, - 'evaluated' => 0, - 'executed' => 0, - 'totalEvaluated' => 0, - 'totalExecuted' => 0, - ] : 0; - } - - $evaluatedEventCount = $executedEventCount = $rootEvaluatedCount = $rootExecutedCount = 0; + $start = $leadProcessedCount = $lastRoundPercentage = $executedEventCount = $evaluatedEventCount = $negativeExecutedCount = $negativeEvaluatedCount = 0; + $nonActionEventCount = $leadCount * count($nonActionEvents); + $eventSettings = $this->campaignModel->getEvents(); + $maxCount = ($max) ? $max : $nonActionEventCount; // Try to save some memory gc_enable(); - $maxCount = ($max) ? $max : $totalStartingEvents; - - if ($output) { - $progress = ProgressBarHelper::init($output, $maxCount); - $progress->start(); - } - - $continue = true; - - $sleepBatchCount = 0; - $batchDebugCounter = 1; - - $this->logger->debug('CAMPAIGN: Processing the following events: '.implode(', ', array_keys($events))); - - while ($continue) { - $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); - - // Get list of all campaign leads; start is always zero in practice because of $pendingOnly - $campaignLeads = ($leadId) ? [$leadId] : $campaignRepo->getCampaignLeadIds($campaignId, 0, $limit, true); - - if (empty($campaignLeads)) { - // No leads found - $this->logger->debug('CAMPAIGN: No campaign contacts found.'); - - break; + if ($leadCount) { + if ($output) { + $progress = ProgressBarHelper::init($output, $maxCount); + $progress->start(); + if ($max) { + $progress->advance($totalEventCount); + } } - $leads = $this->leadModel->getEntities( - [ - 'filter' => [ - 'force' => [ - [ - 'column' => 'l.id', - 'expr' => 'in', - 'value' => $campaignLeads, - ], - ], - ], - 'orderBy' => 'l.id', - 'orderByDir' => 'asc', - 'withPrimaryCompany' => true, - 'withChannelRules' => true, - ] - ); + $sleepBatchCount = 0; + $batchDebugCounter = 1; + while ($start <= $leadCount) { + $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); - $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', array_keys($leads))); + // Get batched campaign ids + $campaignLeads = $campaignRepo->getCampaignLeads($campaignId, $start, $limit, ['cl.lead_id, cl.date_added']); - if (!count($leads)) { - // Just a precaution in case non-existent leads are lingering in the campaign leads table - $this->logger->debug('CAMPAIGN: No contact entities found.'); + $campaignLeadIds = []; + $campaignLeadDates = []; + foreach ($campaignLeads as $r) { + $campaignLeadIds[] = $r['lead_id']; + $campaignLeadDates[$r['lead_id']] = $r['date_added']; + } - break; - } + unset($campaignLeads); - /** @var \Mautic\LeadBundle\Entity\Lead $lead */ - $leadDebugCounter = 1; - foreach ($leads as $lead) { - $this->logger->debug('CAMPAIGN: Current Lead ID# '.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter); + $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignLeadIds)); - if ($rootEvaluatedCount >= $maxCount || ($max && ($rootEvaluatedCount + $rootEventCount) >= $max)) { - // Hit the max or will hit the max mid-progress for a lead - $continue = false; - $this->logger->debug('CAMPAIGN: Hit max so aborting.'); + foreach ($nonActionEvents as $parentId => $events) { + // Just a check to ensure this is an appropriate action + if ($campaignEvents[$parentId]['eventType'] == 'action') { + $this->logger->debug('CAMPAIGN: Parent event ID #'.$parentId.' is an action.'); - break; - } + continue; + } - // Set lead in case this is triggered by the system - $this->leadModel->setSystemCurrentLead($lead); + // Get only leads who have had the action prior to the decision executed + $grandParentId = $campaignEvents[$parentId]['parent_id']; - foreach ($events as $event) { - ++$rootEvaluatedCount; + // Get the lead log for this batch of leads limiting to those that have already triggered + // the decision's parent and haven't executed this level in the path yet + if ($grandParentId) { + $this->logger->debug('CAMPAIGN: Checking for contacts based on grand parent execution.'); - if ($sleepBatchCount == $limit) { - // Keep CPU down - $this->batchSleep(); - $sleepBatchCount = 0; + $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, [$grandParentId], array_keys($events), true); + $applicableLeads = array_keys($leadLog); } else { - ++$sleepBatchCount; - } + $this->logger->debug('CAMPAIGN: Checking for contacts based on exclusion due to being at root level'); - if ($event['eventType'] == 'decision') { - ++$evaluatedEventCount; - ++$totalEventCount; - - $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), - 'createdBy' => $campaign->getCreatedBy(), - ]; - - if (isset($decisionChildren[$event['id']])) { - $decisionEvent = [ - $campaignId => [ - array_merge( - $event, - ['children' => $decisionChildren[$event['id']]] - ), - ], - ]; - $decisionTriggerEvent = new CampaignDecisionEvent($lead, $event['type'], null, $decisionEvent, $eventSettings, true); - $this->dispatcher->dispatch( - CampaignEvents::ON_EVENT_DECISION_TRIGGER, - $decisionTriggerEvent + // The event has no grandparent (likely because the decision is first in the campaign) so find leads that HAVE + // already executed the events in the root level and exclude them + $havingEvents = (isset($actionEvents[$parentId])) + ? array_merge($actionEvents[$parentId], array_keys($events)) + : array_keys( + $events ); - if ($decisionTriggerEvent->wasDecisionTriggered()) { - ++$executedEventCount; - ++$rootExecutedCount; - - $this->logger->debug( - 'CAMPAIGN: Decision ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' noted as completed by event listener thus executing children.' - ); - - // Decision has already been triggered by the lead so process the associated events - $decisionLogged = false; - foreach ($decisionEvent['children'] as $childEvent) { - if ($this->executeEvent( - $childEvent, - $campaign, - $lead, - $eventSettings, - false, - null, - null, - false, - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ) - && !$decisionLogged - ) { - // Log the decision - $log = $this->getLogEntity($decisionEvent['id'], $campaign, $lead, null, true); - $log->setDateTriggered(new \DateTime()); - $log->setNonActionPathTaken(true); - $logRepo->saveEntity($log); - $this->em->detach($log); - unset($log); + $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, $havingEvents); + $unapplicableLeads = array_keys($leadLog); - $decisionLogged = true; - } - } - } + // Only use leads that are not applicable + $applicableLeads = array_diff($campaignLeadIds, $unapplicableLeads); - unset($decisionEvent); - } - } else { - if ($this->executeEvent( - $event, - $campaign, - $lead, - $eventSettings, - false, - null, - null, - false, - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ) - ) { - ++$rootExecutedCount; - } + unset($unapplicableLeads); } - unset($event); + if (empty($applicableLeads)) { + $this->logger->debug('CAMPAIGN: No events are applicable'); - if ($output && isset($progress) && $rootEvaluatedCount < $maxCount) { - $progress->setProgress($rootEvaluatedCount); + continue; } - } - // Free some RAM - $this->em->detach($lead); - unset($lead); + $this->logger->debug('CAMPAIGN: These contacts have have not gone down the positive path: '.implode(', ', $applicableLeads)); - ++$leadDebugCounter; - } + // Get the leads + $leads = $this->leadModel->getEntities( + [ + 'filter' => [ + 'force' => [ + [ + 'column' => 'l.id', + 'expr' => 'in', + 'value' => $applicableLeads, + ], + ], + ], + 'orderBy' => 'l.id', + 'orderByDir' => 'asc', + 'withPrimaryCompany' => true, + 'withChannelRules' => true, + ] + ); - $this->em->clear('Mautic\LeadBundle\Entity\Lead'); - $this->em->clear('Mautic\UserBundle\Entity\User'); + if (!count($leads)) { + // Just a precaution in case non-existent leads are lingering in the campaign leads table + $this->logger->debug('CAMPAIGN: No contact entities found.'); - unset($leads, $campaignLeads); + continue; + } - // Free some memory - gc_collect_cycles(); + // Loop over the non-actions and determine if it has been processed for this lead - $this->triggerConditions($campaign, $evaluatedEventCount, $executedEventCount, $totalEventCount); + $leadDebugCounter = 1; + /** @var \Mautic\LeadBundle\Entity\Lead $lead */ + foreach ($leads as $lead) { + ++$negativeEvaluatedCount; - ++$batchDebugCounter; - } + // Set lead for listeners + $this->leadModel->setSystemCurrentLead($lead); - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } + $this->logger->debug('CAMPAIGN: contact ID #'.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter); - $counts = [ - 'events' => $totalStartingEvents, - 'evaluated' => $rootEvaluatedCount, - 'executed' => $rootExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); + // Prevent path if lead has already gone down this path + if (!isset($leadLog[$lead->getId()]) || !array_key_exists($parentId, $leadLog[$lead->getId()])) { + // Get date to compare against + $utcDateString = ($grandParentId) ? $leadLog[$lead->getId()][$grandParentId]['date_triggered'] + : $campaignLeadDates[$lead->getId()]; - return ($returnCounts) ? $counts : $executedEventCount; - } - - /** - * @param Campaign $campaign - * @param $totalEventCount - * @param int $limit - * @param bool $max - * @param OutputInterface $output - * @param bool|false $returnCounts If true, returns array of counters - * - * @return int - * - * @throws \Doctrine\ORM\ORMException - */ - public function triggerScheduledEvents( - $campaign, - &$totalEventCount, - $limit = 100, - $max = false, - OutputInterface $output = null, - $returnCounts = false - ) { - defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - - $campaignId = $campaign->getId(); - - $this->logger->debug('CAMPAIGN: Triggering scheduled events'); - - $repo = $this->getRepository(); - - // Get a count - $totalScheduledCount = $repo->getScheduledEvents($campaignId, true); - $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); - - if ($output) { - $output->writeln( - $this->translator->trans( - 'mautic.campaign.trigger.event_count', - ['%events%' => $totalScheduledCount, '%batch%' => $limit] - ) - ); - } - - if (empty($totalScheduledCount)) { - $this->logger->debug('CAMPAIGN: No events to trigger'); - - return ($returnCounts) ? [ - 'events' => 0, - 'evaluated' => 0, - 'executed' => 0, - 'totalEvaluated' => 0, - 'totalExecuted' => 0, - ] : 0; - } - - // Get events to avoid joins - $campaignEvents = $repo->getCampaignActionAndConditionEvents($campaignId); - - // Event settings - $eventSettings = $this->campaignModel->getEvents(); - - $evaluatedEventCount = $executedEventCount = $scheduledEvaluatedCount = $scheduledExecutedCount = 0; - $maxCount = ($max) ? $max : $totalScheduledCount; - - // Try to save some memory - gc_enable(); - - if ($output) { - $progress = ProgressBarHelper::init($output, $maxCount); - $progress->start(); - if ($max) { - $progress->setProgress($totalEventCount); - } - } - - $sleepBatchCount = 0; - $batchDebugCounter = 1; - while ($scheduledEvaluatedCount < $totalScheduledCount) { - $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); - - // Get a count - $events = $repo->getScheduledEvents($campaignId, false, $limit); - - if (empty($events)) { - unset($campaignEvents, $event, $leads, $eventSettings); - - $counts = [ - 'events' => $totalScheduledCount, - 'evaluated' => $scheduledEvaluatedCount, - 'executed' => $scheduledExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } - - $leads = $this->leadModel->getEntities( - [ - 'filter' => [ - 'force' => [ - [ - 'column' => 'l.id', - 'expr' => 'in', - 'value' => array_keys($events), - ], - ], - ], - 'orderBy' => 'l.id', - 'orderByDir' => 'asc', - 'withPrimaryCompany' => true, - 'withChannelRules' => true, - ] - ); - - if (!count($leads)) { - // Just a precaution in case non-existent leads are lingering in the campaign leads table - $this->logger->debug('CAMPAIGN: No contacts entities found'); - - break; - } - - $this->logger->debug('CAMPAIGN: Processing the following contacts '.implode(', ', array_keys($events))); - $leadDebugCounter = 1; - foreach ($events as $leadId => $leadEvents) { - if (!isset($leads[$leadId])) { - $this->logger->debug('CAMPAIGN: Lead ID# '.$leadId.' not found'); - - continue; - } - - /** @var \Mautic\LeadBundle\Entity\Lead $lead */ - $lead = $leads[$leadId]; - - $this->logger->debug('CAMPAIGN: Current Lead ID# '.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter); - - // Set lead in case this is triggered by the system - $this->leadModel->setSystemCurrentLead($lead); - - $this->logger->debug('CAMPAIGN: Processing the following events for contact ID '.$leadId.': '.implode(', ', array_keys($leadEvents))); - - foreach ($leadEvents as $log) { - ++$scheduledEvaluatedCount; - - if ($sleepBatchCount == $limit) { - // Keep CPU down - $this->batchSleep(); - $sleepBatchCount = 0; - } else { - ++$sleepBatchCount; - } - - $event = $campaignEvents[$log['event_id']]; - - // Set campaign ID - $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), - 'createdBy' => $campaign->getCreatedBy(), - ]; - - // Skip If was lead was removed from campaign - // Execute event - if (empty($this->campaignModel->getRemovedLeads()[$campaign->getId()][$leadId])) { - if ($this->executeEvent( - $event, - $campaign, - $lead, - $eventSettings, - false, - null, - true, - $log['id'], - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ) - ) { - ++$scheduledExecutedCount; - } - } - - if ($max && $totalEventCount >= $max) { - unset($campaignEvents, $event, $leads, $eventSettings); - - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } - - $this->logger->debug('CAMPAIGN: Max count hit so aborting.'); - - // Hit the max, bye bye - - $counts = [ - 'events' => $totalScheduledCount, - 'evaluated' => $scheduledEvaluatedCount, - 'executed' => $scheduledExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } elseif ($output && isset($progress)) { - $currentCount = ($max) ? $totalEventCount : $evaluatedEventCount; - $progress->setProgress($currentCount); - } - } - - ++$leadDebugCounter; - } - - // Free RAM - $this->em->clear('Mautic\LeadBundle\Entity\Lead'); - $this->em->clear('Mautic\UserBundle\Entity\User'); - unset($events, $leads); - - // Free some memory - gc_collect_cycles(); - - ++$batchDebugCounter; - - $this->triggerConditions($campaign, $evaluatedEventCount, $executedEventCount, $totalEventCount); - } - - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } - - $counts = [ - 'events' => $totalScheduledCount, - 'evaluated' => $scheduledEvaluatedCount, - 'executed' => $scheduledExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } - - /** - * Find and trigger the negative events, i.e. the events with a no decision path. - * - * @param Campaign $campaign - * @param int $totalEventCount - * @param int $limit - * @param bool $max - * @param OutputInterface $output - * @param bool|false $returnCounts If true, returns array of counters - * - * @return int - */ - public function triggerNegativeEvents( - $campaign, - &$totalEventCount = 0, - $limit = 100, - $max = false, - OutputInterface $output = null, - $returnCounts = false - ) { - defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - - $this->logger->debug('CAMPAIGN: Triggering negative events'); - - $campaignId = $campaign->getId(); - $repo = $this->getRepository(); - $campaignRepo = $this->getCampaignRepository(); - $logRepo = $this->getLeadEventLogRepository(); - - // Get events to avoid large number of joins - $campaignEvents = $repo->getCampaignEvents($campaignId); - - // Get an array of events that are non-action based - $nonActionEvents = []; - $actionEvents = []; - foreach ($campaignEvents as $id => $e) { - if (!empty($e['decisionPath']) && !empty($e['parent_id']) && $campaignEvents[$e['parent_id']]['eventType'] != 'condition') { - if ($e['decisionPath'] == 'no') { - $nonActionEvents[$e['parent_id']][$id] = $e; - } elseif ($e['decisionPath'] == 'yes') { - $actionEvents[$e['parent_id']][] = $id; - } - } - } - - $this->logger->debug('CAMPAIGN: Processing the children of the following events: '.implode(', ', array_keys($nonActionEvents))); - - if (empty($nonActionEvents)) { - // No non-action events associated with this campaign - unset($campaignEvents); - - return 0; - } - - // Get a count - $leadCount = $campaignRepo->getCampaignLeadCount($campaignId); - - if ($output) { - $output->writeln( - $this->translator->trans( - 'mautic.campaign.trigger.lead_count_analyzed', - ['%leads%' => $leadCount, '%batch%' => $limit] - ) - ); - } - - $start = $leadProcessedCount = $lastRoundPercentage = $executedEventCount = $evaluatedEventCount = $negativeExecutedCount = $negativeEvaluatedCount = 0; - $nonActionEventCount = $leadCount * count($nonActionEvents); - $eventSettings = $this->campaignModel->getEvents(); - $maxCount = ($max) ? $max : $nonActionEventCount; - - // Try to save some memory - gc_enable(); - - if ($leadCount) { - if ($output) { - $progress = ProgressBarHelper::init($output, $maxCount); - $progress->start(); - if ($max) { - $progress->advance($totalEventCount); - } - } - - $sleepBatchCount = 0; - $batchDebugCounter = 1; - while ($start <= $leadCount) { - $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); - - // Get batched campaign ids - $campaignLeads = $campaignRepo->getCampaignLeads($campaignId, $start, $limit, ['cl.lead_id, cl.date_added']); - - $campaignLeadIds = []; - $campaignLeadDates = []; - foreach ($campaignLeads as $r) { - $campaignLeadIds[] = $r['lead_id']; - $campaignLeadDates[$r['lead_id']] = $r['date_added']; - } - - unset($campaignLeads); - - $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignLeadIds)); - - foreach ($nonActionEvents as $parentId => $events) { - // Just a check to ensure this is an appropriate action - if ($campaignEvents[$parentId]['eventType'] == 'action') { - $this->logger->debug('CAMPAIGN: Parent event ID #'.$parentId.' is an action.'); - - continue; - } - - // Get only leads who have had the action prior to the decision executed - $grandParentId = $campaignEvents[$parentId]['parent_id']; - - // Get the lead log for this batch of leads limiting to those that have already triggered - // the decision's parent and haven't executed this level in the path yet - if ($grandParentId) { - $this->logger->debug('CAMPAIGN: Checking for contacts based on grand parent execution.'); - - $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, [$grandParentId], array_keys($events), true); - $applicableLeads = array_keys($leadLog); - } else { - $this->logger->debug('CAMPAIGN: Checking for contacts based on exclusion due to being at root level'); - - // The event has no grandparent (likely because the decision is first in the campaign) so find leads that HAVE - // already executed the events in the root level and exclude them - $havingEvents = (isset($actionEvents[$parentId])) - ? array_merge($actionEvents[$parentId], array_keys($events)) - : array_keys( - $events - ); - $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, $havingEvents); - $unapplicableLeads = array_keys($leadLog); - - // Only use leads that are not applicable - $applicableLeads = array_diff($campaignLeadIds, $unapplicableLeads); - - unset($unapplicableLeads); - } - - if (empty($applicableLeads)) { - $this->logger->debug('CAMPAIGN: No events are applicable'); - - continue; - } - - $this->logger->debug('CAMPAIGN: These contacts have have not gone down the positive path: '.implode(', ', $applicableLeads)); - - // Get the leads - $leads = $this->leadModel->getEntities( - [ - 'filter' => [ - 'force' => [ - [ - 'column' => 'l.id', - 'expr' => 'in', - 'value' => $applicableLeads, - ], - ], - ], - 'orderBy' => 'l.id', - 'orderByDir' => 'asc', - 'withPrimaryCompany' => true, - 'withChannelRules' => true, - ] - ); - - if (!count($leads)) { - // Just a precaution in case non-existent leads are lingering in the campaign leads table - $this->logger->debug('CAMPAIGN: No contact entities found.'); - - continue; - } - - // Loop over the non-actions and determine if it has been processed for this lead - - $leadDebugCounter = 1; - /** @var \Mautic\LeadBundle\Entity\Lead $lead */ - foreach ($leads as $lead) { - ++$negativeEvaluatedCount; - - // Set lead for listeners - $this->leadModel->setSystemCurrentLead($lead); - - $this->logger->debug('CAMPAIGN: contact ID #'.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter); - - // Prevent path if lead has already gone down this path - if (!isset($leadLog[$lead->getId()]) || !array_key_exists($parentId, $leadLog[$lead->getId()])) { - // Get date to compare against - $utcDateString = ($grandParentId) ? $leadLog[$lead->getId()][$grandParentId]['date_triggered'] - : $campaignLeadDates[$lead->getId()]; - - // Convert to local DateTime - $grandParentDate = (new DateTimeHelper($utcDateString))->getLocalDateTime(); + // Convert to local DateTime + $grandParentDate = (new DateTimeHelper($utcDateString))->getLocalDateTime(); // Non-decision has not taken place yet, so cycle over each associated action to see if timing is right $eventTiming = []; @@ -1192,887 +390,168 @@ public function triggerNegativeEvents( $sleepBatchCount = 0; } else { ++$sleepBatchCount; - } - - if (isset($leadLog[$lead->getId()]) && array_key_exists($id, $leadLog[$lead->getId()])) { - $this->logger->debug('CAMPAIGN: Event (ID #'.$id.') has already been executed'); - unset($e); - - continue; - } - - if (!isset($eventSettings[$e['eventType']][$e['type']])) { - $this->logger->debug('CAMPAIGN: Event (ID #'.$id.') no longer exists'); - unset($e); - - continue; - } - - // First get the timing for all the 'non-decision' actions - $eventTiming[$id] = $this->checkEventTiming($e, $grandParentDate, true); - if ($eventTiming[$id] === true) { - // Includes events to be executed now then schedule the rest if applicable - $executeAction = true; - } - - unset($e); - } - - if (!$executeAction) { - $negativeEvaluatedCount += count($nonActionEvents); - - // Timing is not appropriate so move on to next lead - unset($eventTiming); - - continue; - } - - if ($max && ($totalEventCount + count($nonActionEvents)) >= $max) { - // Hit the max or will hit the max while mid-process for the lead - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } - - $counts = [ - 'events' => $nonActionEventCount, - 'evaluated' => $negativeEvaluatedCount, - 'executed' => $negativeExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } - - $decisionLogged = false; - - // Execute or schedule events - $this->logger->debug( - 'CAMPAIGN: Processing the following events for contact ID# '.$lead->getId().': '.implode( - ', ', - array_keys($eventTiming) - ) - ); - - foreach ($eventTiming as $id => $eventTriggerDate) { - // Set event - $event = $events[$id]; - $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), - 'createdBy' => $campaign->getCreatedBy(), - ]; - - // Set lead in case this is triggered by the system - $this->leadModel->setSystemCurrentLead($lead); - - if ($this->executeEvent( - $event, - $campaign, - $lead, - $eventSettings, - false, - null, - $eventTriggerDate, - false, - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ) - ) { - if (!$decisionLogged) { - // Log the decision - $log = $this->getLogEntity($parentId, $campaign, $lead, null, true); - $log->setDateTriggered(new \DateTime()); - $log->setNonActionPathTaken(true); - $logRepo->saveEntity($log); - $this->em->detach($log); - unset($log); - - $decisionLogged = true; - } - - ++$negativeExecutedCount; - } - - unset($utcDateString, $grandParentDate); - } - } else { - $this->logger->debug('CAMPAIGN: Decision has already been executed.'); - } - - $currentCount = ($max) ? $totalEventCount : $negativeEvaluatedCount; - if ($output && isset($progress) && $currentCount < $maxCount) { - $progress->setProgress($currentCount); - } - - ++$leadDebugCounter; - - // Save RAM - $this->em->detach($lead); - unset($lead); - } - } - - // Next batch - $start += $limit; - - // Save RAM - $this->em->clear('Mautic\LeadBundle\Entity\Lead'); - $this->em->clear('Mautic\UserBundle\Entity\User'); - - unset($leads, $campaignLeadIds, $leadLog); - - $currentCount = ($max) ? $totalEventCount : $negativeEvaluatedCount; - if ($output && isset($progress) && $currentCount < $maxCount) { - $progress->setProgress($currentCount); - } - - // Free some memory - gc_collect_cycles(); - - ++$batchDebugCounter; - } - - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } - - $this->triggerConditions($campaign, $evaluatedEventCount, $executedEventCount, $totalEventCount); - } - - $counts = [ - 'events' => $nonActionEventCount, - 'evaluated' => $negativeEvaluatedCount, - 'executed' => $negativeExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } - - /** - * @param Campaign $campaign - * @param int $evaluatedEventCount - * @param int $executedEventCount - * @param int $totalEventCount - */ - public function triggerConditions(Campaign $campaign, &$evaluatedEventCount = 0, &$executedEventCount = 0, &$totalEventCount = 0) - { - $eventSettings = $this->campaignModel->getEvents(); - $repo = $this->getRepository(); - $sleepBatchCount = 0; - $limit = 100; - - while (!empty($this->triggeredEvents)) { - // Reset the triggered events in order to be hydrated again for chained conditions/actions - $triggeredEvents = $this->triggeredEvents; - $this->triggeredEvents = []; - - foreach ($triggeredEvents as $parentId => $decisionPaths) { - foreach ($decisionPaths as $decisionPath => $contactIds) { - $typeRestriction = null; - if ('null' === $decisionPath) { - // This is an action so check if conditions are attached - $decisionPath = null; - $typeRestriction = 'condition'; - } // otherwise this should be a condition so get all children connected to the given path - - $childEvents = $repo->getEventsByParent($parentId, $decisionPath, $typeRestriction); - $this->logger->debug( - 'CAMPAIGN: Evaluating '.count($childEvents).' child event(s) to process the conditions of parent ID# '.$parentId.'.' - ); - - if (!count($childEvents)) { - continue; - } - - $batchedContactIds = array_chunk($contactIds, $limit); - foreach ($batchedContactIds as $batchDebugCounter => $contactBatch) { - ++$batchDebugCounter; // start with 1 - - if (empty($contactBatch)) { - break; - } - - $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); - - $leads = $this->leadModel->getEntities( - [ - 'filter' => [ - 'force' => [ - [ - 'column' => 'l.id', - 'expr' => 'in', - 'value' => $contactBatch, - ], - ], - ], - 'orderBy' => 'l.id', - 'orderByDir' => 'asc', - 'withPrimaryCompany' => true, - ] - ); - - $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', array_keys($leads))); - - if (!count($leads)) { - // Just a precaution in case non-existent leads are lingering in the campaign leads table - $this->logger->debug('CAMPAIGN: No contact entities found.'); - - continue; - } - - /** @var \Mautic\LeadBundle\Entity\Lead $lead */ - $leadDebugCounter = 0; - foreach ($leads as $lead) { - ++$leadDebugCounter; // start with 1 - - $this->logger->debug( - 'CAMPAIGN: Current Lead ID# '.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter - ); - - // Set lead in case this is triggered by the system - $this->leadModel->setSystemCurrentLead($lead); - - foreach ($childEvents as $childEvent) { - $this->executeEvent( - $childEvent, - $campaign, - $lead, - $eventSettings, - true, - null, - null, - false, - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ); - - if ($sleepBatchCount == $limit) { - // Keep CPU down - $this->batchSleep(); - $sleepBatchCount = 0; - } else { - ++$sleepBatchCount; - } - } - } - - // Free RAM - $this->em->clear('Mautic\LeadBundle\Entity\Lead'); - $this->em->clear('Mautic\UserBundle\Entity\User'); - unset($leads); - - // Free some memory - gc_collect_cycles(); - } - } - } - } - } - - /** - * Execute or schedule an event. Condition events are executed recursively. - * - * @param array $event - * @param Campaign $campaign - * @param Lead $lead - * @param array $eventSettings - * @param bool $allowNegative - * @param \DateTime $parentTriggeredDate - * @param \DateTime|bool $eventTriggerDate - * @param bool $logExists - * @param int $evaluatedEventCount The number of events evaluated for the current method (kickoff, negative/inaction, scheduled) - * @param int $executedEventCount The number of events successfully executed for the current method - * @param int $totalEventCount The total number of events across all methods - * - * @return bool - */ - public function executeEvent( - $event, - $campaign, - $lead, - $eventSettings = null, - $allowNegative = false, - \DateTime $parentTriggeredDate = null, - $eventTriggerDate = null, - $logExists = false, - &$evaluatedEventCount = 0, - &$executedEventCount = 0, - &$totalEventCount = 0 - ) { - ++$evaluatedEventCount; - ++$totalEventCount; - - // Get event settings if applicable - if ($eventSettings === null) { - $eventSettings = $this->campaignModel->getEvents(); - } - - // Set date timing should be compared with if applicable - if ($parentTriggeredDate === null) { - // Default to today - $parentTriggeredDate = new \DateTime(); - } - - $repo = $this->getRepository(); - $logRepo = $this->getLeadEventLogRepository(); - - if (isset($eventSettings[$event['eventType']][$event['type']])) { - $thisEventSettings = $eventSettings[$event['eventType']][$event['type']]; - } else { - $this->logger->debug( - 'CAMPAIGN: Settings not found for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - ); - unset($event); - - return false; - } - - if ($event['eventType'] == 'condition') { - $allowNegative = true; - } - - // Set campaign ID - - $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), - 'createdBy' => $campaign->getCreatedBy(), - ]; - - // Ensure properties is an array - if ($event['properties'] === null) { - $event['properties'] = []; - } elseif (!is_array($event['properties'])) { - $event['properties'] = unserialize($event['properties']); - } - - // Ensure triggerDate is a \DateTime - if ($event['triggerMode'] == 'date' && !$event['triggerDate'] instanceof \DateTime) { - $triggerDate = new DateTimeHelper($event['triggerDate']); - $event['triggerDate'] = $triggerDate->getDateTime(); - unset($triggerDate); - } - - if ($eventTriggerDate == null) { - $eventTriggerDate = $this->checkEventTiming($event, $parentTriggeredDate, $allowNegative); - } - $result = true; - - // Create/get log entry - /* @var LeadEventLog $log **/ - if ($logExists) { - if (true === $logExists) { - $log = $logRepo->findOneBy( - [ - 'lead' => $lead->getId(), - 'event' => $event['id'], - ] - ); - } else { - $log = $this->em->getReference('MauticCampaignBundle:LeadEventLog', $logExists); - } - } - - if (empty($log)) { - $log = $this->getLogEntity($event['id'], $campaign, $lead, null, !defined('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED')); - } + } - if ($eventTriggerDate instanceof \DateTime) { - ++$executedEventCount; + if (isset($leadLog[$lead->getId()]) && array_key_exists($id, $leadLog[$lead->getId()])) { + $this->logger->debug('CAMPAIGN: Event (ID #'.$id.') has already been executed'); + unset($e); - $log->setTriggerDate($eventTriggerDate); - $logRepo->saveEntity($log); + continue; + } - //lead actively triggered this event, a decision wasn't involved, or it was system triggered and a "no" path so schedule the event to be fired at the defined time - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' has timing that is not appropriate and thus scheduled for '.$eventTriggerDate->format('Y-m-d H:m:i T') - ); + if (!isset($eventSettings[$e['eventType']][$e['type']])) { + $this->logger->debug('CAMPAIGN: Event (ID #'.$id.') no longer exists'); + unset($e); - if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_SCHEDULED)) { - $args = [ - 'eventSettings' => $thisEventSettings, - 'eventDetails' => null, - 'event' => $event, - 'lead' => $lead, - 'systemTriggered' => true, - 'dateScheduled' => $eventTriggerDate, - ]; - - $scheduledEvent = new CampaignScheduledEvent($args); - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_SCHEDULED, $scheduledEvent); - unset($scheduledEvent, $args); - } - } elseif ($eventTriggerDate) { - // If log already existed, assume it was scheduled in order to not force - // Doctrine to do a query to fetch the information - $wasScheduled = (!$logExists) ? $log->getIsScheduled() : true; - - $log->setIsScheduled(false); - $log->setDateTriggered(new \DateTime()); - - try { - // Save before executing event to ensure it's not picked up again - $logRepo->saveEntity($log); - $this->logger->debug( - 'CAMPAIGN: Created log for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' prior to execution.' - ); - } catch (EntityNotFoundException $exception) { - // The lead has been likely removed from this lead/list - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' wasn\'t found: '.$exception->getMessage() - ); - - return false; - } catch (DBALException $exception) { - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' failed with DB error: '.$exception->getMessage() - ); - - return false; - } + continue; + } - // Set the channel - $this->campaignModel->setChannelFromEventProperties($log, $event, $thisEventSettings); + // First get the timing for all the 'non-decision' actions + $eventTiming[$id] = $this->checkEventTiming($e, $grandParentDate, true); + if ($eventTiming[$id] === true) { + // Includes events to be executed now then schedule the rest if applicable + $executeAction = true; + } - //trigger the action - $response = $this->invokeEventCallback($event, $thisEventSettings, $lead, null, true, $log); + unset($e); + } - // Check if the lead wasn't deleted during the event callback - if (null === $lead->getId() && $response === true) { - ++$executedEventCount; + if (!$executeAction) { + $negativeEvaluatedCount += count($nonActionEvents); - $this->logger->debug( - 'CAMPAIGN: Contact was deleted while executing '.ucfirst($event['eventType']).' ID# '.$event['id'] - ); + // Timing is not appropriate so move on to next lead + unset($eventTiming); - return true; - } + continue; + } - $eventTriggered = false; - if ($response instanceof LeadEventLog) { - $log = $response; + if ($max && ($totalEventCount + count($nonActionEvents)) >= $max) { + // Hit the max or will hit the max while mid-process for the lead + if ($output && isset($progress)) { + $progress->finish(); + $output->writeln(''); + } - // Listener handled the event and returned a log entry - $this->campaignModel->setChannelFromEventProperties($log, $event, $thisEventSettings); + $counts = [ + 'events' => $nonActionEventCount, + 'evaluated' => $negativeEvaluatedCount, + 'executed' => $negativeExecutedCount, + 'totalEvaluated' => $evaluatedEventCount, + 'totalExecuted' => $executedEventCount, + ]; + $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - ++$executedEventCount; + return ($returnCounts) ? $counts : $executedEventCount; + } - $this->logger->debug( - 'CAMPAIGN: Listener handled event for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - ); + $decisionLogged = false; - if (!$log->getIsScheduled()) { - $eventTriggered = true; - } - } elseif (($response === false || (is_array($response) && isset($response['result']) && false === $response['result'])) - && $event['eventType'] == 'action' - ) { - $result = false; - $debug = 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' failed with a response of '.var_export($response, true); - - // Something failed - if ($wasScheduled || !empty($this->scheduleTimeForFailedEvents)) { - $date = new \DateTime(); - $date->add(new \DateInterval($this->scheduleTimeForFailedEvents)); - - // Reschedule - $log->setTriggerDate($date); - - if (is_array($response)) { - $log->setMetadata($response); - } - $debug .= ' thus placed on hold '.$this->scheduleTimeForFailedEvents; + // Execute or schedule events + $this->logger->debug( + 'CAMPAIGN: Processing the following events for contact ID# '.$lead->getId().': '.implode( + ', ', + array_keys($eventTiming) + ) + ); - $metadata = $log->getMetadata(); - if (is_array($response)) { - $metadata = array_merge($metadata, $response); - } + foreach ($eventTiming as $id => $eventTriggerDate) { + // Set event + $event = $events[$id]; + $event['campaign'] = [ + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), + ]; - $reason = null; - if (isset($metadata['errors'])) { - $reason = (is_array($metadata['errors'])) ? implode('
', $metadata['errors']) : $metadata['errors']; - } elseif (isset($metadata['reason'])) { - $reason = $metadata['reason']; - } - $this->setEventStatus($log, false, $reason); - } else { - // Remove - $debug .= ' thus deleted'; - $repo->deleteEntity($log); - unset($log); - } + // Set lead in case this is triggered by the system + $this->leadModel->setSystemCurrentLead($lead); - $this->notifyOfFailure($lead, $campaign->getCreatedBy(), $campaign->getName().' / '.$event['name']); - $this->logger->debug($debug); - } else { - $this->setEventStatus($log, true); + if ($this->executeEvent( + $event, + $campaign, + $lead, + $eventSettings, + false, + null, + $eventTriggerDate, + false, + $evaluatedEventCount, + $executedEventCount, + $totalEventCount + ) + ) { + if (!$decisionLogged) { + // Log the decision + $log = $this->getLogEntity($parentId, $campaign, $lead, null, true); + $log->setDateTriggered(new \DateTime()); + $log->setNonActionPathTaken(true); + $logRepo->saveEntity($log); + $this->em->detach($log); + unset($log); - ++$executedEventCount; + $decisionLogged = true; + } - if ($response !== true) { - if ($this->triggeredResponses !== false) { - $eventTypeKey = $event['eventType']; - $typeKey = $event['type']; + ++$negativeExecutedCount; + } - if (!array_key_exists($eventTypeKey, $this->triggeredResponses) || !is_array($this->triggeredResponses[$eventTypeKey])) { - $this->triggeredResponses[$eventTypeKey] = []; + unset($utcDateString, $grandParentDate); + } + } else { + $this->logger->debug('CAMPAIGN: Decision has already been executed.'); } - if (!array_key_exists($typeKey, $this->triggeredResponses[$eventTypeKey]) - || !is_array( - $this->triggeredResponses[$eventTypeKey][$typeKey] - ) - ) { - $this->triggeredResponses[$eventTypeKey][$typeKey] = []; + $currentCount = ($max) ? $totalEventCount : $negativeEvaluatedCount; + if ($output && isset($progress) && $currentCount < $maxCount) { + $progress->setProgress($currentCount); } - $this->triggeredResponses[$eventTypeKey][$typeKey][$event['id']] = $response; - } - - $log->setMetadata($response); - } - - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' successfully executed and logged with a response of '.var_export($response, true) - ); - - $eventTriggered = true; - } - - if ($eventTriggered) { - // Collect the events that were triggered so that conditions can be handled properly - - if ('condition' === $event['eventType']) { - // Conditions will need child event processed - $decisionPath = ($response === true) ? 'yes' : 'no'; + ++$leadDebugCounter; - if (true !== $response) { - // Note that a condition took non action path so we can generate a visual stat - $log->setNonActionPathTaken(true); + // Save RAM + $this->em->detach($lead); + unset($lead); } - } else { - // Actions will need to check if conditions are attached to it - $decisionPath = 'null'; - } - - if (!isset($this->triggeredEvents[$event['id']])) { - $this->triggeredEvents[$event['id']] = []; - } - if (!isset($this->triggeredEvents[$event['id']][$decisionPath])) { - $this->triggeredEvents[$event['id']][$decisionPath] = []; - } - - $this->triggeredEvents[$event['id']][$decisionPath][] = $lead->getId(); - } - - if ($log) { - $logRepo->saveEntity($log); - } - } else { - //else do nothing - $result = false; - $this->logger->debug( - 'CAMPAIGN: Timing failed ('.gettype($eventTriggerDate).') for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# ' - .$lead->getId() - ); - } - - if (!empty($log)) { - // Detach log - $this->em->detach($log); - unset($log); - } - - unset($eventTriggerDate, $event); - - return $result; - } - - /** - * Invoke the event's callback function. - * - * @param $event - * @param $settings - * @param null $lead - * @param null $eventDetails - * @param bool $systemTriggered - * @param LeadEventLog $log - * - * @return bool|mixed - */ - public function invokeEventCallback($event, $settings, $lead = null, $eventDetails = null, $systemTriggered = false, LeadEventLog $log = null) - { - if (isset($settings['eventName'])) { - // Create a campaign event with a default successful result - $campaignEvent = new CampaignExecutionEvent( - [ - 'eventSettings' => $settings, - 'eventDetails' => $eventDetails, - 'event' => $event, - 'lead' => $lead, - 'systemTriggered' => $systemTriggered, - 'config' => $event['properties'], - ], - true, - $log - ); - - $eventName = array_key_exists('eventName', $settings) ? $settings['eventName'] : null; - - if ($eventName && $this->dispatcher->hasListeners($eventName)) { - $this->dispatcher->dispatch($eventName, $campaignEvent); - - if ($event['eventType'] !== 'decision' && $this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_EXECUTION)) { - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_EXECUTION, $campaignEvent); - } - - if ($campaignEvent->wasLogUpdatedByListener()) { - $campaignEvent->setResult($campaignEvent->getLogEntry()); } - } - if (null !== $log) { - $log->setChannel($campaignEvent->getChannel()) - ->setChannelId($campaignEvent->getChannelId()); - } + // Next batch + $start += $limit; - return $campaignEvent->getResult(); - } + // Save RAM + $this->em->clear('Mautic\LeadBundle\Entity\Lead'); + $this->em->clear('Mautic\UserBundle\Entity\User'); - /* - * @deprecated 2.0 - to be removed in 3.0; Use the new eventName method instead - */ - if (isset($settings['callback']) && is_callable($settings['callback'])) { - $args = [ - 'eventSettings' => $settings, - 'eventDetails' => $eventDetails, - 'event' => $event, - 'lead' => $lead, - 'factory' => $this->factory, - 'systemTriggered' => $systemTriggered, - 'config' => $event['properties'], - ]; - - if (is_array($settings['callback'])) { - $reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]); - } elseif (strpos($settings['callback'], '::') !== false) { - $parts = explode('::', $settings['callback']); - $reflection = new \ReflectionMethod($parts[0], $parts[1]); - } else { - $reflection = new \ReflectionMethod(null, $settings['callback']); - } + unset($leads, $campaignLeadIds, $leadLog); - $pass = []; - foreach ($reflection->getParameters() as $param) { - if (isset($args[$param->getName()])) { - $pass[] = $args[$param->getName()]; - } else { - $pass[] = null; + $currentCount = ($max) ? $totalEventCount : $negativeEvaluatedCount; + if ($output && isset($progress) && $currentCount < $maxCount) { + $progress->setProgress($currentCount); } - } - $result = $reflection->invokeArgs($this, $pass); - - if ('decision' != $event['eventType'] && $this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_EXECUTION)) { - $executionEvent = $this->dispatcher->dispatch( - CampaignEvents::ON_EVENT_EXECUTION, - new CampaignExecutionEvent($args, $result, $log) - ); + // Free some memory + gc_collect_cycles(); - if ($executionEvent->wasLogUpdatedByListener()) { - $result = $executionEvent->getLogEntry(); - } + ++$batchDebugCounter; } - } else { - $result = true; - } - - return $result; - } - - /** - * Check to see if the interval between events are appropriate to fire currentEvent. - * - * @param $action - * @param \DateTime $parentTriggeredDate - * @param bool $allowNegative - * - * @return bool - */ - public function checkEventTiming($action, \DateTime $parentTriggeredDate = null, $allowNegative = false) - { - $now = new \DateTime(); - - $this->logger->debug('CAMPAIGN: Check timing for '.ucfirst($action['eventType']).' ID# '.$action['id']); - - if ($action instanceof Event) { - $action = $action->convertToArray(); - } - - if ($action['decisionPath'] == 'no' && !$allowNegative) { - $this->logger->debug('CAMPAIGN: '.ucfirst($action['eventType']).' is attached to a negative path which is not allowed'); - - return false; - } else { - $negate = ($action['decisionPath'] == 'no' && $allowNegative); - - if ($action['triggerMode'] == 'interval') { - $triggerOn = $negate ? clone $parentTriggeredDate : new \DateTime(); - - if ($triggerOn == null) { - $triggerOn = new \DateTime(); - } - - $interval = $action['triggerInterval']; - $unit = $action['triggerIntervalUnit']; - - $this->logger->debug('CAMPAIGN: Adding interval of '.$interval.$unit.' to '.$triggerOn->format('Y-m-d H:i:s T')); - - $triggerOn->add((new DateTimeHelper())->buildInterval($interval, $unit)); - - if ($triggerOn > $now) { - $this->logger->debug( - 'CAMPAIGN: Date to execute ('.$triggerOn->format('Y-m-d H:i:s T').') is later than now ('.$now->format('Y-m-d H:i:s T') - .')'.(($action['decisionPath'] == 'no') ? ' so ignore' : ' so schedule') - ); - - // Save some RAM for batch processing - unset($now, $action, $dv, $dt); - - //the event is to be scheduled based on the time interval - return $triggerOn; - } - } elseif ($action['triggerMode'] == 'date') { - if (!$action['triggerDate'] instanceof \DateTime) { - $triggerDate = new DateTimeHelper($action['triggerDate']); - $action['triggerDate'] = $triggerDate->getDateTime(); - unset($triggerDate); - } - - $this->logger->debug('CAMPAIGN: Date execution on '.$action['triggerDate']->format('Y-m-d H:i:s T')); - - $pastDue = $now >= $action['triggerDate']; - - if ($negate) { - $this->logger->debug( - 'CAMPAIGN: Negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' - .$now->format('Y-m-d H:i:s T').') and is thus '.(($pastDue) ? 'overdue' : 'not past due') - ); - - //it is past the scheduled trigger date and the lead has done nothing so return true to trigger - //the event otherwise false to do nothing - $return = ($pastDue) ? true : $action['triggerDate']; - - // Save some RAM for batch processing - unset($now, $action); - - return $return; - } elseif (!$pastDue) { - $this->logger->debug( - 'CAMPAIGN: Non-negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' - .$now->format('Y-m-d H:i:s T').') and is thus not past due' - ); - //schedule the event - return $action['triggerDate']; - } + if ($output && isset($progress)) { + $progress->finish(); + $output->writeln(''); } - } - - $this->logger->debug('CAMPAIGN: Nothing stopped execution based on timing.'); - - //default is to trigger the event - return true; - } - - /** - * @param Event|int $event - * @param Campaign $campaign - * @param \Mautic\LeadBundle\Entity\Lead|null $lead - * @param \Mautic\CoreBundle\Entity\IpAddress|null $ipAddress - * @param bool $systemTriggered - * - * @return LeadEventLog - * - * @throws \Doctrine\ORM\ORMException - */ - public function getLogEntity($event, $campaign, $lead = null, $ipAddress = null, $systemTriggered = false) - { - $log = new LeadEventLog(); - - if ($ipAddress == null) { - // Lead triggered from system IP - $ipAddress = $this->ipLookupHelper->getIpAddress(); - } - $log->setIpAddress($ipAddress); - - if (!$event instanceof Event) { - $event = $this->em->getReference('MauticCampaignBundle:Event', $event); - } - $log->setEvent($event); - - if (!$campaign instanceof Campaign) { - $campaign = $this->em->getReference('MauticCampaignBundle:Campaign', $campaign); - } - $log->setCampaign($campaign); - if ($lead == null) { - $lead = $this->leadModel->getCurrentLead(); + $this->triggerConditions($campaign, $evaluatedEventCount, $executedEventCount, $totalEventCount); } - $log->setLead($lead); - $log->setDateTriggered(new \DateTime()); - $log->setSystemTriggered($systemTriggered); - - // Save some RAM for batch processing - unset($event, $campaign, $lead); - - return $log; - } - - /** - * @param LeadEventLog $log - * @param $status - * @param null $reason - */ - public function setEventStatus(LeadEventLog $log, $status, $reason = null) - { - $failedLog = $log->getFailedLog(); - - if ($status) { - if ($failedLog) { - $this->em->getRepository('MauticCampaignBundle:FailedLeadEventLog')->deleteEntity($failedLog); - $log->setFailedLog(null); - } - - $metadata = $log->getMetadata(); - unset($metadata['errors']); - $log->setMetadata($metadata); - } else { - if (!$failedLog) { - $failedLog = new FailedLeadEventLog(); - } - $failedLog->setDateAdded() - ->setReason($reason) - ->setLog($log); + $counts = [ + 'events' => $nonActionEventCount, + 'evaluated' => $negativeEvaluatedCount, + 'executed' => $negativeExecutedCount, + 'totalEvaluated' => $evaluatedEventCount, + 'totalExecuted' => $executedEventCount, + ]; + $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - $this->em->persist($failedLog); - } + return ($returnCounts) ? $counts : $executedEventCount; } /** @@ -2138,70 +617,4 @@ public function notifyOfFailure(Lead $lead, $campaignCreatedBy, $header) ); } } - - /** - * Handles condition type events. - * - * @deprecated 2.6.0 to be removed in 3.0; use triggerConditions() instead - * - * @param bool $response - * @param array $eventSettings - * @param array $event - * @param Campaign $campaign - * @param Lead $lead - * @param int $evaluatedEventCount The number of events evaluated for the current method (kickoff, negative/inaction, scheduled) - * @param int $executedEventCount The number of events successfully executed for the current method - * @param int $totalEventCount The total number of events across all methods - * - * @return bool True if an event was executed - */ - public function handleCondition( - $response, - $eventSettings, - $event, - $campaign, - $lead, - &$evaluatedEventCount = 0, - &$executedEventCount = 0, - &$totalEventCount = 0 - ) { - $repo = $this->getRepository(); - $decisionPath = ($response === true) ? 'yes' : 'no'; - $childEvents = $repo->getEventsByParent($event['id'], $decisionPath); - - $this->logger->debug( - 'CAMPAIGN: Condition ID# '.$event['id'].' triggered with '.$decisionPath.' decision path. Has '.count($childEvents).' child event(s).' - ); - - $childExecuted = false; - foreach ($childEvents as $childEvent) { - // Trigger child event recursively - if ($this->executeEvent( - $childEvent, - $campaign, - $lead, - $eventSettings, - true, - null, - null, - false, - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ) - ) { - $childExecuted = true; - } - } - - return $childExecuted; - } - - /** - * Batch sleep according to settings. - */ - protected function batchSleep() - { - // No longer used - } } diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php b/app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php new file mode 100644 index 00000000000..1e0af45df52 --- /dev/null +++ b/app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php @@ -0,0 +1,964 @@ +kickoffExecutioner->executeForContact($campaign, $leadId, $output); + } else { + $counter = $this->kickoffExecutioner->executeForCampaign($campaign, $limit, $output); + } + + $totalEventCount += $counter->getEventCount(); + + if ($returnCounts) { + return [ + 'events' => $counter->getEventCount(), + 'evaluated' => $counter->getEvaluated(), + 'executed' => $counter->getExecuted(), + 'totalEvaluated' => $counter->getTotalEvaluated(), + 'totalExecuted' => $counter->getTotalExecuted(), + ]; + } + + return $counter->getTotalExecuted(); + } + + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param $campaign + * @param $totalEventCount + * @param int $limit + * @param bool $max + * @param OutputInterface|null $output + * @param bool $returnCounts + * + * @return array|int + * + * @throws \Doctrine\ORM\Query\QueryException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + public function triggerScheduledEvents( + $campaign, + &$totalEventCount, + $limit = 100, + $max = false, + OutputInterface $output = null, + $returnCounts = false + ) { + $counter = $this->scheduledExecutioner->executeForCampaign($campaign, $limit, $output); + + $totalEventCount += $counter->getEventCount(); + + if ($returnCounts) { + return [ + 'events' => $counter->getEventCount(), + 'evaluated' => $counter->getEvaluated(), + 'executed' => $counter->getExecuted(), + 'totalEvaluated' => $counter->getTotalEvaluated(), + 'totalExecuted' => $counter->getTotalExecuted(), + ]; + } + + return $counter->getTotalExecuted(); + } + + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param $type + * @param null $eventDetails + * @param null $channel + * @param null $channelId + * + * @return array + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + public function triggerEvent($type, $eventDetails = null, $channel = null, $channelId = null) + { + $response = $this->decisionExecutioner->execute($type, $eventDetails, $channel, $channelId); + + return $response->getResponseArray(); + } + + /** + * Handles condition type events. + * + * @deprecated 2.6.0 to be removed in 3.0 + * + * @param bool $response + * @param array $eventSettings + * @param array $event + * @param Campaign $campaign + * @param Lead $lead + * @param int $evaluatedEventCount The number of events evaluated for the current method (kickoff, negative/inaction, scheduled) + * @param int $executedEventCount The number of events successfully executed for the current method + * @param int $totalEventCount The total number of events across all methods + * + * @return bool True if an event was executed + */ + public function handleCondition( + $response, + $eventSettings, + $event, + $campaign, + $lead, + &$evaluatedEventCount = 0, + &$executedEventCount = 0, + &$totalEventCount = 0 + ) { + $repo = $this->getRepository(); + $decisionPath = ($response === true) ? 'yes' : 'no'; + $childEvents = $repo->getEventsByParent($event['id'], $decisionPath); + + $this->logger->debug( + 'CAMPAIGN: Condition ID# '.$event['id'].' triggered with '.$decisionPath.' decision path. Has '.count($childEvents).' child event(s).' + ); + + $childExecuted = false; + foreach ($childEvents as $childEvent) { + // Trigger child event recursively + if ($this->executeEvent( + $childEvent, + $campaign, + $lead, + $eventSettings, + true, + null, + null, + false, + $evaluatedEventCount, + $executedEventCount, + $totalEventCount + ) + ) { + $childExecuted = true; + } + } + + return $childExecuted; + } + + /** + * Invoke the event's callback function. + * + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param $event + * @param $settings + * @param null $lead + * @param null $eventDetails + * @param bool $systemTriggered + * @param LeadEventLog $log + * + * @return bool|mixed + */ + public function invokeEventCallback($event, $settings, $lead = null, $eventDetails = null, $systemTriggered = false, LeadEventLog $log = null) + { + if (isset($settings['eventName'])) { + // Create a campaign event with a default successful result + $campaignEvent = new CampaignExecutionEvent( + [ + 'eventSettings' => $settings, + 'eventDetails' => $eventDetails, + 'event' => $event, + 'lead' => $lead, + 'systemTriggered' => $systemTriggered, + 'config' => $event['properties'], + ], + true, + $log + ); + + $eventName = array_key_exists('eventName', $settings) ? $settings['eventName'] : null; + + if ($eventName && $this->dispatcher->hasListeners($eventName)) { + $this->dispatcher->dispatch($eventName, $campaignEvent); + + if ($event['eventType'] !== 'decision' && $this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_EXECUTION)) { + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_EXECUTION, $campaignEvent); + } + + if ($campaignEvent->wasLogUpdatedByListener()) { + $campaignEvent->setResult($campaignEvent->getLogEntry()); + } + } + + if (null !== $log) { + $log->setChannel($campaignEvent->getChannel()) + ->setChannelId($campaignEvent->getChannelId()); + } + + return $campaignEvent->getResult(); + } + + /* + * @deprecated 2.0 - to be removed in 3.0; Use the new eventName method instead + */ + if (isset($settings['callback']) && is_callable($settings['callback'])) { + $args = [ + 'eventSettings' => $settings, + 'eventDetails' => $eventDetails, + 'event' => $event, + 'lead' => $lead, + 'factory' => $this->factory, + 'systemTriggered' => $systemTriggered, + 'config' => $event['properties'], + ]; + + if (is_array($settings['callback'])) { + $reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]); + } elseif (strpos($settings['callback'], '::') !== false) { + $parts = explode('::', $settings['callback']); + $reflection = new \ReflectionMethod($parts[0], $parts[1]); + } else { + $reflection = new \ReflectionMethod(null, $settings['callback']); + } + + $pass = []; + foreach ($reflection->getParameters() as $param) { + if (isset($args[$param->getName()])) { + $pass[] = $args[$param->getName()]; + } else { + $pass[] = null; + } + } + + $result = $reflection->invokeArgs($this, $pass); + + if ('decision' != $event['eventType'] && $this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_EXECUTION)) { + $executionEvent = $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_EXECUTION, + new CampaignExecutionEvent($args, $result, $log) + ); + + if ($executionEvent->wasLogUpdatedByListener()) { + $result = $executionEvent->getLogEntry(); + } + } + } else { + $result = true; + } + + return $result; + } + + /** + * Execute or schedule an event. Condition events are executed recursively. + * + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param array $event + * @param Campaign $campaign + * @param Lead $lead + * @param array $eventSettings + * @param bool $allowNegative + * @param \DateTime $parentTriggeredDate + * @param \DateTime|bool $eventTriggerDate + * @param bool $logExists + * @param int $evaluatedEventCount The number of events evaluated for the current method (kickoff, negative/inaction, scheduled) + * @param int $executedEventCount The number of events successfully executed for the current method + * @param int $totalEventCount The total number of events across all methods + * + * @return bool + */ + public function executeEvent( + $event, + $campaign, + $lead, + $eventSettings = null, + $allowNegative = false, + \DateTime $parentTriggeredDate = null, + $eventTriggerDate = null, + $logExists = false, + &$evaluatedEventCount = 0, + &$executedEventCount = 0, + &$totalEventCount = 0 + ) { + ++$evaluatedEventCount; + ++$totalEventCount; + + // Get event settings if applicable + if ($eventSettings === null) { + $eventSettings = $this->campaignModel->getEvents(); + } + + // Set date timing should be compared with if applicable + if ($parentTriggeredDate === null) { + // Default to today + $parentTriggeredDate = new \DateTime(); + } + + $repo = $this->getRepository(); + $logRepo = $this->getLeadEventLogRepository(); + + if (isset($eventSettings[$event['eventType']][$event['type']])) { + $thisEventSettings = $eventSettings[$event['eventType']][$event['type']]; + } else { + $this->logger->debug( + 'CAMPAIGN: Settings not found for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + ); + unset($event); + + return false; + } + + if ($event['eventType'] == 'condition') { + $allowNegative = true; + } + + // Set campaign ID + + $event['campaign'] = [ + 'id' => $campaign->getId(), + 'name' => $campaign->getName(), + 'createdBy' => $campaign->getCreatedBy(), + ]; + + // Ensure properties is an array + if ($event['properties'] === null) { + $event['properties'] = []; + } elseif (!is_array($event['properties'])) { + $event['properties'] = unserialize($event['properties']); + } + + // Ensure triggerDate is a \DateTime + if ($event['triggerMode'] == 'date' && !$event['triggerDate'] instanceof \DateTime) { + $triggerDate = new DateTimeHelper($event['triggerDate']); + $event['triggerDate'] = $triggerDate->getDateTime(); + unset($triggerDate); + } + + if ($eventTriggerDate == null) { + $eventTriggerDate = $this->checkEventTiming($event, $parentTriggeredDate, $allowNegative); + } + $result = true; + + // Create/get log entry + if ($logExists) { + if (true === $logExists) { + $log = $logRepo->findOneBy( + [ + 'lead' => $lead->getId(), + 'event' => $event['id'], + ] + ); + } else { + $log = $this->em->getReference('MauticCampaignBundle:LeadEventLog', $logExists); + } + } + + if (empty($log)) { + $log = $this->getLogEntity($event['id'], $campaign, $lead, null, !defined('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED')); + } + + if ($eventTriggerDate instanceof \DateTime) { + ++$executedEventCount; + + $log->setTriggerDate($eventTriggerDate); + $logRepo->saveEntity($log); + + //lead actively triggered this event, a decision wasn't involved, or it was system triggered and a "no" path so schedule the event to be fired at the defined time + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + .' has timing that is not appropriate and thus scheduled for '.$eventTriggerDate->format('Y-m-d H:m:i T') + ); + + if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_SCHEDULED)) { + $args = [ + 'eventSettings' => $thisEventSettings, + 'eventDetails' => null, + 'event' => $event, + 'lead' => $lead, + 'systemTriggered' => true, + 'dateScheduled' => $eventTriggerDate, + ]; + + $scheduledEvent = new CampaignScheduledEvent($args); + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_SCHEDULED, $scheduledEvent); + unset($scheduledEvent, $args); + } + } elseif ($eventTriggerDate) { + // If log already existed, assume it was scheduled in order to not force + // Doctrine to do a query to fetch the information + $wasScheduled = (!$logExists) ? $log->getIsScheduled() : true; + + $log->setIsScheduled(false); + $log->setDateTriggered(new \DateTime()); + + try { + // Save before executing event to ensure it's not picked up again + $logRepo->saveEntity($log); + $this->logger->debug( + 'CAMPAIGN: Created log for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + .' prior to execution.' + ); + } catch (EntityNotFoundException $exception) { + // The lead has been likely removed from this lead/list + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + .' wasn\'t found: '.$exception->getMessage() + ); + + return false; + } catch (DBALException $exception) { + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + .' failed with DB error: '.$exception->getMessage() + ); + + return false; + } + + // Set the channel + $this->campaignModel->setChannelFromEventProperties($log, $event, $thisEventSettings); + + //trigger the action + $response = $this->invokeEventCallback($event, $thisEventSettings, $lead, null, true, $log); + + // Check if the lead wasn't deleted during the event callback + if (null === $lead->getId() && $response === true) { + ++$executedEventCount; + + $this->logger->debug( + 'CAMPAIGN: Contact was deleted while executing '.ucfirst($event['eventType']).' ID# '.$event['id'] + ); + + return true; + } + + $eventTriggered = false; + if ($response instanceof LeadEventLog) { + $log = $response; + + // Listener handled the event and returned a log entry + $this->campaignModel->setChannelFromEventProperties($log, $event, $thisEventSettings); + + ++$executedEventCount; + + $this->logger->debug( + 'CAMPAIGN: Listener handled event for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + ); + + if (!$log->getIsScheduled()) { + $eventTriggered = true; + } + } elseif (($response === false || (is_array($response) && isset($response['result']) && false === $response['result'])) + && $event['eventType'] == 'action' + ) { + $result = false; + $debug = 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + .' failed with a response of '.var_export($response, true); + + // Something failed + if ($wasScheduled || !empty($this->scheduleTimeForFailedEvents)) { + $date = new \DateTime(); + $date->add(new \DateInterval($this->scheduleTimeForFailedEvents)); + + // Reschedule + $log->setTriggerDate($date); + + if (is_array($response)) { + $log->setMetadata($response); + } + $debug .= ' thus placed on hold '.$this->scheduleTimeForFailedEvents; + + $metadata = $log->getMetadata(); + if (is_array($response)) { + $metadata = array_merge($metadata, $response); + } + + $reason = null; + if (isset($metadata['errors'])) { + $reason = (is_array($metadata['errors'])) ? implode('
', $metadata['errors']) : $metadata['errors']; + } elseif (isset($metadata['reason'])) { + $reason = $metadata['reason']; + } + $this->setEventStatus($log, false, $reason); + } else { + // Remove + $debug .= ' thus deleted'; + $repo->deleteEntity($log); + unset($log); + } + + $this->notifyOfFailure($lead, $campaign->getCreatedBy(), $campaign->getName().' / '.$event['name']); + $this->logger->debug($debug); + } else { + $this->setEventStatus($log, true); + + ++$executedEventCount; + + if ($response !== true) { + if ($this->triggeredResponses !== false) { + $eventTypeKey = $event['eventType']; + $typeKey = $event['type']; + + if (!array_key_exists($eventTypeKey, $this->triggeredResponses) || !is_array($this->triggeredResponses[$eventTypeKey])) { + $this->triggeredResponses[$eventTypeKey] = []; + } + + if (!array_key_exists($typeKey, $this->triggeredResponses[$eventTypeKey]) + || !is_array( + $this->triggeredResponses[$eventTypeKey][$typeKey] + ) + ) { + $this->triggeredResponses[$eventTypeKey][$typeKey] = []; + } + + $this->triggeredResponses[$eventTypeKey][$typeKey][$event['id']] = $response; + } + + $log->setMetadata($response); + } + + $this->logger->debug( + 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() + .' successfully executed and logged with a response of '.var_export($response, true) + ); + + $eventTriggered = true; + } + + if ($eventTriggered) { + // Collect the events that were triggered so that conditions can be handled properly + + if ('condition' === $event['eventType']) { + // Conditions will need child event processed + $decisionPath = ($response === true) ? 'yes' : 'no'; + + if (true !== $response) { + // Note that a condition took non action path so we can generate a visual stat + $log->setNonActionPathTaken(true); + } + } else { + // Actions will need to check if conditions are attached to it + $decisionPath = 'null'; + } + + if (!isset($this->triggeredEvents[$event['id']])) { + $this->triggeredEvents[$event['id']] = []; + } + if (!isset($this->triggeredEvents[$event['id']][$decisionPath])) { + $this->triggeredEvents[$event['id']][$decisionPath] = []; + } + + $this->triggeredEvents[$event['id']][$decisionPath][] = $lead->getId(); + } + + if ($log) { + $logRepo->saveEntity($log); + } + } else { + //else do nothing + $result = false; + $this->logger->debug( + 'CAMPAIGN: Timing failed ('.gettype($eventTriggerDate).') for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# ' + .$lead->getId() + ); + } + + if (!empty($log)) { + // Detach log + $this->em->detach($log); + unset($log); + } + + unset($eventTriggerDate, $event); + + return $result; + } + + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param Campaign $campaign + * @param int $evaluatedEventCount + * @param int $executedEventCount + * @param int $totalEventCount + */ + public function triggerConditions(Campaign $campaign, &$evaluatedEventCount = 0, &$executedEventCount = 0, &$totalEventCount = 0) + { + $eventSettings = $this->campaignModel->getEvents(); + $repo = $this->getRepository(); + $sleepBatchCount = 0; + $limit = 100; + + while (!empty($this->triggeredEvents)) { + // Reset the triggered events in order to be hydrated again for chained conditions/actions + $triggeredEvents = $this->triggeredEvents; + $this->triggeredEvents = []; + + foreach ($triggeredEvents as $parentId => $decisionPaths) { + foreach ($decisionPaths as $decisionPath => $contactIds) { + $typeRestriction = null; + if ('null' === $decisionPath) { + // This is an action so check if conditions are attached + $decisionPath = null; + $typeRestriction = 'condition'; + } // otherwise this should be a condition so get all children connected to the given path + + $childEvents = $repo->getEventsByParent($parentId, $decisionPath, $typeRestriction); + $this->logger->debug( + 'CAMPAIGN: Evaluating '.count($childEvents).' child event(s) to process the conditions of parent ID# '.$parentId.'.' + ); + + if (!count($childEvents)) { + continue; + } + + $batchedContactIds = array_chunk($contactIds, $limit); + foreach ($batchedContactIds as $batchDebugCounter => $contactBatch) { + ++$batchDebugCounter; // start with 1 + + if (empty($contactBatch)) { + break; + } + + $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); + + $leads = $this->leadModel->getEntities( + [ + 'filter' => [ + 'force' => [ + [ + 'column' => 'l.id', + 'expr' => 'in', + 'value' => $contactBatch, + ], + ], + ], + 'orderBy' => 'l.id', + 'orderByDir' => 'asc', + 'withPrimaryCompany' => true, + ] + ); + + $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', array_keys($leads))); + + if (!count($leads)) { + // Just a precaution in case non-existent leads are lingering in the campaign leads table + $this->logger->debug('CAMPAIGN: No contact entities found.'); + + continue; + } + + /** @var \Mautic\LeadBundle\Entity\Lead $lead */ + $leadDebugCounter = 0; + foreach ($leads as $lead) { + ++$leadDebugCounter; // start with 1 + + $this->logger->debug( + 'CAMPAIGN: Current Lead ID# '.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter + ); + + // Set lead in case this is triggered by the system + $this->leadModel->setSystemCurrentLead($lead); + + foreach ($childEvents as $childEvent) { + $this->executeEvent( + $childEvent, + $campaign, + $lead, + $eventSettings, + true, + null, + null, + false, + $evaluatedEventCount, + $executedEventCount, + $totalEventCount + ); + + if ($sleepBatchCount == $limit) { + // Keep CPU down + $this->batchSleep(); + $sleepBatchCount = 0; + } else { + ++$sleepBatchCount; + } + } + } + + // Free RAM + $this->em->clear('Mautic\LeadBundle\Entity\Lead'); + $this->em->clear('Mautic\UserBundle\Entity\User'); + unset($leads); + + // Free some memory + gc_collect_cycles(); + } + } + } + } + } + + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param LeadEventLog $log + * @param $status + * @param null $reason + */ + public function setEventStatus(LeadEventLog $log, $status, $reason = null) + { + $failedLog = $log->getFailedLog(); + + if ($status) { + if ($failedLog) { + $this->em->getRepository('MauticCampaignBundle:FailedLeadEventLog')->deleteEntity($failedLog); + $log->setFailedLog(null); + } + + $metadata = $log->getMetadata(); + unset($metadata['errors']); + $log->setMetadata($metadata); + } else { + if (!$failedLog) { + $failedLog = new FailedLeadEventLog(); + } + + $failedLog->setDateAdded() + ->setReason($reason) + ->setLog($log); + + $this->em->persist($failedLog); + } + } + + /** + * Check to see if the interval between events are appropriate to fire currentEvent. + * + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param $action + * @param \DateTime $parentTriggeredDate + * @param bool $allowNegative + * + * @return bool + */ + public function checkEventTiming($action, \DateTime $parentTriggeredDate = null, $allowNegative = false) + { + $now = new \DateTime(); + + $this->logger->debug('CAMPAIGN: Check timing for '.ucfirst($action['eventType']).' ID# '.$action['id']); + + if ($action instanceof Event) { + $action = $action->convertToArray(); + } + + if ($action['decisionPath'] == 'no' && !$allowNegative) { + $this->logger->debug('CAMPAIGN: '.ucfirst($action['eventType']).' is attached to a negative path which is not allowed'); + + return false; + } else { + $negate = ($action['decisionPath'] == 'no' && $allowNegative); + + if ($action['triggerMode'] == 'interval') { + $triggerOn = $negate ? clone $parentTriggeredDate : new \DateTime(); + + if ($triggerOn == null) { + $triggerOn = new \DateTime(); + } + + $interval = $action['triggerInterval']; + $unit = $action['triggerIntervalUnit']; + + $this->logger->debug('CAMPAIGN: Adding interval of '.$interval.$unit.' to '.$triggerOn->format('Y-m-d H:i:s T')); + + $triggerOn->add((new DateTimeHelper())->buildInterval($interval, $unit)); + + if ($triggerOn > $now) { + $this->logger->debug( + 'CAMPAIGN: Date to execute ('.$triggerOn->format('Y-m-d H:i:s T').') is later than now ('.$now->format('Y-m-d H:i:s T') + .')'.(($action['decisionPath'] == 'no') ? ' so ignore' : ' so schedule') + ); + + // Save some RAM for batch processing + unset($now, $action, $dv, $dt); + + //the event is to be scheduled based on the time interval + return $triggerOn; + } + } elseif ($action['triggerMode'] == 'date') { + if (!$action['triggerDate'] instanceof \DateTime) { + $triggerDate = new DateTimeHelper($action['triggerDate']); + $action['triggerDate'] = $triggerDate->getDateTime(); + unset($triggerDate); + } + + $this->logger->debug('CAMPAIGN: Date execution on '.$action['triggerDate']->format('Y-m-d H:i:s T')); + + $pastDue = $now >= $action['triggerDate']; + + if ($negate) { + $this->logger->debug( + 'CAMPAIGN: Negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' + .$now->format('Y-m-d H:i:s T').') and is thus '.(($pastDue) ? 'overdue' : 'not past due') + ); + + //it is past the scheduled trigger date and the lead has done nothing so return true to trigger + //the event otherwise false to do nothing + $return = ($pastDue) ? true : $action['triggerDate']; + + // Save some RAM for batch processing + unset($now, $action); + + return $return; + } elseif (!$pastDue) { + $this->logger->debug( + 'CAMPAIGN: Non-negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' + .$now->format('Y-m-d H:i:s T').') and is thus not past due' + ); + + //schedule the event + return $action['triggerDate']; + } + } + } + + $this->logger->debug('CAMPAIGN: Nothing stopped execution based on timing.'); + + //default is to trigger the event + return true; + } + + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param Event|int $event + * @param Campaign $campaign + * @param \Mautic\LeadBundle\Entity\Lead|null $lead + * @param \Mautic\CoreBundle\Entity\IpAddress|null $ipAddress + * @param bool $systemTriggered + * + * @return LeadEventLog + * + * @throws \Doctrine\ORM\ORMException + */ + public function getLogEntity($event, $campaign, $lead = null, $ipAddress = null, $systemTriggered = false) + { + $log = new LeadEventLog(); + + if ($ipAddress == null) { + // Lead triggered from system IP + $ipAddress = $this->ipLookupHelper->getIpAddress(); + } + $log->setIpAddress($ipAddress); + + if (!$event instanceof Event) { + $event = $this->em->getReference('MauticCampaignBundle:Event', $event); + } + $log->setEvent($event); + + if (!$campaign instanceof Campaign) { + $campaign = $this->em->getReference('MauticCampaignBundle:Campaign', $campaign); + } + $log->setCampaign($campaign); + + if ($lead == null) { + $lead = $this->leadModel->getCurrentLead(); + } + $log->setLead($lead); + $log->setDateTriggered(new \DateTime()); + $log->setSystemTriggered($systemTriggered); + + // Save some RAM for batch processing + unset($event, $campaign, $lead); + + return $log; + } + + /** + * Batch sleep according to settings. + */ + protected function batchSleep() + { + // No longer used + } +} diff --git a/app/bundles/CoreBundle/Entity/CommonRepository.php b/app/bundles/CoreBundle/Entity/CommonRepository.php index 647a488ca45..6974e9dfca9 100644 --- a/app/bundles/CoreBundle/Entity/CommonRepository.php +++ b/app/bundles/CoreBundle/Entity/CommonRepository.php @@ -203,6 +203,16 @@ public function deleteEntity($entity, $flush = true) } } + /** + * @param array $entities + */ + public function detachEntities(array $entities) + { + foreach ($entities as $entity) { + $this->getEntityManager()->detach($entity); + } + } + /** * @param $alias * @param null $catAlias From d07ce622753077e1e630720211e0668560e02435 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 5 Feb 2018 15:05:06 -0600 Subject: [PATCH 432/778] Inactive WIP --- .../CampaignBundle/Entity/EventRepository.php | 96 +------------------ .../Entity/LeadEventLogRepository.php | 75 +++++++++++++-- .../Entity/LegacyEventRepository.php | 92 ++++++++++++++++++ .../Executioner/InactiveExecutioner.php | 93 +++++++++++++++++- .../Executioner/ScheduledExecutioner.php | 3 +- .../CampaignBundle/Model/EventModel.php | 10 +- ...entModelTrait.php => LegacyEventModel.php} | 24 ++++- 7 files changed, 278 insertions(+), 115 deletions(-) rename app/bundles/CampaignBundle/Model/{LegacyEventModelTrait.php => LegacyEventModel.php} (97%) diff --git a/app/bundles/CampaignBundle/Entity/EventRepository.php b/app/bundles/CampaignBundle/Entity/EventRepository.php index dc61fae25fb..107309e3857 100644 --- a/app/bundles/CampaignBundle/Entity/EventRepository.php +++ b/app/bundles/CampaignBundle/Entity/EventRepository.php @@ -56,7 +56,7 @@ public function getEntities(array $args = []) */ public function getContactPendingEvents($contactId, $type) { - // Limit to events that aren't been executed or scheduled yet + // Limit to events that hasn't been executed or scheduled yet $eventQb = $this->getEntityManager()->createQueryBuilder(); $eventQb->select('IDENTITY(log_event.event)') ->from(LeadEventLog::class, 'log_event') @@ -68,7 +68,7 @@ public function getContactPendingEvents($contactId, $type) ) ); - // Limit to events that have no parent or who's parent has already been executed + // Limit to events that has no parent or whose parent has already been executed $parentQb = $this->getEntityManager()->createQueryBuilder(); $parentQb->select('parent_log_event.id') ->from(LeadEventLog::class, 'parent_log_event') @@ -203,98 +203,6 @@ public function getEvents($args = []) return $events; } - /** - * Get the non-action log. - * - * @param $campaignId - * @param array $leads - * @param array $havingEvents - * @param array $excludeEvents - * @param bool|false $excludeScheduledFromHavingEvents - * - * @return array - */ - public function getEventLog($campaignId, $leads = [], $havingEvents = [], $excludeEvents = [], $excludeScheduledFromHavingEvents = false) - { - $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); - - $q->select('e.lead_id, e.event_id, e.date_triggered, e.is_scheduled') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') - ->where( - $q->expr()->eq('e.campaign_id', (int) $campaignId) - ) - ->groupBy('e.lead_id, e.event_id, e.date_triggered, e.is_scheduled'); - - if (!empty($leads)) { - $leadsQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); - - $leadsQb->select('null') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'include_leads') - ->where( - $leadsQb->expr()->eq('include_leads.lead_id', 'e.lead_id'), - $leadsQb->expr()->in('include_leads.lead_id', $leads) - ); - - $q->andWhere( - sprintf('EXISTS (%s)', $leadsQb->getSQL()) - ); - } - - if (!empty($havingEvents)) { - $eventsQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); - - $eventsQb->select('null') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'include_events') - ->where( - $eventsQb->expr()->eq('include_events.lead_id', 'e.lead_id'), - $eventsQb->expr()->in('include_events.event_id', $havingEvents) - ); - - if ($excludeScheduledFromHavingEvents) { - $eventsQb->andWhere( - $eventsQb->expr()->eq('include_events.is_scheduled', ':false') - ); - $q->setParameter('false', false, 'boolean'); - } - - $q->having( - sprintf('EXISTS (%s)', $eventsQb->getSQL()) - ); - } - - if (!empty($excludeEvents)) { - $eventsQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); - - $eventsQb->select('null') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'exclude_events') - ->where( - $eventsQb->expr()->eq('exclude_events.lead_id', 'e.lead_id'), - $eventsQb->expr()->in('exclude_events.event_id', $excludeEvents) - ); - - $eventsQb->andHaving( - sprintf('NOT EXISTS (%s)', $eventsQb->getSQL()) - ); - } - - $results = $q->execute()->fetchAll(); - - $log = []; - foreach ($results as $r) { - $leadId = $r['lead_id']; - $eventId = $r['event_id']; - - unset($r['lead_id']); - unset($r['event_id']); - - $log[$leadId][$eventId] = $r; - } - - unset($results); - - return $log; - } - /** * Null event parents in preparation for deleI'lting a campaign. * diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index 2ede93f156a..626657b5242 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -384,6 +384,58 @@ public function getChartQuery($options) return $chartQuery->fetchTimeData('('.$query.')', 'date_triggered'); } + /** + * @param $contactId + * + * @return array + */ + public function getInactive($campaignId) + { + // Limit to events that hasn't been executed or scheduled yet + $eventQb = $this->getEntityManager()->createQueryBuilder(); + $eventQb->select('IDENTITY(log_event.event)') + ->from(LeadEventLog::class, 'log_event') + ->where( + $eventQb->expr()->andX( + $eventQb->expr()->eq('IDENTITY(log_event.event)', 'IDENTITY(e.parent)'), + $eventQb->expr()->eq('IDENTITY(log_event.lead)', 'IDENTITY(l.lead)'), + $eventQb->expr()->eq('log_event.rotation', 'l.rotation') + ) + ); + + // Limit to events that has a decision parent + $parentQb = $this->getEntityManager()->createQueryBuilder(); + $parentQb->select('parent_log_event.id') + ->from(LeadEventLog::class, 'parent_log_event') + ->where( + $parentQb->expr()->eq('IDENTITY(parent_log_event.event)', 'IDENTITY(e.parent)'), + $parentQb->expr()->eq('IDENTITY(parent_log_event.lead)', 'IDENTITY(l.lead)'), + $parentQb->expr()->eq('parent_log_event.rotation', 'l.rotation') + ); + + $q = $this->createQueryBuilder('l', 'l.id'); + $q->select('e,c') + ->innerJoin('e.campaign', 'c') + ->innerJoin('c.leads', 'l') + ->where( + $q->expr()->andX( + $q->expr()->eq('c.isPublished', 1), + $q->expr()->eq('e.type', ':type'), + $q->expr()->eq('IDENTITY(l.lead)', ':contactId'), + $q->expr()->eq('l.manuallyRemoved', 0), + $q->expr()->notIn('e.id', $eventQb->getDQL()), + $q->expr()->orX( + $q->expr()->isNull('e.parent'), + $q->expr()->exists($parentQb->getDQL()) + ) + ) + ) + ->setParameter('type', $type) + ->setParameter('contactId', (int) $contactId); + + return $q->getQuery()->getResult(); + } + /** * Get a list of scheduled events. * @@ -435,21 +487,28 @@ public function getScheduled($eventId, $limit = null, $contactId = null) * * @return array */ - public function getScheduledCounts($campaignId) + public function getScheduledCounts($campaignId, $contactId = null) { $date = new \Datetime('now', new \DateTimeZone('UTC')); $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $expr = $q->expr()->andX( + $q->expr()->eq('l.campaign_id', ':campaignId'), + $q->expr()->eq('l.is_scheduled', ':true'), + $q->expr()->lte('l.trigger_date', ':now') + ); + + if ($contactId) { + $expr->add( + $q->expr()->eq('l.lead_id', ':contactId') + ); + $q->setParameter('contactId', (int) $contactId); + } + $results = $q->select('COUNT(*) as event_count, l.event_id') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'l') - ->where( - $q->expr()->andX( - $q->expr()->eq('l.campaign_id', ':campaignId'), - $q->expr()->eq('l.is_scheduled', ':true'), - $q->expr()->lte('l.trigger_date', ':now') - ) - ) + ->where($expr) ->setParameter('campaignId', $campaignId) ->setParameter('now', $date->format('Y-m-d H:i:s')) ->setParameter('true', true, \PDO::PARAM_BOOL) diff --git a/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php b/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php index 76dd5aa9913..c39fba10d24 100644 --- a/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php +++ b/app/bundles/CampaignBundle/Entity/LegacyEventRepository.php @@ -261,4 +261,96 @@ public function getCampaignActionAndConditionEvents($campaignId) return $events; } + + /** + * Get the non-action log. + * + * @param $campaignId + * @param array $leads + * @param array $havingEvents + * @param array $excludeEvents + * @param bool|false $excludeScheduledFromHavingEvents + * + * @return array + */ + public function getEventLog($campaignId, $leads = [], $havingEvents = [], $excludeEvents = [], $excludeScheduledFromHavingEvents = false) + { + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $q->select('e.lead_id, e.event_id, e.date_triggered, e.is_scheduled') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') + ->where( + $q->expr()->eq('e.campaign_id', (int) $campaignId) + ) + ->groupBy('e.lead_id, e.event_id, e.date_triggered, e.is_scheduled'); + + if (!empty($leads)) { + $leadsQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $leadsQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'include_leads') + ->where( + $leadsQb->expr()->eq('include_leads.lead_id', 'e.lead_id'), + $leadsQb->expr()->in('include_leads.lead_id', $leads) + ); + + $q->andWhere( + sprintf('EXISTS (%s)', $leadsQb->getSQL()) + ); + } + + if (!empty($havingEvents)) { + $eventsQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $eventsQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'include_events') + ->where( + $eventsQb->expr()->eq('include_events.lead_id', 'e.lead_id'), + $eventsQb->expr()->in('include_events.event_id', $havingEvents) + ); + + if ($excludeScheduledFromHavingEvents) { + $eventsQb->andWhere( + $eventsQb->expr()->eq('include_events.is_scheduled', ':false') + ); + $q->setParameter('false', false, 'boolean'); + } + + $q->having( + sprintf('EXISTS (%s)', $eventsQb->getSQL()) + ); + } + + if (!empty($excludeEvents)) { + $eventsQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $eventsQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'exclude_events') + ->where( + $eventsQb->expr()->eq('exclude_events.lead_id', 'e.lead_id'), + $eventsQb->expr()->in('exclude_events.event_id', $excludeEvents) + ); + + $eventsQb->andHaving( + sprintf('NOT EXISTS (%s)', $eventsQb->getSQL()) + ); + } + + $results = $q->execute()->fetchAll(); + + $log = []; + foreach ($results as $r) { + $leadId = $r['lead_id']; + $eventId = $r['event_id']; + + unset($r['lead_id']); + unset($r['event_id']); + + $log[$leadId][$eventId] = $r; + } + + unset($results); + + return $log; + } } diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 88457019e55..49fd7e4154a 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -11,6 +11,97 @@ namespace Mautic\CampaignBundle\Executioner; -class InactiveExecutioner +use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Result\Counter; +use Mautic\CoreBundle\Helper\ProgressBarHelper; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; + +class InactiveExecutioner implements ExecutionerInterface { + private $campaign; + private $contactId; + private $batchLimit; + private $output; + private $logger; + private $progressBar; + private $translator; + + public function executeForCampaign(Campaign $campaign, $batchLimit = 100, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->batchLimit = $batchLimit; + $this->output = ($output) ? $output : new NullOutput(); + + $this->logger->debug('CAMPAIGN: Triggering inaction events'); + + return $this->execute(); + } + + public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) + { + $this->campaign = $campaign; + $this->contactId = $contactId; + $this->output = ($output) ? $output : new NullOutput(); + $this->batchLimit = null; + + return $this->execute(); + } + + /** + * @return Counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Scheduler\Exception\NotSchedulableException + * @throws \Doctrine\ORM\Query\QueryException + */ + private function execute() + { + $this->counter = new Counter(); + + try { + $this->prepareForExecution(); + $this->executeOrRecheduleEvent(); + } catch (NoEventsFound $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } + + return $this->counter; + } + + /** + * @throws NoEventsFound + */ + private function prepareForExecution() + { + // Get counts by event + $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId()); + $totalScheduledCount = array_sum($scheduledEvents); + $this->scheduledEvents = array_keys($scheduledEvents); + $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); + + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalScheduledCount, + '%batch%' => $this->batchLimit, + ] + ) + ); + + $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); + $this->progressBar->start(); + + if (!$totalScheduledCount) { + throw new NoEventsFound(); + } + } } diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index eef46d5ac30..02f46a43c82 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -198,7 +198,7 @@ private function execute() private function prepareForExecution() { // Get counts by event - $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId()); + $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId(), $this->contactId); $totalScheduledCount = array_sum($scheduledEvents); $this->scheduledEvents = array_keys($scheduledEvents); $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); @@ -224,6 +224,7 @@ private function prepareForExecution() /** * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException * @throws \Doctrine\ORM\Query\QueryException */ diff --git a/app/bundles/CampaignBundle/Model/EventModel.php b/app/bundles/CampaignBundle/Model/EventModel.php index 3e5da406e25..6a968d9695d 100644 --- a/app/bundles/CampaignBundle/Model/EventModel.php +++ b/app/bundles/CampaignBundle/Model/EventModel.php @@ -22,7 +22,6 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Helper\ProgressBarHelper; -use Mautic\CoreBundle\Model\FormModel as CommonFormModel; use Mautic\CoreBundle\Model\NotificationModel; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; @@ -33,10 +32,8 @@ * Class EventModel * {@inheritdoc} */ -class EventModel extends CommonFormModel +class EventModel extends LegacyEventModel { - use LegacyEventModelTrait; - /** * @var IpLookupHelper */ @@ -89,9 +86,8 @@ public function __construct( $this->campaignModel = $campaignModel; $this->userModel = $userModel; $this->notificationModel = $notificationModel; - $this->decisionExecutioner = $decisionExecutioner; - $this->kickoffExecutioner = $kickoffExecutioner; - $this->scheduledExecutioner = $scheduledExecutioner; + + parent::__construct($decisionExecutioner, $kickoffExecutioner, $scheduledExecutioner); } /** diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php similarity index 97% rename from app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php rename to app/bundles/CampaignBundle/Model/LegacyEventModel.php index 1e0af45df52..48d608a928e 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModelTrait.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -25,13 +25,12 @@ use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CoreBundle\Helper\DateTimeHelper; use Symfony\Component\Console\Output\OutputInterface; +use Mautic\CoreBundle\Model\FormModel as CommonFormModel; /** - * Trait LegacyEventModelTrait. - * * @deprecated 2.13.0 to be removed in 3.0 */ -trait LegacyEventModelTrait +class LegacyEventModel extends CommonFormModel { /** * @var DecisionExecutioner @@ -53,6 +52,23 @@ trait LegacyEventModelTrait */ protected $triggeredEvents; + /** + * LegacyEventModel constructor. + * + * @param DecisionExecutioner $decisionExecutioner + * @param KickoffExecutioner $kickoffExecutioner + * @param ScheduledExecutioner $scheduledExecutioner + */ + public function __construct( + DecisionExecutioner $decisionExecutioner, + KickoffExecutioner $kickoffExecutioner, + ScheduledExecutioner $scheduledExecutioner + ) { + $this->decisionExecutioner = $decisionExecutioner; + $this->kickoffExecutioner = $kickoffExecutioner; + $this->scheduledExecutioner = $scheduledExecutioner; + } + /** * Trigger the root level action(s) in campaign(s). * @@ -704,7 +720,7 @@ public function triggerConditions(Campaign $campaign, &$evaluatedEventCount = 0, $leads = $this->leadModel->getEntities( [ - 'filter' => [ + 'filter' => [ 'force' => [ [ 'column' => 'l.id', From 51174df5a521b39226a82eb442bf9084b25d0c08 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 11:21:18 -0600 Subject: [PATCH 433/778] Campaign refactoring for inactive events and support to chunk process in commands --- app/bundles/CampaignBundle/CampaignEvents.php | 24 +- .../Command/ExecuteEventCommand.php | 99 +++ .../Command/TriggerCampaignCommand.php | 392 +++++++++--- .../Command/ValidateEventCommand.php | 126 ++++ app/bundles/CampaignBundle/Config/config.php | 74 ++- .../CampaignBundle/Entity/Campaign.php | 44 +- .../Entity/CampaignRepository.php | 203 +++++-- app/bundles/CampaignBundle/Entity/Event.php | 8 +- .../CampaignBundle/Entity/EventRepository.php | 8 +- .../CampaignBundle/Entity/LeadEventLog.php | 2 +- .../Entity/LeadEventLogRepository.php | 171 +++--- .../CampaignBundle/Entity/LeadRepository.php | 161 +++++ .../Event/DecisionResultsEvent.php | 74 +++ .../Event/ExecutedBatchEvent.php | 25 + .../CampaignBundle/Event/ScheduledEvent.php | 20 +- .../ContactFinder/InactiveContacts.php | 120 ++++ .../ContactFinder/KickoffContacts.php | 24 +- .../ContactFinder/Limiter/ContactLimiter.php | 99 +++ .../Executioner/ContactRangeTrait.php | 17 + .../Executioner/DecisionExecutioner.php | 2 +- .../Dispatcher/EventDispatcher.php | 26 +- .../Dispatcher/LegacyEventDispatcher.php | 28 + .../Executioner/Event/Decision.php | 2 + .../Executioner/EventExecutioner.php | 86 ++- .../Executioner/ExecutionerInterface.php | 14 +- .../Executioner/Helper/InactiveHelper.php | 176 ++++++ .../Executioner/InactiveExecutioner.php | 242 +++++++- .../Executioner/KickoffExecutioner.php | 77 +-- .../Executioner/Logger/EventLogger.php | 13 +- .../Executioner/ScheduledExecutioner.php | 161 +++-- .../Executioner/Scheduler/EventScheduler.php | 18 +- .../Executioner/Scheduler/Mode/DateTime.php | 28 +- .../Executioner/Scheduler/Mode/Interval.php | 13 +- .../CampaignBundle/Model/EventModel.php | 430 +------------ .../CampaignBundle/Model/LegacyEventModel.php | 570 ++++++------------ .../Command/TriggerCampaignCommandTest.php | 17 +- .../Translations/en_US/messages.ini | 6 +- .../CoreBundle/Entity/CommonRepository.php | 2 +- app/config/config_test.php | 4 +- 39 files changed, 2351 insertions(+), 1255 deletions(-) create mode 100644 app/bundles/CampaignBundle/Command/ExecuteEventCommand.php create mode 100644 app/bundles/CampaignBundle/Command/ValidateEventCommand.php create mode 100644 app/bundles/CampaignBundle/Event/DecisionResultsEvent.php create mode 100644 app/bundles/CampaignBundle/Event/ExecutedBatchEvent.php create mode 100644 app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php create mode 100644 app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php create mode 100644 app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php diff --git a/app/bundles/CampaignBundle/CampaignEvents.php b/app/bundles/CampaignBundle/CampaignEvents.php index d10b2b50bec..6c39a9b75f1 100644 --- a/app/bundles/CampaignBundle/CampaignEvents.php +++ b/app/bundles/CampaignBundle/CampaignEvents.php @@ -106,6 +106,15 @@ final class CampaignEvents */ const ON_EVENT_EXECUTED = 'mautic.campaign_on_event_executed'; + /** + * The mautic.campaign_on_event_executed_batch event is dispatched when a batch of campaign events are executed. + * + * The event listener receives a Mautic\CampaignBundle\Event\ExecutedBatchEvent instance. + * + * @var string + */ + const ON_EVENT_EXECUTED_BATCH = 'mautic.campaign_on_event_executed_batch'; + /** * The mautic.campaign_on_event_scheduled event is dispatched when a campaign event is scheduled or scheduling is modified. * @@ -113,7 +122,7 @@ final class CampaignEvents * * @var string */ - const ON_EVENT_SCHEDULED = 'matuic.campaign_on_event_scheduled'; + const ON_EVENT_SCHEDULED = 'mautic.campaign_on_event_scheduled'; /** * The mautic.campaign_on_event_scheduled_batch event is dispatched when a batch of events are scheduled at once. @@ -122,7 +131,7 @@ final class CampaignEvents * * @var string */ - const ON_EVENT_SCHEDULED_BATCH = 'matuic.campaign_on_event_scheduled_batch'; + const ON_EVENT_SCHEDULED_BATCH = 'mautic.campaign_on_event_scheduled_batch'; /** * The mautic.campaign_on_event_failed event is dispatched when an event fails for whatever reason. @@ -131,7 +140,7 @@ final class CampaignEvents * * @var string */ - const ON_EVENT_FAILED = 'matuic.campaign_on_event_failed'; + const ON_EVENT_FAILED = 'mautic.campaign_on_event_failed'; /** * The mautic.campaign_on_event_decision_evaluation event is dispatched when a campaign decision is to be evaluated. @@ -142,6 +151,15 @@ final class CampaignEvents */ const ON_EVENT_DECISION_EVALUATION = 'mautic.campaign_on_event_decision_evaluation'; + /** + * The mautic.campaign_on_event_decision_evaluation_results event is dispatched when a batch of contacts were evaluted for a decision. + * + * The event listener receives a Mautic\CampaignBundle\Event\DecisionBatchEvent instance. + * + * @var string + */ + const ON_EVENT_DECISION_EVALUATION_RESULTS = 'mautic.campaign_on_event_decision_evaluation_results'; + /** * The mautic.campaign_on_event_decision_evaluation event is dispatched when a campaign decision is to be evaluated. * diff --git a/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php b/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php new file mode 100644 index 00000000000..a1d02ae06d8 --- /dev/null +++ b/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php @@ -0,0 +1,99 @@ +scheduledExecutioner = $scheduledExecutioner; + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('mautic:campaigns:execute') + ->setDescription('Execute specific scheduled events.') + ->addOption( + '--scheduled-log-ids', + null, + InputOption::VALUE_REQUIRED, + 'CSV of specific scheduled log IDs to execute.' + ); + + parent::configure(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int|null + * + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); + + $scheduledLogIds = $input->getOption('scheduled-log-ids'); + + $ids = array_map( + function ($id) { + return (int) trim($id); + }, + explode(',', $scheduledLogIds) + ); + + $counter = $this->scheduledExecutioner->executeByIds($ids, $output); + + $output->writeln( + "\n". + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) + .'' + ); + $output->writeln(''); + + return 0; + } +} diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index f5ac5c99c6f..ff6ab7b9493 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -11,26 +11,132 @@ namespace Mautic\CampaignBundle\Command; +use Doctrine\ORM\EntityManagerInterface; use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Event\CampaignTriggerEvent; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; +use Mautic\CampaignBundle\Executioner\InactiveExecutioner; use Mautic\CampaignBundle\Executioner\KickoffExecutioner; use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; +use Mautic\CampaignBundle\Model\CampaignModel; use Mautic\CoreBundle\Command\ModeratedCommand; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Translation\TranslatorInterface; /** * Class TriggerCampaignCommand. */ class TriggerCampaignCommand extends ModeratedCommand { + /** + * @var CampaignModel + */ + private $campaignModel; + /** * @var EventDispatcher */ - protected $dispatcher; + private $dispatcher; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var KickoffExecutioner + */ + private $kickoffExecutioner; + + /** + * @var ScheduledExecutioner + */ + private $scheduledExecutioner; + + /** + * @var InactiveExecutioner + */ + private $inactiveExecutioner; + + /** + * @var EntityManagerInterface + */ + private $em; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var OutputInterface + */ + protected $output; + + /** + * @var bool + */ + private $kickoffOnly = false; + + /** + * @var bool + */ + private $inactiveOnly = false; + + /** + * @var bool + */ + private $scheduleOnly = false; + + /** + * @var ContactLimiter + */ + private $limiter; + + /** + * @var Campaign + */ + private $campaign; + + /** + * TriggerCampaignCommand constructor. + * + * @param CampaignModel $campaignModel + * @param EventDispatcherInterface $dispatcher + * @param TranslatorInterface $translator + * @param KickoffExecutioner $kickoffExecutioner + * @param ScheduledExecutioner $scheduledExecutioner + * @param InactiveExecutioner $inactiveExecutioner + * @param EntityManagerInterface $em + * @param LoggerInterface $logger + */ + public function __construct( + CampaignModel $campaignModel, + EventDispatcherInterface $dispatcher, + TranslatorInterface $translator, + KickoffExecutioner $kickoffExecutioner, + ScheduledExecutioner $scheduledExecutioner, + InactiveExecutioner $inactiveExecutioner, + EntityManagerInterface $em, + LoggerInterface $logger + ) { + parent::__construct(); + + $this->campaignModel = $campaignModel; + $this->dispatcher = $dispatcher; + $this->translator = $translator; + $this->kickoffExecutioner = $kickoffExecutioner; + $this->scheduledExecutioner = $scheduledExecutioner; + $this->inactiveExecutioner = $inactiveExecutioner; + $this->em = $em; + $this->logger = $logger; + } /** * {@inheritdoc} @@ -47,78 +153,158 @@ protected function configure() 'Trigger events for a specific campaign. Otherwise, all campaigns will be triggered.', null ) - ->addOption('--scheduled-only', null, InputOption::VALUE_NONE, 'Trigger only scheduled events') - ->addOption('--negative-only', null, InputOption::VALUE_NONE, 'Trigger only negative events, i.e. with a "no" decision path.') - ->addOption('--batch-limit', '-l', InputOption::VALUE_OPTIONAL, 'Set batch size of contacts to process per round. Defaults to 100.', 100) ->addOption( - '--max-events', - '-m', + '--contact-id', + null, + InputOption::VALUE_OPTIONAL, + 'Trigger events for a specific contact.', + null + ) + ->addOption( + '--contact-ids', + null, + InputOption::VALUE_OPTIONAL, + 'CSV of contact IDs to evaluate.' + ) + ->addOption( + '--min-contact-id', + null, + InputOption::VALUE_OPTIONAL, + 'Trigger events starting at a specific contact ID.', + null + ) + ->addOption( + '--max-contact-id', + null, + InputOption::VALUE_OPTIONAL, + 'Trigger events starting up to a specific contact ID.', + null + ) + ->addOption( + '--kickoff-only', + null, + InputOption::VALUE_NONE, + 'Just kickoff the campaign' + ) + ->addOption( + '--scheduled-only', + null, + InputOption::VALUE_NONE, + 'Just execute scheduled events' + ) + ->addOption( + '--inactive-only', + null, + InputOption::VALUE_NONE, + 'Just execute scheduled events' + ) + ->addOption( + '--batch-limit', + '-l', InputOption::VALUE_OPTIONAL, - 'Set max number of events to process per campaign for this script execution. Defaults to all.', - 0 + 'Set batch size of contacts to process per round. Defaults to 100.', + 100 + ) + // @deprecated 2.13.0 to be removed in 3.0; use inactive-only instead + ->addOption( + '--negative-only', + null, + InputOption::VALUE_NONE, + 'Just execute the inactive events' ); parent::configure(); } /** - * {@inheritdoc} + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int|null + * + * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); - - /** @var \Mautic\CampaignBundle\Model\CampaignModel $campaignModel */ - $campaignModel = $container->get('mautic.campaign.model.campaign'); - $this->dispatcher = $container->get('event_dispatcher'); - $this->translator = $container->get('translator'); - $this->em = $container->get('doctrine')->getManager(); - $this->output = $output; - $id = $input->getOption('campaign-id'); - $scheduleOnly = $input->getOption('scheduled-only'); - $negativeOnly = $input->getOption('negative-only'); - $batchLimit = $input->getOption('batch-limit'); - - /* @var KickoffExecutioner $kickoff */ - $this->kickoff = $container->get('mautic.campaign.executioner.kickoff'); - /* @var ScheduledExecutioner $scheduled */ - $this->scheduled = $container->get('mautic.campaign.executioner.scheduled'); + $this->output = $output; + $this->kickoffOnly = $input->getOption('kickoff-only'); + $this->scheduleOnly = $input->getOption('scheduled-only'); + $this->inactiveOnly = $input->getOption('inactive-only') || $input->getOption('negative-only'); + + $batchLimit = $input->getOption('batch-limit'); + $contactMinId = $input->getOption('min-contact-id'); + $contactMaxId = $input->getOption('max-contact-id'); + $contactId = $input->getOption('contact-id'); + if ($contactIds = $input->getOption('contact-ids')) { + $contactIds = array_map( + function ($id) { + return (int) trim($id); + }, + explode(',', $contactIds) + ); + } + $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); + $id = $input->getOption('campaign-id'); if (!$this->checkRunStatus($input, $output, $id)) { return 0; } + // Specific campaign; if ($id) { /** @var \Mautic\CampaignBundle\Entity\Campaign $campaign */ - if (!$campaign = $campaignModel->getEntity($id)) { + if ($campaign = $this->campaignModel->getEntity($id)) { + $this->triggerCampaign($campaign); + } else { $output->writeln(''.$this->translator->trans('mautic.campaign.rebuild.not_found', ['%id%' => $id]).''); - - return 0; } - $this->triggerCampaign($campaign, $negativeOnly, $scheduleOnly, $batchLimit); - } else { - $campaigns = $campaignModel->getEntities( - [ - 'iterator_mode' => true, - ] - ); + $this->completeRun(); - while (($next = $campaigns->next()) !== false) { - // Key is ID and not 0 - $campaign = reset($next); - $this->triggerCampaign($campaign, $negativeOnly, $scheduleOnly, $batchLimit); - } + return 0; } - $this->completeRun(); + // All published campaigns + /** @var \Doctrine\ORM\Internal\Hydration\IterableResult $campaigns */ + $campaigns = $this->campaignModel->getEntities(['iterator_mode' => true]); + + while (($next = $campaigns->next()) !== false) { + // Key is ID and not 0 + $campaign = reset($next); + $this->triggerCampaign($campaign); + } return 0; } - private function triggerCampaign(Campaign $campaign, $negativeOnly, $scheduleOnly, $batchLimit) + /** + * @param Campaign $campaign + * + * @return bool + */ + protected function dispatchTriggerEvent(Campaign $campaign) + { + if ($this->dispatcher->hasListeners(CampaignEvents::CAMPAIGN_ON_TRIGGER)) { + /** @var CampaignTriggerEvent $event */ + $event = $this->dispatcher->dispatch( + CampaignEvents::CAMPAIGN_ON_TRIGGER, + new CampaignTriggerEvent($campaign) + ); + + return $event->shouldTrigger(); + } + + return true; + } + + /** + * @param Campaign $campaign + * + * @throws \Exception + */ + private function triggerCampaign(Campaign $campaign) { if (!$campaign->isPublished()) { return; @@ -128,58 +314,94 @@ private function triggerCampaign(Campaign $campaign, $negativeOnly, $scheduleOnl return; } - $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $campaign->getId()]).''); - if (!$negativeOnly && !$scheduleOnly) { - //trigger starting action events for newly added contacts - $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.starting').''); - $counter = $this->kickoff->executeForCampaign($campaign, $batchLimit, $this->output); - $this->output->writeln( - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]).'' - ."\n" - ); - } + $this->campaign = $campaign; - if (!$negativeOnly) { - $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.scheduled').''); - $counter = $this->scheduled->executeForCampaign($campaign, $batchLimit, $this->output); - $this->output->writeln( - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]).'' - ."\n" - ); - } + try { + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $campaign->getId()]).''); + if (!$this->inactiveOnly && !$this->scheduleOnly) { + $this->executeKickoff(); + } - /* - if (!$scheduleOnly) { - //find and trigger "no" path events - $output->writeln(''.$translator->trans('mautic.campaign.trigger.negative').''); - $processed = $model->triggerNegativeEvents($c, $totalProcessed, $batch, $max, $output); - $output->writeln( - ''.$translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $processed]).'' - ."\n" - ); + if (!$this->inactiveOnly && !$this->kickoffOnly) { + $this->executeScheduled(); + } + + if (!$this->scheduleOnly && !$this->kickoffOnly) { + $this->executeInactive(); + } + + // Don't detach in tests since this command will be ran multiple times in the same process + if ('test' !== MAUTIC_ENV) { + $this->em->detach($campaign); + } + } catch (\Exception $exception) { + if ('prod' !== MAUTIC_ENV) { + // Throw the exception for dev/test mode + throw $exception; + } + + $this->logger->error('CAMPAIGN: '.$exception->getMessage()); } - */ + } + + /** + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + private function executeKickoff() + { + //trigger starting action events for newly added contacts + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.starting').''); - $this->em->detach($campaign); + $counter = $this->kickoffExecutioner->execute($this->campaign, $this->limiter, $this->output); + + $this->output->writeln( + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) + .'' + ); + $this->output->writeln(''); } /** - * @param Campaign $campaign - * - * @return bool + * @throws \Doctrine\ORM\Query\QueryException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ - protected function dispatchTriggerEvent(Campaign $campaign) + private function executeScheduled() { - if ($this->dispatcher->hasListeners(CampaignEvents::CAMPAIGN_ON_TRIGGER)) { - /** @var CampaignTriggerEvent $event */ - $event = $this->dispatcher->dispatch( - CampaignEvents::CAMPAIGN_ON_TRIGGER, - new CampaignTriggerEvent($campaign) - ); + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.scheduled').''); - return $event->shouldTrigger(); - } + $counter = $this->scheduledExecutioner->execute($this->campaign, $this->limiter, $this->output); - return true; + $this->output->writeln( + "\n". + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) + .'' + ); + $this->output->writeln(''); + } + + /** + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + private function executeInactive() + { + //find and trigger "no" path events + $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.negative').''); + + $counter = $this->inactiveExecutioner->execute($this->campaign, $this->limiter, $this->output); + + $this->output->writeln( + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) + .'' + ); + $this->output->writeln(''); } } diff --git a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php new file mode 100644 index 00000000000..7b4221b0751 --- /dev/null +++ b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php @@ -0,0 +1,126 @@ +inactiveExecution = $inactiveExecutioner; + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('mautic:campaigns:validate') + ->setDescription('Validate if a contact has been inactive for a decision and execute events if so.') + ->addOption( + '--decision-id', + null, + InputOption::VALUE_REQUIRED, + 'ID of the decision to evaluate.' + ) + ->addOption( + '--contact-id', + null, + InputOption::VALUE_OPTIONAL, + 'Evaluate for specific contact' + ) + ->addOption( + '--contact-ids', + null, + InputOption::VALUE_OPTIONAL, + 'CSV of contact IDs to evaluate.' + ); + + parent::configure(); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int|null + * + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); + + $decisionId = $input->getOption('decision-id'); + $contactId = $input->getOption('contact-id'); + if ($contactIds = $input->getOption('contact-ids')) { + $contactIds = array_map( + function ($id) { + return (int) trim($id); + }, + explode(',', $contactIds) + ); + } + + if (!$contactIds && !$contactId) { + $output->writeln( + "\n". + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => 0]) + .'' + ); + + return 0; + } + + $limiter = new ContactLimiter(null, $contactId, null, null, $contactIds); + $counter = $this->inactiveExecution->validate($decisionId, $limiter, $output); + + $output->writeln( + "\n". + ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) + .'' + ); + $output->writeln(''); + + return 0; + } +} diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 689edda9ec4..4ba50f67232 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -221,12 +221,18 @@ 'mautic.campaign.model.event' => [ 'class' => 'Mautic\CampaignBundle\Model\EventModel', 'arguments' => [ - 'mautic.helper.ip_lookup', - 'mautic.lead.model.lead', - 'mautic.campaign.model.campaign', 'mautic.user.model.user', 'mautic.core.model.notification', + 'mautic.campaign.model.campaign', + 'mautic.lead.model.lead', + 'mautic.helper.ip_lookup', 'mautic.campaign.executioner.active_decision', + 'mautic.campaign.executioner.kickoff', + 'mautic.campaign.executioner.scheduled', + 'mautic.campaign.executioner.inactive', + 'mautic.campaign.executioner', + 'mautic.campaign.event_dispatcher', + 'mautic.campaign.event_collector', ], ], 'mautic.campaign.model.event_log' => [ @@ -283,6 +289,15 @@ 'mautic.lead.repository.lead', ], ], + 'mautic.campaign.contact_finder.inactive' => [ + 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts::class, + 'arguments' => [ + 'mautic.lead.repository.lead', + 'mautic.campaign.repository.campaign', + 'mautic.campaign.repository.lead', + 'monolog.logger.mautic', + ], + ], 'mautic.campaign.event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher::class, 'arguments' => [ @@ -395,6 +410,27 @@ 'mautic.campaign.scheduler', ], ], + 'mautic.campaign.executioner.inactive' => [ + 'class' => \Mautic\CampaignBundle\Executioner\InactiveExecutioner::class, + 'arguments' => [ + 'mautic.campaign.contact_finder.inactive', + 'monolog.logger.mautic', + 'translator', + 'mautic.campaign.scheduler', + 'mautic.campaign.helper.inactivity', + 'mautic.campaign.executioner', + ], + ], + 'mautic.campaign.helper.inactivity' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Helper\InactiveHelper::class, + 'arguments' => [ + 'mautic.campaign.scheduler', + 'mautic.campaign.contact_finder.inactive', + 'mautic.campaign.repository.lead_event_log', + 'mautic.campaign.repository.event', + 'monolog.logger.mautic', + ], + ], // @deprecated 2.13.0 for BC support; to be removed in 3.0 'mautic.campaign.legacy_event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher::class, @@ -407,6 +443,38 @@ ], ], ], + 'commands' => [ + 'mautic.campaign.command.trigger' => [ + 'class' => \Mautic\CampaignBundle\Command\TriggerCampaignCommand::class, + 'arguments' => [ + 'mautic.campaign.model.campaign', + 'event_dispatcher', + 'translator', + 'mautic.campaign.executioner.kickoff', + 'mautic.campaign.executioner.scheduled', + 'mautic.campaign.executioner.inactive', + 'doctrine.orm.entity_manager', + 'monolog.logger.mautic', + ], + 'tag' => 'console.command', + ], + 'mautic.campaign.command.execute' => [ + 'class' => \Mautic\CampaignBundle\Command\ExecuteEventCommand::class, + 'arguments' => [ + 'mautic.campaign.executioner.scheduled', + 'translator', + ], + 'tag' => 'console.command', + ], + 'mautic.campaign.command.validate' => [ + 'class' => \Mautic\CampaignBundle\Command\ValidateEventCommand::class, + 'arguments' => [ + 'mautic.campaign.executioner.inactive', + 'translator', + ], + 'tag' => 'console.command', + ], + ], ], 'parameters' => [ 'campaign_time_wait_on_event_false' => 'PT1H', diff --git a/app/bundles/CampaignBundle/Entity/Campaign.php b/app/bundles/CampaignBundle/Entity/Campaign.php index 739c6fce0cf..a1903ea7521 100644 --- a/app/bundles/CampaignBundle/Entity/Campaign.php +++ b/app/bundles/CampaignBundle/Entity/Campaign.php @@ -338,7 +338,7 @@ public function getEvents() } /** - * @return ArrayCollection|\Doctrine\Common\Collections\Collection + * @return ArrayCollection */ public function getRootEvents() { @@ -358,6 +358,48 @@ public function getRootEvents() return $keyedArrayCollection; } + /** + * @return ArrayCollection + */ + public function getInactionBasedEvents() + { + $criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', Event::PATH_INACTION)); + $events = $this->getEvents()->matching($criteria); + + // Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them. + // @see https://github.com/doctrine/doctrine2/issues/4693 + $keyedArrayCollection = new ArrayCollection(); + /** @var Event $event */ + foreach ($events as $event) { + $keyedArrayCollection->set($event->getId(), $event); + } + + unset($events); + + return $keyedArrayCollection; + } + + /** + * @return ArrayCollection + */ + public function getEventsByType($type) + { + $criteria = Criteria::create()->where(Criteria::expr()->eq('eventType', $type)); + $events = $this->getEvents()->matching($criteria); + + // Doctrine loses the indexBy mapping definition when using matching so we have to manually reset them. + // @see https://github.com/doctrine/doctrine2/issues/4693 + $keyedArrayCollection = new ArrayCollection(); + /** @var Event $event */ + foreach ($events as $event) { + $keyedArrayCollection->set($event->getId(), $event); + } + + unset($events); + + return $keyedArrayCollection; + } + /** * Set publishUp. * diff --git a/app/bundles/CampaignBundle/Entity/CampaignRepository.php b/app/bundles/CampaignBundle/Entity/CampaignRepository.php index 3aabfd251a8..cbf5985e3c6 100644 --- a/app/bundles/CampaignBundle/Entity/CampaignRepository.php +++ b/app/bundles/CampaignBundle/Entity/CampaignRepository.php @@ -11,6 +11,7 @@ namespace Mautic\CampaignBundle\Entity; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CoreBundle\Entity\CommonRepository; /** @@ -527,15 +528,13 @@ public function getCampaignOrphanLeads($id, array $lists, $args = []) } /** - * Get a count of leads that belong to the campaign. - * - * @param $campaignId - * @param int $leadId Optional lead ID to check if lead is part of campaign - * @param array $pendingEvents List of specific events to rule out + * @param $campaignId + * @param array $pendingEvents + * @param ContactLimiter $limiter * - * @return mixed + * @return int */ - public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents = []) + public function getPendingEventContactCount($campaignId, array $pendingEvents, ContactLimiter $limiter) { $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); @@ -549,10 +548,20 @@ public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents ) ->setParameter('false', false, 'boolean'); - if ($leadId) { + if ($leadId = $limiter->getContactId()) { $q->andWhere( $q->expr()->eq('cl.lead_id', (int) $leadId) ); + } elseif ($minContactId = $limiter->getMinContactId()) { + $q->andWhere( + 'cl.lead_id BETWEEN :minContactId AND :maxContactId' + ) + ->setParameter('minContactId', $minContactId) + ->setParameter('maxContactId', $limiter->getMaxContactId()); + } elseif ($contactIds = $limiter->getContactIdList()) { + $q->andWhere( + $q->expr()->in('cl.lead_id', $contactIds) + ); } if (count($pendingEvents) > 0) { @@ -577,16 +586,14 @@ public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents } /** - * Get lead IDs of a campaign. + * Get pending contact IDs for a campaign. * - * @param $campaignId - * @param int $start - * @param bool|false $limit - * @param bool|false getCampaignLeadIds + * @param $campaignId + * @param ContactLimiter $limiter * * @return array */ - public function getCampaignLeadIds($campaignId, $start = 0, $limit = false, $pendingOnly = false) + public function getPendingContactIds($campaignId, ContactLimiter $limiter) { $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); @@ -601,32 +608,41 @@ public function getCampaignLeadIds($campaignId, $start = 0, $limit = false, $pen ->setParameter('false', false, 'boolean') ->orderBy('cl.lead_id', 'ASC'); - if ($pendingOnly) { - // Only leads that have not started the campaign - $sq = $this->getEntityManager()->getConnection()->createQueryBuilder(); - - $sq->select('null') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') - ->where( - $sq->expr()->andX( - $sq->expr()->eq('e.lead_id', 'cl.lead_id'), - $sq->expr()->eq('e.campaign_id', ':campaignId'), - $sq->expr()->eq('e.rotation', 'cl.rotation') - ) - ); - + if ($contactId = $limiter->getContactId()) { $q->andWhere( - sprintf('NOT EXISTS (%s)', $sq->getSQL()) + $q->expr()->eq('cl.lead_id', $contactId) + ); + } elseif ($minContactId = $limiter->getMinContactId()) { + $q->andWhere( + 'cl.lead_id BETWEEN :minContactId AND :maxContactId' ) - ->setParameter('campaignId', (int) $campaignId); + ->setParameter('minContactId', $minContactId) + ->setParameter('maxContactId', $limiter->getMaxContactId()); + } elseif ($contactIds = $limiter->getContactIdList()) { + $q->andWhere( + $q->expr()->in('cl.lead_id', $contactIds) + ); } - if (!empty($limit)) { - $q->setMaxResults($limit); - } + // Only leads that have not started the campaign + $sq = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $sq->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') + ->where( + $sq->expr()->andX( + $sq->expr()->eq('e.lead_id', 'cl.lead_id'), + $sq->expr()->eq('e.campaign_id', ':campaignId'), + $sq->expr()->eq('e.rotation', 'cl.rotation') + ) + ); - if (!$pendingOnly && $start) { - $q->setFirstResult($start); + $q->andWhere( + sprintf('NOT EXISTS (%s)', $sq->getSQL()) + ) + ->setParameter('campaignId', (int) $campaignId); + + if ($limit = $limiter->getBatchLimit()) { + $q->setMaxResults($limit); } $results = $q->execute()->fetchAll(); @@ -641,6 +657,56 @@ public function getCampaignLeadIds($campaignId, $start = 0, $limit = false, $pen return $leads; } + /** + * Get a count of leads that belong to the campaign. + * + * @param $campaignId + * @param int $leadId Optional lead ID to check if lead is part of campaign + * @param array $pendingEvents List of specific events to rule out + * + * @return mixed + */ + public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents = []) + { + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $q->select('count(cl.lead_id) as lead_count') + ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') + ->where( + $q->expr()->andX( + $q->expr()->eq('cl.campaign_id', (int) $campaignId), + $q->expr()->eq('cl.manually_removed', ':false') + ) + ) + ->setParameter('false', false, 'boolean'); + + if ($leadId) { + $q->andWhere( + $q->expr()->eq('cl.lead_id', (int) $leadId) + ); + } + + if (count($pendingEvents) > 0) { + $sq = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $sq->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') + ->where( + $sq->expr()->andX( + $sq->expr()->eq('cl.lead_id', 'e.lead_id'), + $sq->expr()->in('e.event_id', $pendingEvents) + ) + ); + + $q->andWhere( + sprintf('NOT EXISTS (%s)', $sq->getSQL()) + ); + } + + $results = $q->execute()->fetchAll(); + + return (int) $results[0]['lead_count']; + } + /** * Get lead data of a campaign. * @@ -675,4 +741,71 @@ public function getCampaignLeads($campaignId, $start = 0, $limit = false, $selec return $results; } + + /** + * Get lead IDs of a campaign. + * + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param $campaignId + * @param int $start + * @param bool|false $limit + * @param bool|false getCampaignLeadIds + * + * @return array + */ + public function getCampaignLeadIds($campaignId, $start = 0, $limit = false, $pendingOnly = false) + { + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $q->select('cl.lead_id') + ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl') + ->where( + $q->expr()->andX( + $q->expr()->eq('cl.campaign_id', (int) $campaignId), + $q->expr()->eq('cl.manually_removed', ':false') + ) + ) + ->setParameter('false', false, 'boolean') + ->orderBy('cl.lead_id', 'ASC'); + + if ($pendingOnly) { + // Only leads that have not started the campaign + $sq = $this->getEntityManager()->getConnection()->createQueryBuilder(); + + $sq->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'e') + ->where( + $sq->expr()->andX( + $sq->expr()->eq('e.lead_id', 'cl.lead_id'), + $sq->expr()->eq('e.campaign_id', ':campaignId'), + $sq->expr()->eq('e.rotation', 'cl.rotation') + ) + ); + + $q->andWhere( + sprintf('NOT EXISTS (%s)', $sq->getSQL()) + ) + ->setParameter('campaignId', (int) $campaignId); + } + + if (!empty($limit)) { + $q->setMaxResults($limit); + } + + if (!$pendingOnly && $start) { + $q->setFirstResult($start); + } + + $results = $q->execute()->fetchAll(); + + $leads = []; + foreach ($results as $r) { + $leads[] = $r['lead_id']; + } + + unset($results); + + return $leads; + } } diff --git a/app/bundles/CampaignBundle/Entity/Event.php b/app/bundles/CampaignBundle/Entity/Event.php index 7b9aeb6153e..84b9a650bb6 100644 --- a/app/bundles/CampaignBundle/Entity/Event.php +++ b/app/bundles/CampaignBundle/Entity/Event.php @@ -604,7 +604,7 @@ public function removeChild(Event $children) } /** - * @return ArrayCollection + * @return ArrayCollection|Event[] */ public function getChildren() { @@ -612,7 +612,7 @@ public function getChildren() } /** - * @return ArrayCollection + * @return ArrayCollection|Event[] */ public function getPositiveChildren() { @@ -622,11 +622,11 @@ public function getPositiveChildren() } /** - * @return ArrayCollection + * @return ArrayCollection|Event[] */ public function getNegativeChildren() { - $criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', self::PATH_ACTION)); + $criteria = Criteria::create()->where(Criteria::expr()->eq('decisionPath', self::PATH_INACTION)); return $this->getChildren()->matching($criteria); } diff --git a/app/bundles/CampaignBundle/Entity/EventRepository.php b/app/bundles/CampaignBundle/Entity/EventRepository.php index 107309e3857..3b3318c1f8b 100644 --- a/app/bundles/CampaignBundle/Entity/EventRepository.php +++ b/app/bundles/CampaignBundle/Entity/EventRepository.php @@ -62,8 +62,8 @@ public function getContactPendingEvents($contactId, $type) ->from(LeadEventLog::class, 'log_event') ->where( $eventQb->expr()->andX( - $eventQb->expr()->eq('IDENTITY(log_event.event)', 'IDENTITY(e.parent)'), - $eventQb->expr()->eq('IDENTITY(log_event.lead)', 'IDENTITY(l.lead)'), + $eventQb->expr()->eq('log_event.event', 'e'), + $eventQb->expr()->eq('log_event.lead', 'l.lead'), $eventQb->expr()->eq('log_event.rotation', 'l.rotation') ) ); @@ -73,8 +73,8 @@ public function getContactPendingEvents($contactId, $type) $parentQb->select('parent_log_event.id') ->from(LeadEventLog::class, 'parent_log_event') ->where( - $parentQb->expr()->eq('IDENTITY(parent_log_event.event)', 'IDENTITY(e.parent)'), - $parentQb->expr()->eq('IDENTITY(parent_log_event.lead)', 'IDENTITY(l.lead)'), + $parentQb->expr()->eq('parent_log_event.event', 'e.parent'), + $parentQb->expr()->eq('parent_log_event.lead', 'l.lead'), $parentQb->expr()->eq('parent_log_event.rotation', 'l.rotation') ); diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLog.php b/app/bundles/CampaignBundle/Entity/LeadEventLog.php index 7c979e79913..0ae5bf45762 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLog.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLog.php @@ -298,7 +298,7 @@ public function getEvent() * * @return $this */ - public function setEvent($event) + public function setEvent(Event $event) { $this->event = $event; diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index 626657b5242..7f9dde6c405 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -12,6 +12,7 @@ namespace Mautic\CampaignBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CoreBundle\Entity\CommonRepository; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\LeadBundle\Entity\TimelineTrait; @@ -385,72 +386,17 @@ public function getChartQuery($options) } /** - * @param $contactId - * - * @return array - */ - public function getInactive($campaignId) - { - // Limit to events that hasn't been executed or scheduled yet - $eventQb = $this->getEntityManager()->createQueryBuilder(); - $eventQb->select('IDENTITY(log_event.event)') - ->from(LeadEventLog::class, 'log_event') - ->where( - $eventQb->expr()->andX( - $eventQb->expr()->eq('IDENTITY(log_event.event)', 'IDENTITY(e.parent)'), - $eventQb->expr()->eq('IDENTITY(log_event.lead)', 'IDENTITY(l.lead)'), - $eventQb->expr()->eq('log_event.rotation', 'l.rotation') - ) - ); - - // Limit to events that has a decision parent - $parentQb = $this->getEntityManager()->createQueryBuilder(); - $parentQb->select('parent_log_event.id') - ->from(LeadEventLog::class, 'parent_log_event') - ->where( - $parentQb->expr()->eq('IDENTITY(parent_log_event.event)', 'IDENTITY(e.parent)'), - $parentQb->expr()->eq('IDENTITY(parent_log_event.lead)', 'IDENTITY(l.lead)'), - $parentQb->expr()->eq('parent_log_event.rotation', 'l.rotation') - ); - - $q = $this->createQueryBuilder('l', 'l.id'); - $q->select('e,c') - ->innerJoin('e.campaign', 'c') - ->innerJoin('c.leads', 'l') - ->where( - $q->expr()->andX( - $q->expr()->eq('c.isPublished', 1), - $q->expr()->eq('e.type', ':type'), - $q->expr()->eq('IDENTITY(l.lead)', ':contactId'), - $q->expr()->eq('l.manuallyRemoved', 0), - $q->expr()->notIn('e.id', $eventQb->getDQL()), - $q->expr()->orX( - $q->expr()->isNull('e.parent'), - $q->expr()->exists($parentQb->getDQL()) - ) - ) - ) - ->setParameter('type', $type) - ->setParameter('contactId', (int) $contactId); - - return $q->getQuery()->getResult(); - } - - /** - * Get a list of scheduled events. - * - * @param $eventId - * @param null $limit - * @param null $contactId + * @param $eventId + * @param \DateTime $now + * @param ContactLimiter $limiter * * @return ArrayCollection * * @throws \Doctrine\ORM\Query\QueryException */ - public function getScheduled($eventId, $limit = null, $contactId = null) + public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter) { - $date = new \Datetime(); - $q = $this->createQueryBuilder('o'); + $q = $this->createQueryBuilder('o'); $q->select('o, e, c') ->indexBy('o', 'o.id') @@ -460,57 +406,109 @@ public function getScheduled($eventId, $limit = null, $contactId = null) $q->expr()->andX( $q->expr()->eq('IDENTITY(o.event)', ':eventId'), $q->expr()->eq('o.isScheduled', ':true'), - $q->expr()->lte('o.triggerDate', ':now') + $q->expr()->lte('o.triggerDate', ':now'), + $q->expr()->eq('c.isPublished', 1) ) ) ->setParameter('eventId', (int) $eventId) - ->setParameter('now', $date) + ->setParameter('now', $now) ->setParameter('true', true, 'boolean'); - if ($contactId) { + if ($contactId = $limiter->getContactId()) { $q->andWhere( $q->expr()->eq('IDENTITY(o.lead)', ':contactId') ) ->setParameter('contactId', (int) $contactId); + } elseif ($minContactId = $limiter->getMinContactId()) { + $q->andWhere( + $q->expr()->between('IDENTITY(o.lead)', ':minContactId', ':maxContactId') + ) + ->setParameter('minContactId', $minContactId) + ->setParameter('maxContactId', $limiter->getMaxContactId()); + } elseif ($contactIds = $limiter->getContactIdList()) { + $q->andWhere( + $q->expr()->in('IDENTITY(o.lead)', $contactIds) + ); } - if ($limit) { - $q->setFirstResult(0) - ->setMaxResults($limit); + if ($limit = $limiter->getBatchLimit()) { + $q->setMaxResults($limit); } return new ArrayCollection($q->getQuery()->getResult()); } /** - * @param $campaignId + * @param $campaignId + * @param array $ids + * + * @return ArrayCollection + * + * @throws \Doctrine\ORM\Query\QueryException + */ + public function getScheduledById(array $ids) + { + $q = $this->createQueryBuilder('o'); + + $q->select('o, e, c') + ->indexBy('o', 'o.id') + ->innerJoin('o.event', 'e') + ->innerJoin('o.campaign', 'c') + ->where( + $q->expr()->andX( + $q->expr()->in('o.id', $ids), + $q->expr()->eq('o.isScheduled', 1), + $q->expr()->eq('c.isPublished', 1) + ) + ); + + return new ArrayCollection($q->getQuery()->getResult()); + } + + /** + * @param $campaignId + * @param \DateTime $date + * @param ContactLimiter $limiter * * @return array */ - public function getScheduledCounts($campaignId, $contactId = null) + public function getScheduledCounts($campaignId, \DateTime $date, ContactLimiter $limiter) { - $date = new \Datetime('now', new \DateTimeZone('UTC')); + $now = clone $date; + $now->setTimezone(new \DateTimeZone('UTC')); $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); $expr = $q->expr()->andX( $q->expr()->eq('l.campaign_id', ':campaignId'), $q->expr()->eq('l.is_scheduled', ':true'), - $q->expr()->lte('l.trigger_date', ':now') + $q->expr()->lte('l.trigger_date', ':now'), + $q->expr()->eq('c.is_published', 1) ); - if ($contactId) { + if ($contactId = $limiter->getContactId()) { $expr->add( $q->expr()->eq('l.lead_id', ':contactId') ); $q->setParameter('contactId', (int) $contactId); + } elseif ($minContactId = $limiter->getMinContactId()) { + $q->andWhere( + 'l.lead_id BETWEEN :minContactId AND :maxContactId' + ) + ->setParameter('minContactId', $minContactId) + ->setParameter('maxContactId', $limiter->getMaxContactId()); + } elseif ($contactIds = $limiter->getContactIdList()) { + $q->andWhere( + $q->expr()->in('l.lead_id', $contactIds) + ); } $results = $q->select('COUNT(*) as event_count, l.event_id') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'l') + ->join('l', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'l.campaign_id = c.id') ->where($expr) ->setParameter('campaignId', $campaignId) - ->setParameter('now', $date->format('Y-m-d H:i:s')) + ->setParameter('now', $now->format('Y-m-d H:i:s')) ->setParameter('true', true, \PDO::PARAM_BOOL) ->groupBy('l.event_id') ->execute() @@ -524,4 +522,33 @@ public function getScheduledCounts($campaignId, $contactId = null) return $events; } + + /** + * @param $eventId + * @param array $contactIds + * + * @return array + */ + public function getDatesExecuted($eventId, array $contactIds) + { + $qb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $qb->select('log.lead_id, log.date_triggered') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('log.event_id', $eventId), + $qb->expr()->eq('log.is_scheduled', 0), + $qb->expr()->in('log.lead_id', $contactIds) + ) + ); + + $results = $qb->execute()->fetchAll(); + + $dates = []; + foreach ($results as $result) { + $dates[$result['lead_id']] = new \DateTime($result['date_triggered'], new \DateTimeZone('UTC')); + } + + return $dates; + } } diff --git a/app/bundles/CampaignBundle/Entity/LeadRepository.php b/app/bundles/CampaignBundle/Entity/LeadRepository.php index a33f3591f39..0abeabf0c35 100644 --- a/app/bundles/CampaignBundle/Entity/LeadRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadRepository.php @@ -11,6 +11,7 @@ namespace Mautic\CampaignBundle\Entity; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CoreBundle\Entity\CommonRepository; /** @@ -189,4 +190,164 @@ public function checkLeadInCampaigns($lead, $options = []) return (bool) $q->execute()->fetchColumn(); } + + /** + * @param $campaignId + * @param $decisionId + * @param $parentDecisionId + * @param $startAtContactId + * @param ContactLimiter $limiter + * + * @return array + */ + public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, $startAtContactId, ContactLimiter $limiter) + { + // Main query + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $q->select('l.lead_id, l.date_added') + ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') + // Order by ID so we can query by greater than X contact ID when batching + ->orderBy('l.lead_id') + ->setMaxResults($limiter->getBatchLimit()) + ->setParameter('campaignId', (int) $campaignId) + ->setParameter('decisionId', (int) $decisionId); + + // Contact IDs + $expr = $q->expr()->andX(); + if ($specificContactId = $limiter->getContactId()) { + // Still query for this ID in case the ID fed to the command no longer exists + $expr->add( + $q->expr()->eq('l.lead_id', ':contactId') + ); + $q->setParameter('contactId', (int) $specificContactId); + } elseif ($contactIds = $limiter->getContactIdList()) { + $expr->add( + $q->expr()->in('l.lead_id', $contactIds) + ); + } else { + $expr->add( + $q->expr()->gt('l.lead_id', ':minContactId') + ); + $q->setParameter('minContactId', (int) $startAtContactId); + + if ($maxContactId = $limiter->getMaxContactId()) { + $expr->add( + $q->expr()->lte('l.lead_id', ':maxContactId') + ); + $q->setParameter('maxContactId', $maxContactId); + } + } + + // Limit to specific campaign + $expr->add( + $q->expr()->eq('l.campaign_id', ':campaignId') + ); + + // Limit to events that have not been executed or scheduled yet + $eventQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $eventQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') + ->where( + $eventQb->expr()->andX( + $eventQb->expr()->eq('log.event_id', ':decisionId'), + $eventQb->expr()->eq('log.lead_id', 'l.lead_id'), + $eventQb->expr()->eq('log.rotation', 'l.rotation') + ) + ); + $expr->add( + sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) + ); + + if ($parentDecisionId) { + // Limit to events that have no grandparent or whose grandparent has already been executed + $grandparentQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $grandparentQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'grandparent_log') + ->where( + $grandparentQb->expr()->eq('grandparent_log.event_id', ':grandparentId'), + $grandparentQb->expr()->eq('grandparent_log.lead_id', 'l.lead_id'), + $grandparentQb->expr()->eq('grandparent_log.rotation', 'l.rotation') + ); + $q->setParameter('grandparentId', (int) $parentDecisionId); + + $expr->add( + sprintf('EXISTS (%s)', $grandparentQb->getSQL()) + ); + } + + $q->where($expr); + + $results = $q->execute()->fetchAll(); + $contacts = []; + foreach ($results as $result) { + $contacts[$result['lead_id']] = new \DateTime($result['date_added'], new \DateTimeZone('UTC')); + } + + return $contacts; + } + + /** + * This is approximate because the query that fetches contacts per decision is based on if the grandparent has been executed or not. + * + * @param $decisionId + * @param $parentDecisionId + * @param null $specificContactId + * + * @return int + */ + public function getInactiveContactCount($campaignId, array $decisionIds, ContactLimiter $limiter) + { + // Main query + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $q->select('count(*)') + ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') + // Order by ID so we can query by greater than X contact ID when batching + ->orderBy('l.lead_id') + ->setParameter('campaignId', (int) $campaignId); + + // Contact IDs + $expr = $q->expr()->andX(); + if ($specificContactId = $limiter->getContactId()) { + // Still query for this ID in case the ID fed to the command no longer exists + $expr->add( + $q->expr()->eq('l.lead_id', ':contactId') + ); + $q->setParameter('contactId', $specificContactId); + } elseif ($minContactId = $limiter->getMinContactId()) { + // Still query for this ID in case the ID fed to the command no longer exists + $expr->add( + 'l.lead_id BETWEEN :minContactId AND :maxContactId' + ); + $q->setParameter('minContactId', $limiter->getMinContactId()) + ->setParameter('maxContactId', $limiter->getMaxContactId()); + } elseif ($contactIds = $limiter->getContactIdList()) { + $expr->add( + $q->expr()->in('l.lead_id', $contactIds) + ); + } + + // Limit to specific campaign + $expr->add( + $q->expr()->eq('l.campaign_id', ':campaignId') + ); + + // Limit to events that have not been executed or scheduled yet + $eventQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $eventQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') + ->where( + $eventQb->expr()->andX( + $eventQb->expr()->in('log.event_id', $decisionIds), + $eventQb->expr()->eq('log.lead_id', 'l.lead_id'), + $eventQb->expr()->eq('log.rotation', 'l.rotation') + ) + ); + $expr->add( + sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) + ); + + $q->where($expr); + + return (int) $q->execute()->fetchColumn(); + } } diff --git a/app/bundles/CampaignBundle/Event/DecisionResultsEvent.php b/app/bundles/CampaignBundle/Event/DecisionResultsEvent.php new file mode 100644 index 00000000000..147c12f63bd --- /dev/null +++ b/app/bundles/CampaignBundle/Event/DecisionResultsEvent.php @@ -0,0 +1,74 @@ +eventConfig = $config; + $this->eventLogs = $logs; + $this->evaluatedContacts = $evaluatedContacts; + } + + /** + * @return AbstractEventAccessor + */ + public function getEventConfig() + { + return $this->eventConfig; + } + + /** + * @return ArrayCollection|LeadEventLog[] + */ + public function getLogs() + { + return $this->eventLogs; + } + + /** + * @return EvaluatedContacts + */ + public function getEvaluatedContacts() + { + return $this->evaluatedContacts; + } +} diff --git a/app/bundles/CampaignBundle/Event/ExecutedBatchEvent.php b/app/bundles/CampaignBundle/Event/ExecutedBatchEvent.php new file mode 100644 index 00000000000..3308f0e0d43 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/ExecutedBatchEvent.php @@ -0,0 +1,25 @@ +logs; + } +} diff --git a/app/bundles/CampaignBundle/Event/ScheduledEvent.php b/app/bundles/CampaignBundle/Event/ScheduledEvent.php index 6561e4579e6..ebb5eb25e5a 100644 --- a/app/bundles/CampaignBundle/Event/ScheduledEvent.php +++ b/app/bundles/CampaignBundle/Event/ScheduledEvent.php @@ -28,16 +28,22 @@ class ScheduledEvent extends CampaignScheduledEvent */ private $eventLog; + /** + * @var bool + */ + private $isReschedule; + /** * ScheduledEvent constructor. * * @param AbstractEventAccessor $config * @param LeadEventLog $log */ - public function __construct(AbstractEventAccessor $config, LeadEventLog $log) + public function __construct(AbstractEventAccessor $config, LeadEventLog $log, $isReschedule = false) { - $this->eventConfig = $config; - $this->eventLog = $log; + $this->eventConfig = $config; + $this->eventLog = $log; + $this->isReschedule = $isReschedule; // @deprecated support for pre 2.13.0; to be removed in 3.0 parent::__construct( @@ -68,4 +74,12 @@ public function getLog() { return $this->eventLog; } + + /** + * @return bool + */ + public function isReschedule() + { + return $this->isReschedule; + } } diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php index 43738b9b102..e19beb7726c 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php @@ -11,6 +11,126 @@ namespace Mautic\CampaignBundle\Executioner\ContactFinder; +use Doctrine\Common\Collections\ArrayCollection; +use Mautic\CampaignBundle\Entity\CampaignRepository; +use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadRepository as CampaignLeadRepository; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; +use Mautic\LeadBundle\Entity\LeadRepository; +use Psr\Log\LoggerInterface; + class InactiveContacts { + /** + * @var LeadRepository + */ + private $leadRepository; + + /** + * @var CampaignRepository + */ + private $campaignRepository; + + /** + * @var CampaignLeadRepository + */ + private $campaignLeadRepository; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ArrayCollection + */ + private $campaignMemberDatesAdded; + + /** + * InactiveContacts constructor. + * + * @param LeadRepository $leadRepository + * @param CampaignRepository $campaignRepository + * @param CampaignLeadRepository $campaignLeadRepository + * @param LoggerInterface $logger + */ + public function __construct(LeadRepository $leadRepository, CampaignRepository $campaignRepository, CampaignLeadRepository $campaignLeadRepository, LoggerInterface $logger) + { + $this->leadRepository = $leadRepository; + $this->campaignRepository = $campaignRepository; + $this->campaignLeadRepository = $campaignLeadRepository; + $this->logger = $logger; + } + + /** + * @param $campaignId + * @param Event $decisionEvent + * @param $startAtContactId + * @param ContactLimiter $limiter + * + * @return ArrayCollection + * + * @throws NoContactsFound + */ + public function getContacts($campaignId, Event $decisionEvent, $startAtContactId, ContactLimiter $limiter) + { + // Get list of all campaign leads + $decisionParentEvent = $decisionEvent->getParent(); + $this->campaignMemberDatesAdded = $this->campaignLeadRepository->getInactiveContacts( + $campaignId, + $decisionEvent->getId(), + ($decisionParentEvent) ? $decisionParentEvent->getId() : null, + $startAtContactId, + $limiter + ); + + if (empty($this->campaignMemberDatesAdded)) { + // No new contacts found in the campaign + throw new NoContactsFound(); + } + + $campaignContacts = array_keys($this->campaignMemberDatesAdded); + $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignContacts)); + + // Fetch entity objects for the found contacts + $contacts = $this->leadRepository->getContactCollection($campaignContacts); + + if (!count($contacts)) { + // Just a precaution in case non-existent contacts are lingering in the campaign leads table + $this->logger->debug('CAMPAIGN: No contact entities found.'); + + throw new NoContactsFound(); + } + + return $contacts; + } + + /** + * @return ArrayCollection + */ + public function getDatesAdded() + { + return $this->campaignMemberDatesAdded; + } + + /** + * @param $campaignId + * @param array $decisionEvents + * @param null $specificContactId + * + * @return int + */ + public function getContactCount($campaignId, array $decisionEvents, ContactLimiter $limiter) + { + return $this->campaignLeadRepository->getInactiveContactCount($campaignId, $decisionEvents, $limiter); + } + + /** + * Clear Lead entities from memory. + */ + public function clear() + { + $this->leadRepository->clear(); + } } diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php index 377a2aaeb62..c805a4e7f08 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php @@ -13,6 +13,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\CampaignRepository; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; @@ -50,29 +51,28 @@ public function __construct(LeadRepository $leadRepository, CampaignRepository $ } /** - * @param $campaignId - * @param $limit - * @param null $specificContactId + * @param $campaignId + * @param ContactLimiter $limiter * - * @return Lead[]|ArrayCollection + * @return ArrayCollection * * @throws NoContactsFound */ - public function getContacts($campaignId, $limit, $specificContactId = null) + public function getContacts($campaignId, ContactLimiter $limiter) { // Get list of all campaign leads; start is always zero in practice because of $pendingOnly - if ($campaignLeads = ($specificContactId) ? [$specificContactId] : $this->campaignRepository->getCampaignLeadIds($campaignId, 0, $limit, true)) { - $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignLeads)); - } + $campaignContacts = $this->campaignRepository->getPendingContactIds($campaignId, $limiter); - if (empty($campaignLeads)) { + if (empty($campaignContacts)) { // No new contacts found in the campaign throw new NoContactsFound(); } + $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignContacts)); + // Fetch entity objects for the found contacts - $contacts = $this->leadRepository->getContactCollection($campaignLeads); + $contacts = $this->leadRepository->getContactCollection($campaignContacts); if (!count($contacts)) { // Just a precaution in case non-existent contacts are lingering in the campaign leads table @@ -91,9 +91,9 @@ public function getContacts($campaignId, $limit, $specificContactId = null) * * @return mixed */ - public function getContactCount($campaignId, array $eventIds, $specificContactId = null) + public function getContactCount($campaignId, array $eventIds, ContactLimiter $limiter) { - return $this->campaignRepository->getCampaignLeadCount($campaignId, $specificContactId, $eventIds); + return $this->campaignRepository->getPendingEventContactCount($campaignId, $eventIds, $limiter); } /** diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php new file mode 100644 index 00000000000..cf91d95e05d --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -0,0 +1,99 @@ +batchLimit = ($batchLimit) ? (int) $batchLimit : 100; + $this->contactId = ($contactId) ? (int) $contactId : null; + $this->minContactId = ($minContactId) ? (int) $minContactId : null; + $this->maxContactId = ($maxContactId) ? (int) $maxContactId : null; + $this->contactIdList = $contactIdList; + } + + /** + * @return int|null + */ + public function getBatchLimit() + { + return $this->batchLimit; + } + + /** + * @return int|null + */ + public function getContactId() + { + return $this->contactId; + } + + /** + * @return int|null + */ + public function getMinContactId() + { + return $this->minContactId; + } + + /** + * @return int|null + */ + public function getMaxContactId() + { + return $this->maxContactId; + } + + /** + * @return array + */ + public function getContactIdList() + { + return $this->contactIdList; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php b/app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php new file mode 100644 index 00000000000..9c20235806a --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php @@ -0,0 +1,17 @@ +dispatcher->dispatch($customEvent, $pendingEvent); $success = $pendingEvent->getSuccessful(); - $this->dispatchExecutedEvent($config, $success); + $this->dispatchExecutedEvent($config, $event, $success); $failed = $pendingEvent->getFailures(); - $this->dispatchedFailedEvent($config, $failed); + $this->dispatchedFailedEvent($config, $event, $failed); $this->validateProcessedLogs($logs, $success, $failed); @@ -124,6 +126,19 @@ public function dispatchDecisionEvent(DecisionAccessor $config, LeadEventLog $lo return $event; } + /** + * @param $config + * @param $logs + * @param $evaluatedContacts + */ + public function dispatchDecisionResultsEvent($config, $logs, $evaluatedContacts) + { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, + new DecisionResultsEvent($config, $logs, $evaluatedContacts) + ); + } + /** * @param ConditionAccessor $config * @param LeadEventLog $log @@ -143,7 +158,7 @@ public function dispatchConditionEvent(ConditionAccessor $config, LeadEventLog $ * @param AbstractEventAccessor $config * @param ArrayCollection $logs */ - public function dispatchExecutedEvent(AbstractEventAccessor $config, ArrayCollection $logs) + public function dispatchExecutedEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) { foreach ($logs as $log) { $this->dispatcher->dispatch( @@ -151,6 +166,11 @@ public function dispatchExecutedEvent(AbstractEventAccessor $config, ArrayCollec new ExecutedEvent($config, $log) ); } + + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_EXECUTED_BATCH, + new ExecutedBatchEvent($config, $event, $logs) + ); } /** diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 47becc132c5..3ac57ab6fa3 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -20,6 +20,7 @@ use Mautic\CampaignBundle\Event\CampaignExecutionEvent; use Mautic\CampaignBundle\Event\DecisionEvent; use Mautic\CampaignBundle\Event\EventArrayTrait; +use Mautic\CampaignBundle\Event\ExecutedBatchEvent; use Mautic\CampaignBundle\Event\ExecutedEvent; use Mautic\CampaignBundle\Event\FailedEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; @@ -128,6 +129,8 @@ public function dispatchCustomEvent(AbstractEventAccessor $config, ArrayCollecti continue; } + $this->processSuccessLog($log); + $this->dispatchExecutedEvent($config, $log); } } @@ -295,6 +298,13 @@ private function dispatchExecutedEvent(AbstractEventAccessor $config, LeadEventL CampaignEvents::ON_EVENT_EXECUTED, new ExecutedEvent($config, $log) ); + + $collection = new ArrayCollection(); + $collection->set($log->getId(), $log); + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_EXECUTED_BATCH, + new ExecutedBatchEvent($config, $log->getEvent(), $collection) + ); } /** @@ -358,4 +368,22 @@ private function processFailedLog($result, LeadEventLog $log) $log->setFailedLog($failedLog); } + + /** + * @param LeadEventLog $log + */ + private function processSuccessLog(LeadEventLog $log) + { + if ($failedLog = $log->getFailedLog()) { + // Delete existing entries + $failedLog->setLog(null); + $log->setFailedLog(null); + } + + $metadata = $log->getMetadata(); + unset($metadata['errors']); + $log->setMetadata($metadata); + + $log->setIsScheduled(false); + } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Decision.php b/app/bundles/CampaignBundle/Executioner/Event/Decision.php index bc0dca556b8..f11504cb66a 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Decision.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Decision.php @@ -75,6 +75,8 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs } } + $this->dispatcher->dispatchDecisionResultsEvent($config, $logs, $evaluatedContacts); + return $evaluatedContacts; } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 58785fcb319..3f0c1a79e71 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -69,6 +69,11 @@ class EventExecutioner */ private $now; + /** + * @var Responses + */ + private $responses; + /** * EventExecutioner constructor. * @@ -100,34 +105,45 @@ public function __construct( } /** - * @param Event $event - * @param Lead $contact - * - * @return ArrayCollection + * @param \DateTime $now + */ + public function setNow(\DateTime $now) + { + $this->now = $now; + } + + /** + * @param Event $event + * @param Lead $contact + * @param Responses|null $responses + * @param Counter|null $counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeForContact(Event $event, Lead $contact, Responses $responses = null) + public function executeForContact(Event $event, Lead $contact, Responses $responses = null, Counter $counter = null) { + $this->responses = $responses; + $contacts = new ArrayCollection([$contact->getId() => $contact]); - $this->executeForContacts($event, $contacts, null, $responses); + $this->executeForContacts($event, $contacts, $counter); } /** * @param Event $event * @param ArrayCollection $contacts * @param Counter|null $counter + * @param bool $inactive * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, Responses $responses = null) + public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, $inactive = false) { if (!$contacts->count()) { $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); @@ -136,18 +152,9 @@ public function executeForContacts(Event $event, ArrayCollection $contacts, Coun } $config = $this->collector->getEventConfig($event); - $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $inactive); $this->executeLogs($event, $logs, $counter); - - if ($responses) { - // Extract responses - $responses->setFromLogs($logs); - } - - // Save updated log entries and clear from memory - $this->eventLogger->persistCollection($logs) - ->clear(); } /** @@ -191,6 +198,21 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte } } + /** + * @param Event $event + * @param ArrayCollection $contacts + * @param bool $inactive + */ + public function recordLogsAsExecutedForEvent(Event $event, ArrayCollection $contacts, $inactive = false) + { + $config = $this->collector->getEventConfig($event); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $inactive); + + // Save updated log entries and clear from memory + $this->eventLogger->persistCollection($logs) + ->clear(); + } + /** * @param Event $event * @param EvaluatedContacts $contacts @@ -218,6 +240,7 @@ public function executeContactsForDecisionPathChildren(Event $event, EvaluatedCo $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $negative->getKeys()).' failed evaluation for event ID '.$event->getId()); $children = $event->getNegativeChildren(); + $childrenCounter->advanceEvaluated($children->count()); $this->executeContactsForChildren($children, $negative, $childrenCounter); } @@ -255,17 +278,17 @@ public function executeContactsForConditionChildren(Event $event, ArrayCollectio } /** - * @param Event $event * @param ArrayCollection $children * @param ArrayCollection $contacts * @param Counter $childrenCounter + * @param bool $inactive * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter) + public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, $inactive = false) { /** @var Event $child */ foreach ($children as $child) { @@ -282,11 +305,11 @@ public function executeContactsForChildren(ArrayCollection $children, ArrayColle ); if ($executionDate > $this->now) { - $this->scheduler->schedule($child, $executionDate, $contacts); + $this->scheduler->schedule($child, $executionDate, $contacts, $inactive); continue; } - $this->executeForContacts($child, $contacts, $childrenCounter); + $this->executeForContacts($child, $contacts, $childrenCounter, $inactive); } } @@ -309,7 +332,7 @@ private function executeAction(AbstractEventAccessor $config, Event $event, Arra $contacts = $this->eventLogger->extractContactsFromLogs($logs); // Update and clear any pending logs - $this->eventLogger->persistCollection($logs); + $this->persistLogs($logs); // Process conditions that are attached to this action $this->executeContactsForConditionChildren($event, $contacts, $counter); @@ -331,7 +354,7 @@ private function executeCondition(AbstractEventAccessor $config, Event $event, A $evaluatedContacts = $this->conditionExecutioner->executeLogs($config, $logs); // Update and clear any pending logs - $this->eventLogger->persistCollection($logs); + $this->persistLogs($logs); $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); } @@ -352,8 +375,23 @@ private function executeDecision(AbstractEventAccessor $config, Event $event, Ar $evaluatedContacts = $this->decisionExecutioner->executeLogs($config, $logs); // Update and clear any pending logs - $this->eventLogger->persistCollection($logs); + $this->persistLogs($logs); $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); } + + /** + * @param ArrayCollection $logs + */ + private function persistLogs(ArrayCollection $logs) + { + if ($this->responses) { + // Extract responses + $this->responses->setFromLogs($logs); + } + + // Save updated log entries and clear from memory + $this->eventLogger->persistCollection($logs) + ->clear(); + } } diff --git a/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php b/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php index 2222fcd8908..f3454be946c 100644 --- a/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php +++ b/app/bundles/CampaignBundle/Executioner/ExecutionerInterface.php @@ -12,25 +12,17 @@ namespace Mautic\CampaignBundle\Executioner; use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Symfony\Component\Console\Output\OutputInterface; interface ExecutionerInterface { /** * @param Campaign $campaign - * @param $contactId + * @param ContactLimiter $limiter * @param OutputInterface|null $output * * @return mixed */ - public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null); - - /** - * @param Campaign $campaign - * @param int $batchLimit - * @param OutputInterface|null $output - * - * @return mixed - */ - public function executeForCampaign(Campaign $campaign, $batchLimit = 100, OutputInterface $output = null); + public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInterface $output = null); } diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php new file mode 100644 index 00000000000..ee67f1bef97 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -0,0 +1,176 @@ +scheduler = $scheduler; + $this->inactiveContacts = $inactiveContacts; + $this->eventLogRepository = $eventLogRepository; + $this->eventRepository = $eventRepository; + $this->logger = $logger; + } + + /** + * @param ArrayCollection|Event[] $decisions + */ + public function removeDecisionsWithoutNegativeChildren(ArrayCollection $decisions) + { + /** + * @var int + * @var Event $decision + */ + foreach ($decisions as $key => $decision) { + $negativeChildren = $decision->getNegativeChildren(); + if (!$negativeChildren->count()) { + $decisions->remove($key); + } + } + } + + /** + * @param \DateTime $now + * @param ArrayCollection $contacts + * @param array $lastActiveDates + * @param ArrayCollection $negativeChildren + * + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollection $contacts, $eventId, ArrayCollection $negativeChildren) + { + $contactIds = $contacts->getKeys(); + + // If there is a parent ID, get last active dates based on when that event was executed for the given contact + // Otherwise, use when the contact was added to the campaign for comparison + if ($eventId) { + $lastActiveDates = $this->eventLogRepository->getDatesExecuted($eventId, $contactIds); + } else { + $lastActiveDates = $this->inactiveContacts->getDatesAdded(); + } + + /* @var Event $event */ + foreach ($contactIds as $contactId) { + if (!isset($lastActiveDates[$contactId])) { + // This contact does not have a last active date so likely the event is scheduled + $contacts->remove($contactId); + continue; + } + + $isInactive = false; + + // We have to loop over all the events till we have a confirmed event that is overdue + foreach ($negativeChildren as $event) { + $excuctionDate = $this->scheduler->getExecutionDateTime($event, $now, $lastActiveDates[$contactId]); + if ($excuctionDate <= $now) { + $isInactive = true; + break; + } + } + + // If any are found to be inactive, we process or schedule all the events associated with the inactive path of a decision + if (!$isInactive) { + $contacts->remove($contactId); + $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' has been active and thus not applicable'); + + continue; + } + + $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' has not been active'); + } + } + + /** + * @param $decisionId + * + * @return ArrayCollection + */ + public function getCollectionByDecisionId($decisionId) + { + $collection = new ArrayCollection(); + + /** @var Event $decision */ + if ($decision = $this->eventRepository->find($decisionId)) { + $collection->set($decision->getId(), $decision); + } + + return $collection; + } + + /** + * @param ArrayCollection $negativeChildren + * @param \DateTime $lastActiveDate + * + * @return \DateTime + * + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \DateTime $lastActiveDate) + { + $earliestDate = $lastActiveDate; + foreach ($negativeChildren as $event) { + $excuctionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate); + if ($excuctionDate <= $earliestDate) { + $earliestDate = $excuctionDate; + } + } + + return $lastActiveDate; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 49fd7e4154a..5447547291c 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -11,59 +11,182 @@ namespace Mautic\CampaignBundle\Executioner; +use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Helper\InactiveHelper; use Mautic\CampaignBundle\Executioner\Result\Counter; +use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Helper\ProgressBarHelper; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Translation\TranslatorInterface; class InactiveExecutioner implements ExecutionerInterface { + use ContactRangeTrait; + + /** + * @var Campaign + */ private $campaign; - private $contactId; - private $batchLimit; + + /** + * @var ContactLimiter + */ + private $limiter; + + /** + * @var OutputInterface + */ private $output; + + /** + * @var LoggerInterface + */ private $logger; + + /** + * @var ProgressBar + */ private $progressBar; + + /** + * @var TranslatorInterface + */ private $translator; - public function executeForCampaign(Campaign $campaign, $batchLimit = 100, OutputInterface $output = null) - { - $this->campaign = $campaign; - $this->batchLimit = $batchLimit; - $this->output = ($output) ? $output : new NullOutput(); + /** + * @var EventScheduler + */ + private $scheduler; - $this->logger->debug('CAMPAIGN: Triggering inaction events'); + /** + * @var EventExecutioner + */ + private $executioner; + + /** + * @var Counter + */ + private $counter; + + /** + * @var InactiveContacts + */ + private $inactiveContacts; + + /** + * @var ArrayCollection + */ + private $decisions; - return $this->execute(); + /** + * @var InactiveHelper + */ + private $helper; + + /** + * InactiveExecutioner constructor. + * + * @param InactiveContacts $inactiveContacts + * @param LoggerInterface $logger + * @param TranslatorInterface $translator + * @param EventScheduler $scheduler + * @param InactiveHelper $helper + * @param EventExecutioner $executioner + */ + public function __construct( + InactiveContacts $inactiveContacts, + LoggerInterface $logger, + TranslatorInterface $translator, + EventScheduler $scheduler, + InactiveHelper $helper, + EventExecutioner $executioner + ) { + $this->inactiveContacts = $inactiveContacts; + $this->logger = $logger; + $this->translator = $translator; + $this->scheduler = $scheduler; + $this->helper = $helper; + $this->executioner = $executioner; } - public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) + /** + * @param Campaign $campaign + * @param ContactLimiter $limiter + * @param OutputInterface|null $output + * + * @return Counter|mixed + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInterface $output = null) { - $this->campaign = $campaign; - $this->contactId = $contactId; - $this->output = ($output) ? $output : new NullOutput(); - $this->batchLimit = null; + $this->campaign = $campaign; + $this->limiter = $limiter; + $this->output = ($output) ? $output : new NullOutput(); + $this->counter = new Counter(); + + try { + $this->decisions = $this->campaign->getEventsByType(Event::TYPE_DECISION); + + $this->prepareForExecution(); + $this->executeEvents(); + } catch (NoContactsFound $exception) { + $this->logger->debug('CAMPAIGN: No more contacts to process'); + } catch (NoEventsFound $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } - return $this->execute(); + return $this->counter; } /** + * @param $decisionId + * @param ContactLimiter $limiter + * @param OutputInterface|null $output + * * @return Counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException - * @throws \Doctrine\ORM\Query\QueryException */ - private function execute() + public function validate($decisionId, ContactLimiter $limiter, OutputInterface $output = null) { + $this->limiter = $limiter; + $this->output = ($output) ? $output : new NullOutput(); $this->counter = new Counter(); try { + $this->decisions = $this->helper->getCollectionByDecisionId($decisionId); + if ($this->decisions->count()) { + $this->campaign = $this->decisions->first()->getCampaign(); + if (!$this->campaign->isPublished()) { + throw new NoEventsFound(); + } + } + $this->prepareForExecution(); - $this->executeOrRecheduleEvent(); + $this->executeEvents(); + } catch (NoContactsFound $exception) { + $this->logger->debug('CAMPAIGN: No more contacts to process'); } catch (NoEventsFound $exception) { $this->logger->debug('CAMPAIGN: No events to process'); } finally { @@ -77,31 +200,92 @@ private function execute() } /** + * @throws NoContactsFound * @throws NoEventsFound */ private function prepareForExecution() { - // Get counts by event - $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId()); - $totalScheduledCount = array_sum($scheduledEvents); - $this->scheduledEvents = array_keys($scheduledEvents); - $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); + $this->logger->debug('CAMPAIGN: Triggering inaction events'); + + $this->helper->removeDecisionsWithoutNegativeChildren($this->decisions); + $totalDecisions = $this->decisions->count(); + if (!$totalDecisions) { + throw new NoEventsFound(); + } + + $totalContacts = $this->inactiveContacts->getContactCount($this->campaign->getId(), $this->decisions->getKeys(), $this->limiter); $this->output->writeln( $this->translator->trans( - 'mautic.campaign.trigger.event_count', + 'mautic.campaign.trigger.decision_count_analyzed', [ - '%events%' => $totalScheduledCount, - '%batch%' => $this->batchLimit, + '%decisions%' => $totalDecisions, + '%leads%' => $totalContacts, + '%batch%' => $this->limiter->getBatchLimit(), ] ) ); - $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); + if (!$totalContacts) { + throw new NoContactsFound(); + } + + // Approximate total count because the query to fetch contacts will filter out those that have not arrived to this point in the campaign yet + $this->progressBar = ProgressBarHelper::init($this->output, $totalContacts * $totalDecisions); $this->progressBar->start(); + } - if (!$totalScheduledCount) { - throw new NoEventsFound(); + /** + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws NoContactsFound + * @throws Scheduler\Exception\NotSchedulableException + */ + private function executeEvents() + { + // Use the same timestamp across all contacts processed + $now = new \DateTime(); + + /** @var Event $decisionEvent */ + foreach ($this->decisions as $decisionEvent) { + // We need the parent ID of the decision in order to fetch the time the contact executed this event + $parentEvent = $decisionEvent->getParent(); + $parentEventId = ($parentEvent) ? $parentEvent->getId() : null; + + // Because timing may not be appropriate, the starting row of the query may or may not change. + // So use the max contact ID to filter/sort results. + $startAtContactId = $this->limiter->getMinContactId() ?: 0; + + // Ge the first batch of contacts + $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); + + // Loop over all contacts till we've processed all those applicable for this decision + while ($contacts->count()) { + // Get the max contact ID before any are removed + $startAtContactId = max($contacts->getKeys()); + + $this->progressBar->advance($contacts->count()); + $this->counter->advanceEvaluated($contacts->count()); + + $inactiveEvents = $decisionEvent->getNegativeChildren(); + $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); + + if ($contacts->count()) { + // For simplicity sake, we're going to execute or schedule from date of evaluation + $this->executioner->setNow($now); + // Execute or schedule the events attached to the inactive side of the decision + $this->executioner->executeContactsForChildren($inactiveEvents, $contacts, $this->counter, true); + // Record decision for these contacts + $this->executioner->recordLogsAsExecutedForEvent($decisionEvent, $contacts, true); + } + + // Clear contacts from memory + $this->inactiveContacts->clear(); + + // Get the next batch, starting with the max contact ID + $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); + } } } } diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index fc519d8ee79..3e805558a0f 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -15,6 +15,7 @@ use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; use Mautic\CampaignBundle\Executioner\Result\Counter; @@ -27,23 +28,20 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Translation\TranslatorInterface; -class KickoffExecutioner +class KickoffExecutioner implements ExecutionerInterface { + use ContactRangeTrait; + /** - * @var null|int + * @var ContactLimiter */ - private $contactId; + private $limiter; /** * @var Campaign */ private $campaign; - /** - * @var int - */ - private $batchLimit = 100; - /** * @var OutputInterface */ @@ -119,56 +117,22 @@ public function __construct( /** * @param Campaign $campaign - * @param int $batchLimit - * @param OutputInterface|null $output - * - * @return Counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws NotSchedulableException - */ - public function executeForCampaign(Campaign $campaign, $batchLimit = 100, OutputInterface $output = null) - { - $this->campaign = $campaign; - $this->contactId = null; - $this->batchLimit = $batchLimit; - $this->output = ($output) ? $output : new NullOutput(); - - return $this->execute(); - } - - /** - * @param Campaign $campaign - * @param $contactId + * @param ContactLimiter $limiter * @param OutputInterface|null $output * * @return Counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws NotSchedulableException */ - public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) - { - $this->campaign = $campaign; - $this->contactId = $contactId; - $this->output = ($output) ? $output : new NullOutput(); - $this->batchLimit = null; - - return $this->execute(); - } - - /** - * @return Counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws NotSchedulableException - */ - private function execute() + public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInterface $output = null) { - $this->counter = new Counter(); + $this->campaign = $campaign; + $this->limiter = $limiter; + $this->output = ($output) ? $output : new NullOutput(); + $this->counter = new Counter(); try { $this->prepareForExecution(); @@ -194,13 +158,14 @@ private function prepareForExecution() { $this->logger->debug('CAMPAIGN: Triggering kickoff events'); + $this->progressBar = null; $this->batchCounter = 0; $this->rootEvents = $this->campaign->getRootEvents(); $totalRootEvents = $this->rootEvents->count(); $this->logger->debug('CAMPAIGN: Processing the following events: '.implode(', ', $this->rootEvents->getKeys())); - $totalContacts = $this->kickoffContacts->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->contactId); + $totalContacts = $this->kickoffContacts->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->limiter); $totalKickoffEvents = $totalRootEvents * $totalContacts; $this->output->writeln( @@ -208,17 +173,17 @@ private function prepareForExecution() 'mautic.campaign.trigger.event_count', [ '%events%' => $totalKickoffEvents, - '%batch%' => $this->batchLimit, + '%batch%' => $this->limiter->getBatchLimit(), ] ) ); - $this->progressBar = ProgressBarHelper::init($this->output, $totalKickoffEvents); - $this->progressBar->start(); - if (!$totalKickoffEvents) { throw new NoEventsFound(); } + + $this->progressBar = ProgressBarHelper::init($this->output, $totalKickoffEvents); + $this->progressBar->start(); } /** @@ -235,7 +200,7 @@ private function executeOrScheduleEvent() $this->counter->advanceEventCount($this->rootEvents->count()); // Loop over contacts until the entire campaign is executed - $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->batchLimit, $this->contactId); + $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->limiter); while ($contacts->count()) { /** @var Event $event */ foreach ($this->rootEvents as $event) { @@ -262,7 +227,7 @@ private function executeOrScheduleEvent() $this->kickoffContacts->clear(); // Get the next batch - $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->batchLimit, $this->contactId); + $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->limiter); } } } diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index b052a2584db..cb2d1dbccbf 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -87,22 +87,28 @@ public function persistLog(LeadEventLog $log) /** * @param Event $event * @param null $lead + * @param bool $inactive * * @return LeadEventLog */ - public function buildLogEntry(Event $event, $lead = null) + public function buildLogEntry(Event $event, $lead = null, $inactive = false) { $log = new LeadEventLog(); $log->setIpAddress($this->ipLookupHelper->getIpAddress()); $log->setEvent($event); + $log->setCampaign($event->getCampaign()); if ($lead == null) { $lead = $this->leadModel->getCurrentLead(); } $log->setLead($lead); + if ($inactive) { + $log->setNonActionPathTaken(true); + } + $log->setDateTriggered(new \DateTime()); $log->setSystemTriggered(defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED')); @@ -212,14 +218,15 @@ public function extractContactsFromLogs(ArrayCollection $logs) * @param Event $event * @param AbstractEventAccessor $config * @param ArrayCollection $contacts + * @param bool $inactive * * @return ArrayCollection */ - public function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts) + public function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts, $inactive = false) { // Ensure each contact has a log entry to prevent them from being picked up again prematurely foreach ($contacts as $contact) { - $log = $this->buildLogEntry($event, $contact); + $log = $this->buildLogEntry($event, $contact, $inactive); $log->setIsScheduled(false); $log->setDateTriggered(new \DateTime()); diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index 02f46a43c82..d0d5d983fcf 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -16,6 +16,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; use Mautic\CampaignBundle\Executioner\Result\Counter; @@ -29,6 +30,8 @@ class ScheduledExecutioner implements ExecutionerInterface { + use ContactRangeTrait; + /** * @var LeadEventLogRepository */ @@ -65,14 +68,9 @@ class ScheduledExecutioner implements ExecutionerInterface private $campaign; /** - * @var - */ - private $contactId; - - /** - * @var int + * @var ContactLimiter */ - private $batchLimit; + private $limiter; /** * @var OutputInterface @@ -94,6 +92,11 @@ class ScheduledExecutioner implements ExecutionerInterface */ private $counter; + /** + * @var \DateTime + */ + private $now; + /** * ScheduledExecutioner constructor. * @@ -122,73 +125,101 @@ public function __construct( /** * @param Campaign $campaign - * @param int $batchLimit + * @param ContactLimiter $limiter * @param OutputInterface|null $output * * @return Counter|mixed * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException * @throws \Doctrine\ORM\Query\QueryException */ - public function executeForCampaign(Campaign $campaign, $batchLimit = 100, OutputInterface $output = null) + public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInterface $output = null) { $this->campaign = $campaign; - $this->batchLimit = $batchLimit; + $this->limiter = $limiter; $this->output = ($output) ? $output : new NullOutput(); + $this->counter = new Counter(); $this->logger->debug('CAMPAIGN: Triggering scheduled events'); - return $this->execute(); + try { + $this->prepareForExecution(); + $this->executeOrRecheduleEvent(); + } catch (NoEventsFound $exception) { + $this->logger->debug('CAMPAIGN: No events to process'); + } finally { + if ($this->progressBar) { + $this->progressBar->finish(); + $this->output->writeln("\n"); + } + } + + return $this->counter; } /** - * @param Campaign $campaign - * @param $contactId + * @param array $logIds * @param OutputInterface|null $output * - * @return Counter|mixed - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Scheduler\Exception\NotSchedulableException - * @throws \Doctrine\ORM\Query\QueryException - */ - public function executeForContact(Campaign $campaign, $contactId, OutputInterface $output = null) - { - $this->campaign = $campaign; - $this->contactId = $contactId; - $this->output = ($output) ? $output : new NullOutput(); - $this->batchLimit = null; - - return $this->execute(); - } - - /** * @return Counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException * @throws \Doctrine\ORM\Query\QueryException */ - private function execute() + public function executeByIds(array $logIds, OutputInterface $output = null) { + $this->output = ($output) ? $output : new NullOutput(); $this->counter = new Counter(); - try { - $this->prepareForExecution(); - $this->executeOrRecheduleEvent(); - } catch (NoEventsFound $exception) { - $this->logger->debug('CAMPAIGN: No events to process'); - } finally { - if ($this->progressBar) { - $this->progressBar->finish(); - $this->output->writeln("\n"); - } + if (!$logIds) { + return $this->counter; + } + + $logs = $this->repo->getScheduledById($logIds); + $totalLogsFound = $logs->count(); + $this->counter->advanceEvaluated($totalLogsFound); + + $this->logger->debug('CAMPAIGN: '.$logs->count().' events scheduled to execute.'); + $this->output->writeln( + $this->translator->trans( + 'mautic.campaign.trigger.event_count', + [ + '%events%' => $totalLogsFound, + '%batch%' => 'n/a', + ] + ) + ); + + if (!$logs->count()) { + return $this->counter; } + $this->progressBar = ProgressBarHelper::init($this->output, $totalLogsFound); + $this->progressBar->start(); + + // Validate that the schedule is still appropriate + $now = new \DateTime(); + $this->validateSchedule($logs, $now); + $scheduledLogCount = $totalLogsFound - $logs->count(); + $this->progressBar->advance($scheduledLogCount); + + // Organize the logs by event ID + $organized = $this->organizeByEvent($logs); + foreach ($organized as $organizedLogs) { + $this->progressBar->advance($organizedLogs->count()); + + $event = $organizedLogs->first()->getEvent(); + $this->executioner->executeLogs($event, $organizedLogs, $this->counter); + } + + $this->progressBar->finish(); + return $this->counter; } @@ -197,8 +228,11 @@ private function execute() */ private function prepareForExecution() { + $this->progressBar = null; + $this->now = new \Datetime(); + // Get counts by event - $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId(), $this->contactId); + $scheduledEvents = $this->repo->getScheduledCounts($this->campaign->getId(), $this->now, $this->limiter); $totalScheduledCount = array_sum($scheduledEvents); $this->scheduledEvents = array_keys($scheduledEvents); $this->logger->debug('CAMPAIGN: '.$totalScheduledCount.' events scheduled to execute.'); @@ -208,17 +242,17 @@ private function prepareForExecution() 'mautic.campaign.trigger.event_count', [ '%events%' => $totalScheduledCount, - '%batch%' => $this->batchLimit, + '%batch%' => $this->limiter->getBatchLimit(), ] ) ); - $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); - $this->progressBar->start(); - if (!$totalScheduledCount) { throw new NoEventsFound(); } + + $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); + $this->progressBar->start(); } /** @@ -253,7 +287,7 @@ private function executeOrRecheduleEvent() */ private function executeScheduled($eventId, \DateTime $now) { - $logs = $this->repo->getScheduled($eventId, $this->batchLimit, $this->contactId); + $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); $this->scheduledContacts->hydrateContacts($logs); while ($logs->count()) { @@ -262,30 +296,31 @@ private function executeScheduled($eventId, \DateTime $now) $this->counter->advanceEvaluated($logs->count()); // Validate that the schedule is still appropriate - $this->validateSchedule($logs, $event, $now); + $this->validateSchedule($logs, $now); // Execute if there are any that did not get rescheduled $this->executioner->executeLogs($event, $logs, $this->counter); // Get next batch $this->scheduledContacts->clear(); - $logs = $this->repo->getScheduled($eventId, $this->batchLimit, $this->contactId); + $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); } } /** * @param ArrayCollection $logs - * @param Event $event * @param \DateTime $now * * @throws Scheduler\Exception\NotSchedulableException */ - private function validateSchedule(ArrayCollection $logs, Event $event, \DateTime $now) + private function validateSchedule(ArrayCollection $logs, \DateTime $now) { // Check if the event should be scheduled (let the schedulers do the debug logging) /** @var LeadEventLog $log */ foreach ($logs as $key => $log) { if ($createdDate = $log->getDateTriggered()) { + $event = $log->getEvent(); + // Date Triggered will be when the log entry was first created so use it to compare to ensure that the event's schedule // hasn't been changed since this event was first scheduled $executionDate = $this->scheduler->getExecutionDateTime($event, $now, $createdDate); @@ -305,4 +340,26 @@ private function validateSchedule(ArrayCollection $logs, Event $event, \DateTime } } } + + /** + * @param ArrayCollection $logs + * + * @return ArrayCollection[] + */ + private function organizeByEvent(ArrayCollection $logs) + { + $organized = []; + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $event = $log->getEvent(); + + if (!isset($organized[$event->getId()])) { + $organized[$event->getId()] = new ArrayCollection(); + } + + $organized[$event->getId()]->set($log->getId(), $log); + } + + return $organized; + } } diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index b8c190c8510..fd70a5052a2 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -107,15 +107,17 @@ public function scheduleForContact(Event $event, \DateTime $executionDate, Lead /** * @param Event $event + * @param \DateTime $executionDate * @param ArrayCollection $contacts + * @param bool $inactive */ - public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts) + public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts, $inactive = false) { $config = $this->collector->getEventConfig($event); foreach ($contacts as $contact) { // Create the entry - $log = $this->eventLogger->buildLogEntry($event, $contact); + $log = $this->eventLogger->buildLogEntry($event, $contact, $inactive); // Schedule it $log->setTriggerDate($executionDate); @@ -154,7 +156,7 @@ public function reschedule(LeadEventLog $log, \DateTime $toBeExecutedOn) $event = $log->getEvent(); $config = $this->collector->getEventConfig($event); - $this->dispatchScheduledEvent($config, $log); + $this->dispatchScheduledEvent($config, $log, true); } /** @@ -191,13 +193,12 @@ public function getExecutionDateTime(Event $event, \DateTime $now = null, \DateT if (null === $comparedToDateTime) { $comparedToDateTime = clone $now; - } else { - // Prevent comparisons from modifying original object - $comparedToDateTime = clone $comparedToDateTime; } switch ($event->getTriggerMode()) { case Event::TRIGGER_MODE_IMMEDIATE: + $this->logger->debug('CAMPAIGN: ('.$event->getId().') Executing immediately'); + return $now; case Event::TRIGGER_MODE_INTERVAL: return $this->intervalScheduler->getExecutionDateTime($event, $now, $comparedToDateTime); @@ -211,12 +212,13 @@ public function getExecutionDateTime(Event $event, \DateTime $now = null, \DateT /** * @param AbstractEventAccessor $config * @param LeadEventLog $log + * @param bool $isReschedule */ - private function dispatchScheduledEvent(AbstractEventAccessor $config, LeadEventLog $log) + private function dispatchScheduledEvent(AbstractEventAccessor $config, LeadEventLog $log, $isReschedule = false) { $this->dispatcher->dispatch( CampaignEvents::ON_EVENT_SCHEDULED, - new ScheduledEvent($config, $log) + new ScheduledEvent($config, $log, $isReschedule) ); } diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php index a8ee8e22be7..9dd87a6ec83 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php @@ -51,7 +51,7 @@ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $co if ($now >= $triggerDate) { $this->logger->debug( - 'CAMPAIGN: Date to execute ('.$triggerDate->format('Y-m-d H:i:s T').') compared to now (' + 'CAMPAIGN: ('.$event->getId().') Date to execute ('.$triggerDate->format('Y-m-d H:i:s T').') compared to now (' .$now->format('Y-m-d H:i:s T').') and is thus overdue' ); @@ -59,31 +59,5 @@ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $co } return $triggerDate; - - /* - if ($negate) { - $this->logger->debug( - 'CAMPAIGN: Negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' - .$now->format('Y-m-d H:i:s T').') and is thus '.(($pastDue) ? 'overdue' : 'not past due') - ); - - //it is past the scheduled trigger date and the lead has done nothing so return true to trigger - //the event otherwise false to do nothing - $return = ($pastDue) ? true : $action['triggerDate']; - - // Save some RAM for batch processing - unset($now, $action); - - return $return; - } elseif (!$pastDue) { - $this->logger->debug( - 'CAMPAIGN: Non-negative comparison; Date to execute ('.$action['triggerDate']->format('Y-m-d H:i:s T').') compared to now (' - .$now->format('Y-m-d H:i:s T').') and is thus not past due' - ); - - //schedule the event - return $action['triggerDate']; - } - * */ } } diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php index fafb4f57fec..d06330b64ce 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -44,11 +44,15 @@ public function __construct(LoggerInterface $logger) */ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime) { - // $triggerOn = $negate ? clone $parentTriggeredDate : new \DateTime(); $interval = $event->getTriggerInterval(); $unit = $event->getTriggerIntervalUnit(); - $this->logger->debug('CAMPAIGN: Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format('Y-m-d H:i:s T')); + // Prevent comparisons from modifying original object + $comparedToDateTime = clone $comparedToDateTime; + + $this->logger->debug( + 'CAMPAIGN: ('.$event->getId().') Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format('Y-m-d H:i:s T') + ); try { $comparedToDateTime->add((new DateTimeHelper())->buildInterval($interval, $unit)); @@ -59,7 +63,10 @@ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $co } if ($comparedToDateTime > $now) { - $this->logger->debug("CAMPAIGN: Interval of $interval $unit to execute (".$comparedToDateTime->format('Y-m-d H:i:s T').') is later than now ('.$now->format('Y-m-d H:i:s T')); + $this->logger->debug( + "CAMPAIGN: Interval of $interval $unit to execute (".$comparedToDateTime->format('Y-m-d H:i:s T').') is later than now (' + .$now->format('Y-m-d H:i:s T') + ); //the event is to be scheduled based on the time interval return $comparedToDateTime; diff --git a/app/bundles/CampaignBundle/Model/EventModel.php b/app/bundles/CampaignBundle/Model/EventModel.php index 6a968d9695d..3edea970197 100644 --- a/app/bundles/CampaignBundle/Model/EventModel.php +++ b/app/bundles/CampaignBundle/Model/EventModel.php @@ -11,22 +11,22 @@ namespace Mautic\CampaignBundle\Model; -use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; +use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\DecisionExecutioner; +use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\EventExecutioner; +use Mautic\CampaignBundle\Executioner\InactiveExecutioner; use Mautic\CampaignBundle\Executioner\KickoffExecutioner; use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; -use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; -use Mautic\CoreBundle\Helper\ProgressBarHelper; use Mautic\CoreBundle\Model\NotificationModel; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; use Mautic\UserBundle\Model\UserModel; -use Symfony\Component\Console\Output\OutputInterface; /** * Class EventModel @@ -34,21 +34,6 @@ */ class EventModel extends LegacyEventModel { - /** - * @var IpLookupHelper - */ - protected $ipLookupHelper; - - /** - * @var LeadModel - */ - protected $leadModel; - - /** - * @var CampaignModel - */ - protected $campaignModel; - /** * @var UserModel */ @@ -64,30 +49,45 @@ class EventModel extends LegacyEventModel * * @param IpLookupHelper $ipLookupHelper * @param LeadModel $leadModel - * @param CampaignModel $campaignModel * @param UserModel $userModel * @param NotificationModel $notificationModel + * @param CampaignModel $campaignModel * @param DecisionExecutioner $decisionExecutioner * @param KickoffExecutioner $kickoffExecutioner * @param ScheduledExecutioner $scheduledExecutioner + * @param InactiveExecutioner $inactiveExecutioner + * @param EventExecutioner $eventExecutioner + * @param EventDispatcher $eventDispatcher */ public function __construct( - IpLookupHelper $ipLookupHelper, - LeadModel $leadModel, - CampaignModel $campaignModel, UserModel $userModel, NotificationModel $notificationModel, + CampaignModel $campaignModel, + LeadModel $leadModel, + IpLookupHelper $ipLookupHelper, DecisionExecutioner $decisionExecutioner, KickoffExecutioner $kickoffExecutioner, - ScheduledExecutioner $scheduledExecutioner + ScheduledExecutioner $scheduledExecutioner, + InactiveExecutioner $inactiveExecutioner, + EventExecutioner $eventExecutioner, + EventDispatcher $eventDispatcher, + EventCollector $eventCollector ) { - $this->ipLookupHelper = $ipLookupHelper; - $this->leadModel = $leadModel; - $this->campaignModel = $campaignModel; - $this->userModel = $userModel; - $this->notificationModel = $notificationModel; - - parent::__construct($decisionExecutioner, $kickoffExecutioner, $scheduledExecutioner); + $this->userModel = $userModel; + $this->notificationModel = $notificationModel; + + parent::__construct( + $campaignModel, + $leadModel, + $ipLookupHelper, + $decisionExecutioner, + $kickoffExecutioner, + $scheduledExecutioner, + $inactiveExecutioner, + $eventExecutioner, + $eventDispatcher, + $eventCollector + ); } /** @@ -182,374 +182,6 @@ public function deleteEvents($currentEvents, $deletedEvents) } } - /** - * Find and trigger the negative events, i.e. the events with a no decision path. - * - * @param Campaign $campaign - * @param int $totalEventCount - * @param int $limit - * @param bool $max - * @param OutputInterface $output - * @param bool|false $returnCounts If true, returns array of counters - * - * @return int - */ - public function triggerNegativeEvents( - $campaign, - &$totalEventCount = 0, - $limit = 100, - $max = false, - OutputInterface $output = null, - $returnCounts = false - ) { - defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - - $this->logger->debug('CAMPAIGN: Triggering negative events'); - - $campaignId = $campaign->getId(); - $repo = $this->getRepository(); - $campaignRepo = $this->getCampaignRepository(); - $logRepo = $this->getLeadEventLogRepository(); - - // Get events to avoid large number of joins - $campaignEvents = $repo->getCampaignEvents($campaignId); - - // Get an array of events that are non-action based - $nonActionEvents = []; - $actionEvents = []; - foreach ($campaignEvents as $id => $e) { - if (!empty($e['decisionPath']) && !empty($e['parent_id']) && $campaignEvents[$e['parent_id']]['eventType'] != 'condition') { - if ($e['decisionPath'] == 'no') { - $nonActionEvents[$e['parent_id']][$id] = $e; - } elseif ($e['decisionPath'] == 'yes') { - $actionEvents[$e['parent_id']][] = $id; - } - } - } - - $this->logger->debug('CAMPAIGN: Processing the children of the following events: '.implode(', ', array_keys($nonActionEvents))); - - if (empty($nonActionEvents)) { - // No non-action events associated with this campaign - unset($campaignEvents); - - return 0; - } - - // Get a count - $leadCount = $campaignRepo->getCampaignLeadCount($campaignId); - - if ($output) { - $output->writeln( - $this->translator->trans( - 'mautic.campaign.trigger.lead_count_analyzed', - ['%leads%' => $leadCount, '%batch%' => $limit] - ) - ); - } - - $start = $leadProcessedCount = $lastRoundPercentage = $executedEventCount = $evaluatedEventCount = $negativeExecutedCount = $negativeEvaluatedCount = 0; - $nonActionEventCount = $leadCount * count($nonActionEvents); - $eventSettings = $this->campaignModel->getEvents(); - $maxCount = ($max) ? $max : $nonActionEventCount; - - // Try to save some memory - gc_enable(); - - if ($leadCount) { - if ($output) { - $progress = ProgressBarHelper::init($output, $maxCount); - $progress->start(); - if ($max) { - $progress->advance($totalEventCount); - } - } - - $sleepBatchCount = 0; - $batchDebugCounter = 1; - while ($start <= $leadCount) { - $this->logger->debug('CAMPAIGN: Batch #'.$batchDebugCounter); - - // Get batched campaign ids - $campaignLeads = $campaignRepo->getCampaignLeads($campaignId, $start, $limit, ['cl.lead_id, cl.date_added']); - - $campaignLeadIds = []; - $campaignLeadDates = []; - foreach ($campaignLeads as $r) { - $campaignLeadIds[] = $r['lead_id']; - $campaignLeadDates[$r['lead_id']] = $r['date_added']; - } - - unset($campaignLeads); - - $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignLeadIds)); - - foreach ($nonActionEvents as $parentId => $events) { - // Just a check to ensure this is an appropriate action - if ($campaignEvents[$parentId]['eventType'] == 'action') { - $this->logger->debug('CAMPAIGN: Parent event ID #'.$parentId.' is an action.'); - - continue; - } - - // Get only leads who have had the action prior to the decision executed - $grandParentId = $campaignEvents[$parentId]['parent_id']; - - // Get the lead log for this batch of leads limiting to those that have already triggered - // the decision's parent and haven't executed this level in the path yet - if ($grandParentId) { - $this->logger->debug('CAMPAIGN: Checking for contacts based on grand parent execution.'); - - $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, [$grandParentId], array_keys($events), true); - $applicableLeads = array_keys($leadLog); - } else { - $this->logger->debug('CAMPAIGN: Checking for contacts based on exclusion due to being at root level'); - - // The event has no grandparent (likely because the decision is first in the campaign) so find leads that HAVE - // already executed the events in the root level and exclude them - $havingEvents = (isset($actionEvents[$parentId])) - ? array_merge($actionEvents[$parentId], array_keys($events)) - : array_keys( - $events - ); - $leadLog = $repo->getEventLog($campaignId, $campaignLeadIds, $havingEvents); - $unapplicableLeads = array_keys($leadLog); - - // Only use leads that are not applicable - $applicableLeads = array_diff($campaignLeadIds, $unapplicableLeads); - - unset($unapplicableLeads); - } - - if (empty($applicableLeads)) { - $this->logger->debug('CAMPAIGN: No events are applicable'); - - continue; - } - - $this->logger->debug('CAMPAIGN: These contacts have have not gone down the positive path: '.implode(', ', $applicableLeads)); - - // Get the leads - $leads = $this->leadModel->getEntities( - [ - 'filter' => [ - 'force' => [ - [ - 'column' => 'l.id', - 'expr' => 'in', - 'value' => $applicableLeads, - ], - ], - ], - 'orderBy' => 'l.id', - 'orderByDir' => 'asc', - 'withPrimaryCompany' => true, - 'withChannelRules' => true, - ] - ); - - if (!count($leads)) { - // Just a precaution in case non-existent leads are lingering in the campaign leads table - $this->logger->debug('CAMPAIGN: No contact entities found.'); - - continue; - } - - // Loop over the non-actions and determine if it has been processed for this lead - - $leadDebugCounter = 1; - /** @var \Mautic\LeadBundle\Entity\Lead $lead */ - foreach ($leads as $lead) { - ++$negativeEvaluatedCount; - - // Set lead for listeners - $this->leadModel->setSystemCurrentLead($lead); - - $this->logger->debug('CAMPAIGN: contact ID #'.$lead->getId().'; #'.$leadDebugCounter.' in batch #'.$batchDebugCounter); - - // Prevent path if lead has already gone down this path - if (!isset($leadLog[$lead->getId()]) || !array_key_exists($parentId, $leadLog[$lead->getId()])) { - // Get date to compare against - $utcDateString = ($grandParentId) ? $leadLog[$lead->getId()][$grandParentId]['date_triggered'] - : $campaignLeadDates[$lead->getId()]; - - // Convert to local DateTime - $grandParentDate = (new DateTimeHelper($utcDateString))->getLocalDateTime(); - - // Non-decision has not taken place yet, so cycle over each associated action to see if timing is right - $eventTiming = []; - $executeAction = false; - foreach ($events as $id => $e) { - if ($sleepBatchCount == $limit) { - // Keep CPU down - $this->batchSleep(); - $sleepBatchCount = 0; - } else { - ++$sleepBatchCount; - } - - if (isset($leadLog[$lead->getId()]) && array_key_exists($id, $leadLog[$lead->getId()])) { - $this->logger->debug('CAMPAIGN: Event (ID #'.$id.') has already been executed'); - unset($e); - - continue; - } - - if (!isset($eventSettings[$e['eventType']][$e['type']])) { - $this->logger->debug('CAMPAIGN: Event (ID #'.$id.') no longer exists'); - unset($e); - - continue; - } - - // First get the timing for all the 'non-decision' actions - $eventTiming[$id] = $this->checkEventTiming($e, $grandParentDate, true); - if ($eventTiming[$id] === true) { - // Includes events to be executed now then schedule the rest if applicable - $executeAction = true; - } - - unset($e); - } - - if (!$executeAction) { - $negativeEvaluatedCount += count($nonActionEvents); - - // Timing is not appropriate so move on to next lead - unset($eventTiming); - - continue; - } - - if ($max && ($totalEventCount + count($nonActionEvents)) >= $max) { - // Hit the max or will hit the max while mid-process for the lead - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } - - $counts = [ - 'events' => $nonActionEventCount, - 'evaluated' => $negativeEvaluatedCount, - 'executed' => $negativeExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } - - $decisionLogged = false; - - // Execute or schedule events - $this->logger->debug( - 'CAMPAIGN: Processing the following events for contact ID# '.$lead->getId().': '.implode( - ', ', - array_keys($eventTiming) - ) - ); - - foreach ($eventTiming as $id => $eventTriggerDate) { - // Set event - $event = $events[$id]; - $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), - 'createdBy' => $campaign->getCreatedBy(), - ]; - - // Set lead in case this is triggered by the system - $this->leadModel->setSystemCurrentLead($lead); - - if ($this->executeEvent( - $event, - $campaign, - $lead, - $eventSettings, - false, - null, - $eventTriggerDate, - false, - $evaluatedEventCount, - $executedEventCount, - $totalEventCount - ) - ) { - if (!$decisionLogged) { - // Log the decision - $log = $this->getLogEntity($parentId, $campaign, $lead, null, true); - $log->setDateTriggered(new \DateTime()); - $log->setNonActionPathTaken(true); - $logRepo->saveEntity($log); - $this->em->detach($log); - unset($log); - - $decisionLogged = true; - } - - ++$negativeExecutedCount; - } - - unset($utcDateString, $grandParentDate); - } - } else { - $this->logger->debug('CAMPAIGN: Decision has already been executed.'); - } - - $currentCount = ($max) ? $totalEventCount : $negativeEvaluatedCount; - if ($output && isset($progress) && $currentCount < $maxCount) { - $progress->setProgress($currentCount); - } - - ++$leadDebugCounter; - - // Save RAM - $this->em->detach($lead); - unset($lead); - } - } - - // Next batch - $start += $limit; - - // Save RAM - $this->em->clear('Mautic\LeadBundle\Entity\Lead'); - $this->em->clear('Mautic\UserBundle\Entity\User'); - - unset($leads, $campaignLeadIds, $leadLog); - - $currentCount = ($max) ? $totalEventCount : $negativeEvaluatedCount; - if ($output && isset($progress) && $currentCount < $maxCount) { - $progress->setProgress($currentCount); - } - - // Free some memory - gc_collect_cycles(); - - ++$batchDebugCounter; - } - - if ($output && isset($progress)) { - $progress->finish(); - $output->writeln(''); - } - - $this->triggerConditions($campaign, $evaluatedEventCount, $executedEventCount, $totalEventCount); - } - - $counts = [ - 'events' => $nonActionEventCount, - 'evaluated' => $negativeEvaluatedCount, - 'executed' => $negativeExecutedCount, - 'totalEvaluated' => $evaluatedEventCount, - 'totalExecuted' => $executedEventCount, - ]; - $this->logger->debug('CAMPAIGN: Counts - '.var_export($counts, true)); - - return ($returnCounts) ? $counts : $executedEventCount; - } - /** * Get line chart data of campaign events. * diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModel.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php index 48d608a928e..bc2820657e3 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModel.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -11,21 +11,28 @@ namespace Mautic\CampaignBundle\Model; -use Doctrine\DBAL\DBALException; -use Doctrine\ORM\EntityNotFoundException; -use Mautic\CampaignBundle\CampaignEvents; +use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\FailedLeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLog; -use Mautic\CampaignBundle\Event\CampaignExecutionEvent; -use Mautic\CampaignBundle\Event\CampaignScheduledEvent; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; +use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\DecisionExecutioner; +use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\EventExecutioner; +use Mautic\CampaignBundle\Executioner\InactiveExecutioner; use Mautic\CampaignBundle\Executioner\KickoffExecutioner; +use Mautic\CampaignBundle\Executioner\Result\Counter; +use Mautic\CampaignBundle\Executioner\Result\Responses; use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CoreBundle\Helper\DateTimeHelper; -use Symfony\Component\Console\Output\OutputInterface; +use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Model\FormModel as CommonFormModel; +use Mautic\LeadBundle\Model\LeadModel; +use Symfony\Component\Console\Output\OutputInterface; /** * @deprecated 2.13.0 to be removed in 3.0 @@ -47,26 +54,77 @@ class LegacyEventModel extends CommonFormModel */ private $scheduledExecutioner; + /** + * @var InactiveExecutioner + */ + private $inactiveExecutioner; + + /** + * @var EventExecutioner + */ + private $eventExecutioner; + + /** + * @var EventDispatcher + */ + private $eventDispatcher; + + /** + * @var EventCollector + */ + private $eventCollector; + /** * @var */ protected $triggeredEvents; + /** + * @var CampaignModel + */ + protected $campaignModel; + + /** + * @var LeadModel + */ + protected $leadModel; + + /** + * @var IpLookupHelper + */ + protected $ipLookupHelper; + /** * LegacyEventModel constructor. * * @param DecisionExecutioner $decisionExecutioner * @param KickoffExecutioner $kickoffExecutioner * @param ScheduledExecutioner $scheduledExecutioner + * @param InactiveExecutioner $inactiveExecutioner + * @param EventExecutioner $eventExecutioner */ public function __construct( + CampaignModel $campaignModel, + LeadModel $leadModel, + IpLookupHelper $ipLookupHelper, DecisionExecutioner $decisionExecutioner, KickoffExecutioner $kickoffExecutioner, - ScheduledExecutioner $scheduledExecutioner + ScheduledExecutioner $scheduledExecutioner, + InactiveExecutioner $inactiveExecutioner, + EventExecutioner $eventExecutioner, + EventDispatcher $eventDispatcher, + EventCollector $eventCollector ) { + $this->campaignModel = $campaignModel; + $this->leadModel = $leadModel; + $this->ipLookupHelper = $ipLookupHelper; $this->decisionExecutioner = $decisionExecutioner; $this->kickoffExecutioner = $kickoffExecutioner; $this->scheduledExecutioner = $scheduledExecutioner; + $this->inactiveExecutioner = $inactiveExecutioner; + $this->eventExecutioner = $eventExecutioner; + $this->eventDispatcher = $eventDispatcher; + $this->eventCollector = $eventCollector; } /** @@ -82,11 +140,7 @@ public function __construct( * @param null $leadId * @param bool $returnCounts * - * @return array|int - * - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException - * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + * @return array */ public function triggerStartingEvents( Campaign $campaign, @@ -128,12 +182,7 @@ public function triggerStartingEvents( * @param OutputInterface|null $output * @param bool $returnCounts * - * @return array|int - * - * @throws \Doctrine\ORM\Query\QueryException - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException - * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException - * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + * @return array */ public function triggerScheduledEvents( $campaign, @@ -160,6 +209,45 @@ public function triggerScheduledEvents( return $counter->getTotalExecuted(); } + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param $campaign + * @param int $totalEventCount + * @param int $limit + * @param bool $max + * @param OutputInterface|null $output + * @param bool $returnCounts + * + * @return array + */ + public function triggerNegativeEvents( + $campaign, + &$totalEventCount = 0, + $limit = 100, + $max = false, + OutputInterface $output = null, + $returnCounts = false + ) { + defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); + + $counter = $this->scheduledExecutioner->executeForCampaign($campaign, $limit, $output); + + $totalEventCount += $counter->getEventCount(); + + if ($returnCounts) { + return [ + 'events' => $counter->getEventCount(), + 'evaluated' => $counter->getEvaluated(), + 'executed' => $counter->getExecuted(), + 'totalEvaluated' => $counter->getTotalEvaluated(), + 'totalExecuted' => $counter->getTotalExecuted(), + ]; + } + + return $counter->getTotalExecuted(); + } + /** * @deprecated 2.13.0 to be removed in 3.0 * @@ -241,127 +329,74 @@ public function handleCondition( } /** - * Invoke the event's callback function. - * * @deprecated 2.13.0 to be removed in 3.0 * - * @param $event - * @param $settings - * @param null $lead - * @param null $eventDetails - * @param bool $systemTriggered - * @param LeadEventLog $log + * @param $event + * @param $settings + * @param null $lead + * @param null $eventDetails + * @param bool $systemTriggered + * @param LeadEventLog|null $log + * + * @return bool * - * @return bool|mixed + * @throws \Doctrine\ORM\ORMException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException */ public function invokeEventCallback($event, $settings, $lead = null, $eventDetails = null, $systemTriggered = false, LeadEventLog $log = null) { - if (isset($settings['eventName'])) { - // Create a campaign event with a default successful result - $campaignEvent = new CampaignExecutionEvent( - [ - 'eventSettings' => $settings, - 'eventDetails' => $eventDetails, - 'event' => $event, - 'lead' => $lead, - 'systemTriggered' => $systemTriggered, - 'config' => $event['properties'], - ], - true, - $log - ); - - $eventName = array_key_exists('eventName', $settings) ? $settings['eventName'] : null; - - if ($eventName && $this->dispatcher->hasListeners($eventName)) { - $this->dispatcher->dispatch($eventName, $campaignEvent); - - if ($event['eventType'] !== 'decision' && $this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_EXECUTION)) { - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_EXECUTION, $campaignEvent); - } - - if ($campaignEvent->wasLogUpdatedByListener()) { - $campaignEvent->setResult($campaignEvent->getLogEntry()); - } - } - - if (null !== $log) { - $log->setChannel($campaignEvent->getChannel()) - ->setChannelId($campaignEvent->getChannelId()); - } + if (is_array($event)) { + /** @var Event $event */ + $event = $this->getEntity($event['id']); + } - return $campaignEvent->getResult(); + $config = $this->eventCollector->getEventConfig($event); + if (null === $log) { + $log = $this->getLogEntity($event, $event->getCampaign(), $lead, null, $systemTriggered); } - /* - * @deprecated 2.0 - to be removed in 3.0; Use the new eventName method instead - */ - if (isset($settings['callback']) && is_callable($settings['callback'])) { - $args = [ - 'eventSettings' => $settings, - 'eventDetails' => $eventDetails, - 'event' => $event, - 'lead' => $lead, - 'factory' => $this->factory, - 'systemTriggered' => $systemTriggered, - 'config' => $event['properties'], - ]; + switch ($event->getEventType()) { + case Event::TYPE_ACTION: + $logs = new ArrayCollection([$log]); + /* @var ActionAccessor $config */ + $this->eventDispatcher->executeActionEvent($config, $event, $logs); - if (is_array($settings['callback'])) { - $reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]); - } elseif (strpos($settings['callback'], '::') !== false) { - $parts = explode('::', $settings['callback']); - $reflection = new \ReflectionMethod($parts[0], $parts[1]); - } else { - $reflection = new \ReflectionMethod(null, $settings['callback']); - } + return !$log->getFailedLog(); + case Event::TYPE_CONDITION: + /** @var ConditionAccessor $config */ + $eventResult = $this->eventDispatcher->dispatchConditionEvent($config, $log); - $pass = []; - foreach ($reflection->getParameters() as $param) { - if (isset($args[$param->getName()])) { - $pass[] = $args[$param->getName()]; - } else { - $pass[] = null; - } - } + return $eventResult->getResult(); + case Event::TYPE_DECISION: + /** @var DecisionAccessor $config */ + $eventResult = $this->eventDispatcher->dispatchDecisionEvent($config, $log, $eventDetails); - $result = $reflection->invokeArgs($this, $pass); - - if ('decision' != $event['eventType'] && $this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_EXECUTION)) { - $executionEvent = $this->dispatcher->dispatch( - CampaignEvents::ON_EVENT_EXECUTION, - new CampaignExecutionEvent($args, $result, $log) - ); - - if ($executionEvent->wasLogUpdatedByListener()) { - $result = $executionEvent->getLogEntry(); - } - } - } else { - $result = true; + return $eventResult->getResult(); } - - return $result; } /** - * Execute or schedule an event. Condition events are executed recursively. - * * @deprecated 2.13.0 to be removed in 3.0 * - * @param array $event - * @param Campaign $campaign - * @param Lead $lead - * @param array $eventSettings + * @param $event + * @param $campaign + * @param $lead + * @param null $eventSettings * @param bool $allowNegative - * @param \DateTime $parentTriggeredDate - * @param \DateTime|bool $eventTriggerDate + * @param \DateTime|null $parentTriggeredDate + * @param null $eventTriggerDate * @param bool $logExists - * @param int $evaluatedEventCount The number of events evaluated for the current method (kickoff, negative/inaction, scheduled) - * @param int $executedEventCount The number of events successfully executed for the current method - * @param int $totalEventCount The total number of events across all methods + * @param int $evaluatedEventCount + * @param int $executedEventCount + * @param int $totalEventCount * - * @return bool + * @return array|bool|mixed + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ public function executeEvent( $event, @@ -376,298 +411,29 @@ public function executeEvent( &$executedEventCount = 0, &$totalEventCount = 0 ) { - ++$evaluatedEventCount; - ++$totalEventCount; - - // Get event settings if applicable - if ($eventSettings === null) { - $eventSettings = $this->campaignModel->getEvents(); - } - - // Set date timing should be compared with if applicable - if ($parentTriggeredDate === null) { - // Default to today - $parentTriggeredDate = new \DateTime(); - } - - $repo = $this->getRepository(); - $logRepo = $this->getLeadEventLogRepository(); - - if (isset($eventSettings[$event['eventType']][$event['type']])) { - $thisEventSettings = $eventSettings[$event['eventType']][$event['type']]; - } else { - $this->logger->debug( - 'CAMPAIGN: Settings not found for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - ); - unset($event); - - return false; - } + $counter = new Counter(); + $responses = new Responses(); - if ($event['eventType'] == 'condition') { - $allowNegative = true; + if (is_array($event)) { + /** @var Event $event */ + $event = $this->getEntity($event['id']); } - // Set campaign ID - - $event['campaign'] = [ - 'id' => $campaign->getId(), - 'name' => $campaign->getName(), - 'createdBy' => $campaign->getCreatedBy(), - ]; - - // Ensure properties is an array - if ($event['properties'] === null) { - $event['properties'] = []; - } elseif (!is_array($event['properties'])) { - $event['properties'] = unserialize($event['properties']); - } - - // Ensure triggerDate is a \DateTime - if ($event['triggerMode'] == 'date' && !$event['triggerDate'] instanceof \DateTime) { - $triggerDate = new DateTimeHelper($event['triggerDate']); - $event['triggerDate'] = $triggerDate->getDateTime(); - unset($triggerDate); - } - - if ($eventTriggerDate == null) { - $eventTriggerDate = $this->checkEventTiming($event, $parentTriggeredDate, $allowNegative); - } - $result = true; - - // Create/get log entry - if ($logExists) { - if (true === $logExists) { - $log = $logRepo->findOneBy( - [ - 'lead' => $lead->getId(), - 'event' => $event['id'], - ] - ); - } else { - $log = $this->em->getReference('MauticCampaignBundle:LeadEventLog', $logExists); - } - } - - if (empty($log)) { - $log = $this->getLogEntity($event['id'], $campaign, $lead, null, !defined('MAUTIC_CAMPAIGN_NOT_SYSTEM_TRIGGERED')); - } - - if ($eventTriggerDate instanceof \DateTime) { - ++$executedEventCount; - - $log->setTriggerDate($eventTriggerDate); - $logRepo->saveEntity($log); - - //lead actively triggered this event, a decision wasn't involved, or it was system triggered and a "no" path so schedule the event to be fired at the defined time - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' has timing that is not appropriate and thus scheduled for '.$eventTriggerDate->format('Y-m-d H:m:i T') - ); - - if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_SCHEDULED)) { - $args = [ - 'eventSettings' => $thisEventSettings, - 'eventDetails' => null, - 'event' => $event, - 'lead' => $lead, - 'systemTriggered' => true, - 'dateScheduled' => $eventTriggerDate, - ]; - - $scheduledEvent = new CampaignScheduledEvent($args); - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_SCHEDULED, $scheduledEvent); - unset($scheduledEvent, $args); - } - } elseif ($eventTriggerDate) { - // If log already existed, assume it was scheduled in order to not force - // Doctrine to do a query to fetch the information - $wasScheduled = (!$logExists) ? $log->getIsScheduled() : true; - - $log->setIsScheduled(false); - $log->setDateTriggered(new \DateTime()); - - try { - // Save before executing event to ensure it's not picked up again - $logRepo->saveEntity($log); - $this->logger->debug( - 'CAMPAIGN: Created log for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' prior to execution.' - ); - } catch (EntityNotFoundException $exception) { - // The lead has been likely removed from this lead/list - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' wasn\'t found: '.$exception->getMessage() - ); - - return false; - } catch (DBALException $exception) { - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' failed with DB error: '.$exception->getMessage() - ); - - return false; - } - - // Set the channel - $this->campaignModel->setChannelFromEventProperties($log, $event, $thisEventSettings); - - //trigger the action - $response = $this->invokeEventCallback($event, $thisEventSettings, $lead, null, true, $log); - - // Check if the lead wasn't deleted during the event callback - if (null === $lead->getId() && $response === true) { - ++$executedEventCount; - - $this->logger->debug( - 'CAMPAIGN: Contact was deleted while executing '.ucfirst($event['eventType']).' ID# '.$event['id'] - ); - - return true; - } - - $eventTriggered = false; - if ($response instanceof LeadEventLog) { - $log = $response; - - // Listener handled the event and returned a log entry - $this->campaignModel->setChannelFromEventProperties($log, $event, $thisEventSettings); + $this->eventExecutioner->executeForContact($event, $lead, $responses, $counter); - ++$executedEventCount; - - $this->logger->debug( - 'CAMPAIGN: Listener handled event for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - ); - - if (!$log->getIsScheduled()) { - $eventTriggered = true; - } - } elseif (($response === false || (is_array($response) && isset($response['result']) && false === $response['result'])) - && $event['eventType'] == 'action' - ) { - $result = false; - $debug = 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' failed with a response of '.var_export($response, true); - - // Something failed - if ($wasScheduled || !empty($this->scheduleTimeForFailedEvents)) { - $date = new \DateTime(); - $date->add(new \DateInterval($this->scheduleTimeForFailedEvents)); - - // Reschedule - $log->setTriggerDate($date); - - if (is_array($response)) { - $log->setMetadata($response); - } - $debug .= ' thus placed on hold '.$this->scheduleTimeForFailedEvents; - - $metadata = $log->getMetadata(); - if (is_array($response)) { - $metadata = array_merge($metadata, $response); - } - - $reason = null; - if (isset($metadata['errors'])) { - $reason = (is_array($metadata['errors'])) ? implode('
', $metadata['errors']) : $metadata['errors']; - } elseif (isset($metadata['reason'])) { - $reason = $metadata['reason']; - } - $this->setEventStatus($log, false, $reason); - } else { - // Remove - $debug .= ' thus deleted'; - $repo->deleteEntity($log); - unset($log); - } - - $this->notifyOfFailure($lead, $campaign->getCreatedBy(), $campaign->getName().' / '.$event['name']); - $this->logger->debug($debug); - } else { - $this->setEventStatus($log, true); - - ++$executedEventCount; - - if ($response !== true) { - if ($this->triggeredResponses !== false) { - $eventTypeKey = $event['eventType']; - $typeKey = $event['type']; - - if (!array_key_exists($eventTypeKey, $this->triggeredResponses) || !is_array($this->triggeredResponses[$eventTypeKey])) { - $this->triggeredResponses[$eventTypeKey] = []; - } - - if (!array_key_exists($typeKey, $this->triggeredResponses[$eventTypeKey]) - || !is_array( - $this->triggeredResponses[$eventTypeKey][$typeKey] - ) - ) { - $this->triggeredResponses[$eventTypeKey][$typeKey] = []; - } - - $this->triggeredResponses[$eventTypeKey][$typeKey][$event['id']] = $response; - } - - $log->setMetadata($response); - } - - $this->logger->debug( - 'CAMPAIGN: '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# '.$lead->getId() - .' successfully executed and logged with a response of '.var_export($response, true) - ); - - $eventTriggered = true; - } - - if ($eventTriggered) { - // Collect the events that were triggered so that conditions can be handled properly - - if ('condition' === $event['eventType']) { - // Conditions will need child event processed - $decisionPath = ($response === true) ? 'yes' : 'no'; - - if (true !== $response) { - // Note that a condition took non action path so we can generate a visual stat - $log->setNonActionPathTaken(true); - } - } else { - // Actions will need to check if conditions are attached to it - $decisionPath = 'null'; - } - - if (!isset($this->triggeredEvents[$event['id']])) { - $this->triggeredEvents[$event['id']] = []; - } - if (!isset($this->triggeredEvents[$event['id']][$decisionPath])) { - $this->triggeredEvents[$event['id']][$decisionPath] = []; - } - - $this->triggeredEvents[$event['id']][$decisionPath][] = $lead->getId(); - } + $evaluatedEventCount += $counter->getTotalEvaluated(); + $executedEventCount += $counter->getTotalExecuted(); + $totalEventCount += $counter->getEventCount(); - if ($log) { - $logRepo->saveEntity($log); - } - } else { - //else do nothing - $result = false; - $this->logger->debug( - 'CAMPAIGN: Timing failed ('.gettype($eventTriggerDate).') for '.ucfirst($event['eventType']).' ID# '.$event['id'].' for contact ID# ' - .$lead->getId() - ); + if (Event::TYPE_ACTION === $event->getEventType()) { + return $responses->getActionResponses(); } - if (!empty($log)) { - // Detach log - $this->em->detach($log); - unset($log); + if (Event::TYPE_CONDITION === $event->getEventType()) { + return $responses->getConditionResponses(); } - unset($eventTriggerDate, $event); - - return $result; + return true; } /** @@ -972,6 +738,8 @@ public function getLogEntity($event, $campaign, $lead = null, $ipAddress = null, /** * Batch sleep according to settings. + * + * @deprecated 2.13.0 to be removed in 3.0 */ protected function batchSleep() { diff --git a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php index a9c7576c663..3fc9d12cd2a 100644 --- a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php @@ -12,7 +12,6 @@ namespace Mautic\CampaignBundle\Tests\Command; use Doctrine\DBAL\Connection; -use Mautic\CampaignBundle\Command\TriggerCampaignCommand; use Mautic\CoreBundle\Test\MauticMysqlTestCase; class TriggerCampaignCommandTest extends MauticMysqlTestCase @@ -82,8 +81,7 @@ public function tearDown() */ public function testCampaignExecution() { - $command = new TriggerCampaignCommand(); - $this->runCommand('mautic:campaigns:trigger', ['-i' => 1], $command); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); // Let's analyze $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13]); @@ -126,7 +124,7 @@ public function testCampaignExecution() // Wait 15 seconds then execute the campaign again to send scheduled events sleep(15); - $this->runCommand('mautic:campaigns:trigger', ['-i' => 1], $command); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); // Send email 1 should no longer be scheduled $byEvent = $this->getCampaignEventLogs([2]); @@ -149,24 +147,25 @@ public function testCampaignExecution() // Now let's simulate email opens foreach ($stats as $stat) { $this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif'); - $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent())); } - // Those 25 should now have open email decisions logged and the next email sent $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14]); - $this->assertCount(25, $byEvent[3]); - $this->assertCount(25, $byEvent[10]); // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); $this->assertCount(0, $byEvent[5]); $this->assertCount(0, $byEvent[14]); + // Those 25 should now have open email decisions logged and the next email sent + $this->assertCount(25, $byEvent[3]); + $this->assertCount(25, $byEvent[10]); + // Wait 15 seconds to go beyond the inaction timeframe sleep(15); // Execute the command again to trigger inaction related events - $this->runCommand('mautic:campaigns:trigger', ['-i' => 1], $command); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); // Now we should have 50 email open decisions $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14]); diff --git a/app/bundles/CampaignBundle/Translations/en_US/messages.ini b/app/bundles/CampaignBundle/Translations/en_US/messages.ini index 92c27a1d9b5..9bd98a5b28a 100644 --- a/app/bundles/CampaignBundle/Translations/en_US/messages.ini +++ b/app/bundles/CampaignBundle/Translations/en_US/messages.ini @@ -104,11 +104,11 @@ mautic.campaign.rebuild.to_be_removed="%leads% total contact(s) to be removed in mautic.campaign.scheduled="Campaign event scheduled" mautic.campaign.trigger.event_count="%events% total events(s) to be processed in batches of %batch% contacts" mautic.campaign.trigger.events_executed="%events% event(s) executed" +mautic.campaign.trigger.decision_count_analyzed="%decisions% total decisions(s) to be analyzed for inactivity for approximately %leads% contacts in batches of %batch%" mautic.campaign.trigger.lead_count_processed="%leads% total contact(s) to be processed in batches of %batch%" -mautic.campaign.trigger.lead_count_analyzed="%leads% total contact(s) to be analyzed in batches of %batch%" -mautic.campaign.trigger.negative="Triggering 'non-action' events" +mautic.campaign.trigger.negative="Triggering events for inactive contacts" mautic.campaign.trigger.scheduled="Triggering scheduled events" -mautic.campaign.trigger.starting="Triggering first level events" +mautic.campaign.trigger.starting="Triggering events for newly added contacts" mautic.campaign.trigger.triggering="Triggering events for campaign %id%" mautic.campaign.triggered="Campaign action triggered" mautic.campaign.user.devent.description="Event description: %description%" diff --git a/app/bundles/CoreBundle/Entity/CommonRepository.php b/app/bundles/CoreBundle/Entity/CommonRepository.php index 6974e9dfca9..2e5ac87f734 100644 --- a/app/bundles/CoreBundle/Entity/CommonRepository.php +++ b/app/bundles/CoreBundle/Entity/CommonRepository.php @@ -318,7 +318,7 @@ public function getBaseColumns($entityClass, $returnColumnNames = false) * * @param array $args * - * @return Paginator + * @return array|\Doctrine\ORM\Internal\Hydration\IterableResult|Paginator */ public function getEntities(array $args = []) { diff --git a/app/config/config_test.php b/app/config/config_test.php index 5469e22291a..c3aecc01d30 100644 --- a/app/config/config_test.php +++ b/app/config/config_test.php @@ -88,7 +88,7 @@ 'formatter' => 'mautic.monolog.fulltrace.formatter', 'type' => 'rotating_file', 'path' => '%kernel.logs_dir%/%kernel.environment%.php', - 'level' => 'error', + 'level' => getenv('MAUTIC_DEBUG_LEVEL') ?: 'error', 'channels' => [ '!mautic', ], @@ -102,7 +102,7 @@ 'formatter' => 'mautic.monolog.fulltrace.formatter', 'type' => 'rotating_file', 'path' => '%kernel.logs_dir%/mautic_%kernel.environment%.php', - 'level' => 'error', + 'level' => getenv('MAUTIC_DEBUG_LEVEL') ?: 'error', 'channels' => [ 'mautic', ], From 0219f9ab5942bfbdb3618ff9114add5ce8f3cfc6 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 12:28:36 -0600 Subject: [PATCH 434/778] Fixed contactIds array input when empty --- .../Command/ContactIdsInputTrait.php | 37 +++++++++++++++++++ .../Command/TriggerCampaignCommand.php | 12 ++---- .../Command/ValidateEventCommand.php | 11 ++---- 3 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php diff --git a/app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php b/app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php new file mode 100644 index 00000000000..91866b2fcde --- /dev/null +++ b/app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php @@ -0,0 +1,37 @@ +getOption('contact-ids'); + if ($string) { + return array_map( + function ($id) { + return (int) trim($id); + }, + explode(',', $string) + ); + } + + return []; + } +} diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index ff6ab7b9493..44dc5891a92 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -34,6 +34,8 @@ */ class TriggerCampaignCommand extends ModeratedCommand { + use ContactIdsInputTrait; + /** * @var CampaignModel */ @@ -235,14 +237,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $contactMinId = $input->getOption('min-contact-id'); $contactMaxId = $input->getOption('max-contact-id'); $contactId = $input->getOption('contact-id'); - if ($contactIds = $input->getOption('contact-ids')) { - $contactIds = array_map( - function ($id) { - return (int) trim($id); - }, - explode(',', $contactIds) - ); - } + $contactIds = $this->getContactIds($input); + $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); diff --git a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php index 7b4221b0751..45dcc61ee87 100644 --- a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php +++ b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php @@ -25,6 +25,8 @@ */ class ValidateEventCommand extends Command { + use ContactIdsInputTrait; + /** * @var InactiveExecutioner */ @@ -92,14 +94,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $decisionId = $input->getOption('decision-id'); $contactId = $input->getOption('contact-id'); - if ($contactIds = $input->getOption('contact-ids')) { - $contactIds = array_map( - function ($id) { - return (int) trim($id); - }, - explode(',', $contactIds) - ); - } + $contactIds = $this->getContactIds($input); if (!$contactIds && !$contactId) { $output->writeln( From 1987238bc87145ef4bd296b537aa41590997f7be Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 12:46:48 -0600 Subject: [PATCH 435/778] fixed failed event arguments --- .../CampaignBundle/Executioner/Dispatcher/EventDispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index a721f633379..681fae09696 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -95,7 +95,7 @@ public function executeActionEvent(ActionAccessor $config, Event $event, ArrayCo $this->dispatchExecutedEvent($config, $event, $success); $failed = $pendingEvent->getFailures(); - $this->dispatchedFailedEvent($config, $event, $failed); + $this->dispatchedFailedEvent($config, $failed); $this->validateProcessedLogs($logs, $success, $failed); From 9a22253ded9dd2e8f1687318c1d9dfefa2b41c18 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 13:04:05 -0600 Subject: [PATCH 436/778] Fixed handling email failures --- .../CampaignBundle/Event/AbstractLogCollectionEvent.php | 4 +++- .../EmailBundle/EventListener/CampaignSubscriber.php | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index e08b94a0c52..4ca2ebf8b5a 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -92,7 +92,9 @@ public function getContacts() */ public function getContactIds() { - return array_keys($this->logContactXref); + $contactIds = array_keys($this->logContactXref); + + return array_combine($contactIds, $contactIds); } /** diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index d3483113295..fd39db98613 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -295,9 +295,9 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) ]; // Determine if this email is transactional/marketing - $logAssignments = []; $pending = $event->getPending(); $contacts = $event->getContacts(); + $contactIds = $event->getContactIds(); $credentialArray = []; /** @@ -311,6 +311,7 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) $pending->get($logId), $this->translator->trans('mautic.email.contact_has_no_email', ['%contact%' => $contact->getPrimaryIdentifier()]) ); + unset($contactIds[$contact->getId()]); continue; } @@ -319,7 +320,7 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) if ('marketing' == $type) { // Determine if this lead has received the email before and if so, don't send it again - $stats = $this->emailModel->getStatRepository()->checkContactsSentEmail(array_keys($logAssignments), $emailId, true); + $stats = $this->emailModel->getStatRepository()->checkContactsSentEmail($contactIds, $emailId, true); foreach ($stats as $contactId => $sent) { /** @var LeadEventLog $log */ @@ -337,7 +338,7 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) // Fail those that failed to send foreach ($errors as $failedContactId => $reason) { - $log = $pending->get($logAssignments[$failedContactId]); + $log = $event->findLogByContactId($failedContactId); $event->fail($log, $reason); unset($credentialArray[$log->getId()]); } From 3231bb0f64bfa7473add7a76c4a5e509be671fe5 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 14:57:57 -0600 Subject: [PATCH 437/778] Fixed email issue due to sendTo not indexed by contact ID --- .../EventListener/CampaignSubscriber.php | 4 +++- app/bundles/EmailBundle/Model/EmailModel.php | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index fd39db98613..622f3ea0195 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -279,7 +279,9 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) $email = $this->emailModel->getEntity($emailId); if (!$email || !$email->isPublished()) { - return $event->failAll('Email not found or published'); + $event->failAll('Email not found or published'); + + return; } $event->setChannel('email', $emailId); diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index d1c3bf74c1f..9a16c26f9da 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1170,10 +1170,20 @@ public function sendEmail(Email $email, $leads, $options = []) return false; } + // Ensure $sendTo is indexed by lead ID + $leadIds = []; $singleEmail = false; if (isset($leads['id'])) { - $singleEmail = $leads['id']; - $leads = [$leads['id'] => $leads]; + $singleEmail = $leads['id']; + $leadIds[$leads['id']] = $leads['id']; + $leads = [$leads['id'] => $leads]; + $sendTo = $leads; + } else { + $sendTo = []; + foreach ($leads as $lead) { + $sendTo[$lead['id']] = $lead; + $leadIds[$lead['id']] = $lead['id']; + } } /** @var \Mautic\EmailBundle\Entity\EmailRepository $emailRepo */ @@ -1182,10 +1192,6 @@ public function sendEmail(Email $email, $leads, $options = []) //get email settings such as templates, weights, etc $emailSettings = &$this->getEmailSettings($email); - $sendTo = $leads; - $leadIds = array_keys($sendTo); - $leadIds = array_combine($leadIds, $leadIds); - if (!$ignoreDNC) { $dnc = $emailRepo->getDoNotEmailList($leadIds); @@ -2055,7 +2061,8 @@ private function getContactCompanies(array &$sendTo) } if (!empty($fetchCompanies)) { - $companies = $this->companyModel->getRepository()->getCompaniesForContacts($fetchCompanies); // Simple dbal query that fetches lead_id IN $fetchCompanies and returns as array + // Simple dbal query that fetches lead_id IN $fetchCompanies and returns as array + $companies = $this->companyModel->getRepository()->getCompaniesForContacts(array_keys($fetchCompanies)); foreach ($companies as $contactId => $contactCompanies) { $key = $fetchCompanies[$contactId]; From c1952db437c898ad5f8d1148199b6e77d70dbc18 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 15:12:55 -0600 Subject: [PATCH 438/778] Pass empty email addresses and tell timeline UI --- app/bundles/CampaignBundle/Entity/LeadEventLog.php | 13 +++++++++++++ .../EventListener/CampaignSubscriber.php | 14 +++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLog.php b/app/bundles/CampaignBundle/Entity/LeadEventLog.php index 0ae5bf45762..a2e15eb09dd 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLog.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLog.php @@ -432,6 +432,19 @@ public function getMetadata() return $this->metadata; } + /** + * @param $metadata + */ + public function appendToMetadata($metadata) + { + if (!is_array($metadata)) { + // Assumed output for timeline + $metadata = ['timeline' => $metadata]; + } + + $this->metadata = array_merge($this->metadata, $metadata); + } + /** * @param $metadata * diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 622f3ea0195..fdfa462e582 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -309,10 +309,18 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) foreach ($contacts as $logId => $contact) { $leadCredentials = $contact->getProfileFields(); if (empty($leadCredentials['email'])) { - $event->fail( - $pending->get($logId), - $this->translator->trans('mautic.email.contact_has_no_email', ['%contact%' => $contact->getPrimaryIdentifier()]) + // Pass with a note to the UI because no use retrying + /** @var LeadEventLog $log */ + $log = $pending->get($logId); + $log->appendToMetadata( + [ + 'failed' => $this->translator->trans( + 'mautic.email.contact_already_received_marketing_email', + ['%contact%' => $contact->getPrimaryIdentifier()] + ), + ] ); + $event->pass($pending->get($logId)); unset($contactIds[$contact->getId()]); continue; } From c0efd2b6cb7ea302c7baad507fc03361906835c7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 15:44:00 -0600 Subject: [PATCH 439/778] Do not fail campaign emails because of DNC --- .../EmailBundle/EventListener/CampaignSubscriber.php | 8 ++++++++ app/bundles/EmailBundle/Model/EmailModel.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index fdfa462e582..907424f7d1d 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -349,6 +349,14 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) // Fail those that failed to send foreach ($errors as $failedContactId => $reason) { $log = $event->findLogByContactId($failedContactId); + + if ($this->translator->trans('mautic.email.dnc') === $reason) { + // Do not log DNC as errors because they'll be retried rather just let the UI know + $log->appendToMetadata(['failed' => $reason]); + $event->pass($log); + continue; + } + $event->fail($log, $reason); unset($credentialArray[$log->getId()]); } diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 9a16c26f9da..86fcc04b95e 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1322,7 +1322,7 @@ public function sendEmail(Email $email, $leads, $options = []) $this->sendModel->finalFlush(); // Get the errors to return - $errorMessages = $this->sendModel->getErrors(); + $errorMessages = array_merge($errors, $this->sendModel->getErrors()); $failedContacts = $this->sendModel->getFailedContacts(); // Get sent counts to update email stats From 580589068330f0525b980391d36a21eb3429ae07 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 15:54:23 -0600 Subject: [PATCH 440/778] Add helper method to passWithError --- .../CampaignBundle/Event/PendingEvent.php | 51 ++++++++++++++----- .../EventListener/CampaignSubscriber.php | 19 +++---- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php index fd3e8affb63..c3d45e987ca 100644 --- a/app/bundles/CampaignBundle/Event/PendingEvent.php +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -115,20 +115,30 @@ public function failAll($reason) */ public function pass(LeadEventLog $log) { - if ($failedLog = $log->getFailedLog()) { - // Delete existing entries - $failedLog->setLog(null); - $log->setFailedLog(null); - - $metadata = $log->getMetadata(); - unset($metadata['errors']); - $log->setMetadata($metadata); + $metadata = $log->getMetadata(); + unset($metadata['errors']); + if (isset($metadata['failed'])) { + unset($metadata['failed'], $metadata['reason']); } - $this->logChannel($log); - $log->setIsScheduled(false) - ->setDateTriggered($this->now); + $log->setMetadata($metadata); - $this->successful->add($log); + $this->passLog($log); + } + + /** + * @param LeadEventLog $log + * @param $error + */ + public function passWithError(LeadEventLog $log, $error) + { + $log->appendToMetadata( + [ + 'failed' => 1, + 'reason' => $error, + ] + ); + + $this->passLog($log); } /** @@ -178,4 +188,21 @@ private function logChannel(LeadEventLog $log) ->setChannelId($this->channelId); } } + + /** + * @param LeadEventLog $log + */ + private function passLog(LeadEventLog $log) + { + if ($failedLog = $log->getFailedLog()) { + // Delete existing entries + $failedLog->setLog(null); + $log->setFailedLog(null); + } + $this->logChannel($log); + $log->setIsScheduled(false) + ->setDateTriggered($this->now); + + $this->successful->add($log); + } } diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 907424f7d1d..7221b7450a6 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -310,17 +310,13 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) $leadCredentials = $contact->getProfileFields(); if (empty($leadCredentials['email'])) { // Pass with a note to the UI because no use retrying - /** @var LeadEventLog $log */ - $log = $pending->get($logId); - $log->appendToMetadata( - [ - 'failed' => $this->translator->trans( - 'mautic.email.contact_already_received_marketing_email', - ['%contact%' => $contact->getPrimaryIdentifier()] - ), - ] + $event->passWithError( + $pending->get($logId), + $this->translator->trans( + 'mautic.email.contact_already_received_marketing_email', + ['%contact%' => $contact->getPrimaryIdentifier()] + ) ); - $event->pass($pending->get($logId)); unset($contactIds[$contact->getId()]); continue; } @@ -352,8 +348,7 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) if ($this->translator->trans('mautic.email.dnc') === $reason) { // Do not log DNC as errors because they'll be retried rather just let the UI know - $log->appendToMetadata(['failed' => $reason]); - $event->pass($log); + $event->passWithError($log, $reason); continue; } From 8878516610b159d9395afa5971e5ead471088d6d Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 15:57:30 -0600 Subject: [PATCH 441/778] Prevent pass from overriding passWithError --- app/bundles/EmailBundle/EventListener/CampaignSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 7221b7450a6..ba2c0ff673c 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -345,6 +345,7 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) // Fail those that failed to send foreach ($errors as $failedContactId => $reason) { $log = $event->findLogByContactId($failedContactId); + unset($credentialArray[$log->getId()]); if ($this->translator->trans('mautic.email.dnc') === $reason) { // Do not log DNC as errors because they'll be retried rather just let the UI know @@ -353,7 +354,6 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) } $event->fail($log, $reason); - unset($credentialArray[$log->getId()]); } // Pass everyone else From d7939c89b6a1cadba8b95561e21b70794c962d94 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 18:42:20 -0600 Subject: [PATCH 442/778] Fix case of switch --- app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php | 1 - app/bundles/EmailBundle/Helper/MailHelper.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index 7f9dde6c405..9cc64f0b52e 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -439,7 +439,6 @@ public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter) } /** - * @param $campaignId * @param array $ids * * @return ArrayCollection diff --git a/app/bundles/EmailBundle/Helper/MailHelper.php b/app/bundles/EmailBundle/Helper/MailHelper.php index 8919b6cc339..ae6c2650383 100644 --- a/app/bundles/EmailBundle/Helper/MailHelper.php +++ b/app/bundles/EmailBundle/Helper/MailHelper.php @@ -548,7 +548,7 @@ public function queue($dispatchSendEvent = false, $returnMode = self::QUEUE_RESE $this->queuedRecipients = []; // Reset message - switch (ucwords($returnMode)) { + switch (strtoupper($returnMode)) { case self::QUEUE_RESET_TO: $this->message->setTo([]); $this->clearErrors(); From 75f8856c7c93d7ad17fd1efe3a65bf949ce69cc7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 19:25:01 -0600 Subject: [PATCH 443/778] Added option to bypass locking on moderated commands --- .../Command/TriggerCampaignCommand.php | 2 ++ app/bundles/CoreBundle/Command/ModeratedCommand.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 44dc5891a92..5239f74bea6 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -272,6 +272,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->triggerCampaign($campaign); } + $this->completeRun(); + return 0; } diff --git a/app/bundles/CoreBundle/Command/ModeratedCommand.php b/app/bundles/CoreBundle/Command/ModeratedCommand.php index c58bb64ffb6..0926bfd32ea 100644 --- a/app/bundles/CoreBundle/Command/ModeratedCommand.php +++ b/app/bundles/CoreBundle/Command/ModeratedCommand.php @@ -30,6 +30,7 @@ abstract class ModeratedCommand extends ContainerAwareCommand protected $lockExpiration = false; protected $lockHandler; protected $lockFile; + private $bypassLocking; /* @var OutputInterface $output */ protected $output; @@ -41,6 +42,7 @@ protected function configure() { $this ->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force execution even if another process is assumed running.') + ->addOption('--bypass-locking', '-b', InputOption::VALUE_NONE, 'Bypass locking.') ->addOption( '--timeout', '-t', @@ -67,6 +69,7 @@ protected function checkRunStatus(InputInterface $input, OutputInterface $output { $this->output = $output; $this->lockExpiration = $input->getOption('timeout'); + $this->bypassLocking = $input->getOption('bypass-locking'); $lockMode = $input->getOption('lock_mode'); if (!in_array($lockMode, ['pid', 'file_lock'])) { @@ -75,6 +78,11 @@ protected function checkRunStatus(InputInterface $input, OutputInterface $output return false; } + // If bypass locking, then don't bother locking + if ($this->bypassLocking) { + return true; + } + // Allow multiple runs of the same command if executing different IDs, etc $this->moderationKey = $this->getName().$moderationKey; @@ -110,6 +118,10 @@ protected function checkRunStatus(InputInterface $input, OutputInterface $output */ protected function completeRun() { + if ($this->bypassLocking) { + return; + } + if (self::MODE_LOCK == $this->moderationMode) { $this->lockHandler->release(); } From d211735bfaaec776a116b6363d3bd88297ac79a8 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 28 Feb 2018 19:30:03 -0600 Subject: [PATCH 444/778] Removed shortcut --- app/bundles/CoreBundle/Command/ModeratedCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/CoreBundle/Command/ModeratedCommand.php b/app/bundles/CoreBundle/Command/ModeratedCommand.php index 0926bfd32ea..3d679c54397 100644 --- a/app/bundles/CoreBundle/Command/ModeratedCommand.php +++ b/app/bundles/CoreBundle/Command/ModeratedCommand.php @@ -42,7 +42,7 @@ protected function configure() { $this ->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force execution even if another process is assumed running.') - ->addOption('--bypass-locking', '-b', InputOption::VALUE_NONE, 'Bypass locking.') + ->addOption('--bypass-locking', null, InputOption::VALUE_NONE, 'Bypass locking.') ->addOption( '--timeout', '-t', From 405b717354b1449eab465329dd79195010c6b1e3 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 7 Mar 2018 16:58:22 -0600 Subject: [PATCH 445/778] Fixed some scheduling issues with inactivity --- .../Command/ExecuteEventCommand.php | 9 +-- .../Command/TriggerCampaignCommand.php | 20 ++---- .../Command/ValidateEventCommand.php | 14 ++-- .../Command/WriteCountTrait.php | 46 +++++++++++++ .../Entity/LeadEventLogRepository.php | 8 +-- .../Event/AbstractLogCollectionEvent.php | 2 +- .../ContactFinder/ScheduledContacts.php | 2 - .../Executioner/DecisionExecutioner.php | 2 +- .../Dispatcher/EventDispatcher.php | 29 ++++++--- .../Dispatcher/LegacyEventDispatcher.php | 65 +++++++------------ .../Executioner/Event/Action.php | 2 +- .../Executioner/EventExecutioner.php | 21 +++--- .../Executioner/Helper/InactiveHelper.php | 12 ++-- .../Executioner/InactiveExecutioner.php | 5 ++ .../Executioner/KickoffExecutioner.php | 8 ++- .../Executioner/Result/Counter.php | 33 +++++++++- .../Executioner/ScheduledExecutioner.php | 7 +- .../Executioner/Scheduler/EventScheduler.php | 61 +++++++++++++---- .../Executioner/Scheduler/Mode/DateTime.php | 15 ++--- .../Executioner/Scheduler/Mode/Interval.php | 25 ++++--- .../CampaignBundle/Model/LegacyEventModel.php | 16 ++--- .../Translations/en_US/messages.ini | 3 +- 22 files changed, 249 insertions(+), 156 deletions(-) create mode 100644 app/bundles/CampaignBundle/Command/WriteCountTrait.php diff --git a/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php b/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php index a1d02ae06d8..8a7aa051513 100644 --- a/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php +++ b/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php @@ -23,6 +23,8 @@ */ class ExecuteEventCommand extends Command { + use WriteCountTrait; + /** * @var ScheduledExecutioner */ @@ -87,12 +89,7 @@ function ($id) { $counter = $this->scheduledExecutioner->executeByIds($ids, $output); - $output->writeln( - "\n". - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) - .'' - ); - $output->writeln(''); + $this->writeCounts($output, $this->translator, $counter); return 0; } diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 5239f74bea6..3a83995a472 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -35,6 +35,7 @@ class TriggerCampaignCommand extends ModeratedCommand { use ContactIdsInputTrait; + use WriteCountTrait; /** * @var CampaignModel @@ -355,11 +356,7 @@ private function executeKickoff() $counter = $this->kickoffExecutioner->execute($this->campaign, $this->limiter, $this->output); - $this->output->writeln( - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) - .'' - ); - $this->output->writeln(''); + $this->writeCounts($this->output, $this->translator, $counter); } /** @@ -375,12 +372,7 @@ private function executeScheduled() $counter = $this->scheduledExecutioner->execute($this->campaign, $this->limiter, $this->output); - $this->output->writeln( - "\n". - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) - .'' - ); - $this->output->writeln(''); + $this->writeCounts($this->output, $this->translator, $counter); } /** @@ -396,10 +388,6 @@ private function executeInactive() $counter = $this->inactiveExecutioner->execute($this->campaign, $this->limiter, $this->output); - $this->output->writeln( - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) - .'' - ); - $this->output->writeln(''); + $this->writeCounts($this->output, $this->translator, $counter); } } diff --git a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php index 45dcc61ee87..d4e10e95189 100644 --- a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php +++ b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php @@ -13,7 +13,6 @@ use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\InactiveExecutioner; -use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,6 +25,7 @@ class ValidateEventCommand extends Command { use ContactIdsInputTrait; + use WriteCountTrait; /** * @var InactiveExecutioner @@ -38,9 +38,10 @@ class ValidateEventCommand extends Command private $translator; /** - * ExecuteEventCommand constructor. + * ValidateEventCommand constructor. * - * @param ScheduledExecutioner $scheduledExecutioner + * @param InactiveExecutioner $inactiveExecutioner + * @param TranslatorInterface $translator */ public function __construct(InactiveExecutioner $inactiveExecutioner, TranslatorInterface $translator) { @@ -109,12 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $limiter = new ContactLimiter(null, $contactId, null, null, $contactIds); $counter = $this->inactiveExecution->validate($decisionId, $limiter, $output); - $output->writeln( - "\n". - ''.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%events%' => $counter->getExecuted()]) - .'' - ); - $output->writeln(''); + $this->writeCounts($output, $this->translator, $counter); return 0; } diff --git a/app/bundles/CampaignBundle/Command/WriteCountTrait.php b/app/bundles/CampaignBundle/Command/WriteCountTrait.php new file mode 100644 index 00000000000..0ce41532550 --- /dev/null +++ b/app/bundles/CampaignBundle/Command/WriteCountTrait.php @@ -0,0 +1,46 @@ +writeln( + "\n". + ''.$translator->transChoice( + 'mautic.campaign.trigger.events_executed', + $counter->getTotalExecuted(), + ['%events%' => $counter->getTotalExecuted()] + ) + .'' + ); + $output->writeln( + ''.$translator->transChoice( + 'mautic.campaign.trigger.events_scheduled', + $counter->getTotalScheduled(), + ['%events%' => $counter->getTotalScheduled()] + ) + .'' + ); + $output->writeln(''); + } +} diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index 9cc64f0b52e..43a361640b2 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -491,13 +491,13 @@ public function getScheduledCounts($campaignId, \DateTime $date, ContactLimiter ); $q->setParameter('contactId', (int) $contactId); } elseif ($minContactId = $limiter->getMinContactId()) { - $q->andWhere( + $expr->add( 'l.lead_id BETWEEN :minContactId AND :maxContactId' - ) - ->setParameter('minContactId', $minContactId) + ); + $q->setParameter('minContactId', $minContactId) ->setParameter('maxContactId', $limiter->getMaxContactId()); } elseif ($contactIds = $limiter->getContactIdList()) { - $q->andWhere( + $expr->add( $q->expr()->in('l.lead_id', $contactIds) ); } diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index 4ca2ebf8b5a..34d29cf1285 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -78,7 +78,7 @@ public function getEvent() /** * Return an array of Lead entities keyed by LeadEventLog ID. * - * @return Lead[] + * @return Lead[]|ArrayCollection */ public function getContacts() { diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php index d32ec4169ae..a00e261d17c 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php @@ -36,8 +36,6 @@ public function __construct(LeadRepository $leadRepository) * Hydrate contacts with custom field value, companies, etc. * * @param ArrayCollection $logs - * - * @return ArrayCollection */ public function hydrateContacts(ArrayCollection $logs) { diff --git a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php index bfcdeb951a2..f59012082a6 100644 --- a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php @@ -192,7 +192,7 @@ private function executeAssociatedEvents(ArrayCollection $children, \DateTime $n ' to be executed on '.$executionDate->format('Y-m-d H:i:s') ); - if ($executionDate > $now) { + if ($this->scheduler->shouldSchedule($executionDate, $now)) { $this->scheduler->scheduleForContact($child, $executionDate, $this->contact); continue; } diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index 681fae09696..f70d6abad4f 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -77,27 +77,33 @@ public function __construct( } /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs + * @param ActionAccessor $config + * @param Event $event + * @param ArrayCollection $logs + * @param PendingEvent|null $pendingEvent + * + * @return PendingEvent * * @throws LogNotProcessedException * @throws LogPassedAndFailedException + * @throws \ReflectionException */ - public function executeActionEvent(ActionAccessor $config, Event $event, ArrayCollection $logs) + public function dispatchActionEvent(ActionAccessor $config, Event $event, ArrayCollection $logs, PendingEvent $pendingEvent = null) { + if (!$pendingEvent) { + $pendingEvent = new PendingEvent($config, $event, $logs); + } + // this if statement can be removed when legacy dispatcher is removed if ($customEvent = $config->getBatchEventName()) { - $pendingEvent = new PendingEvent($config, $event, $logs); $this->dispatcher->dispatch($customEvent, $pendingEvent); $success = $pendingEvent->getSuccessful(); - $this->dispatchExecutedEvent($config, $event, $success); - - $failed = $pendingEvent->getFailures(); - $this->dispatchedFailedEvent($config, $failed); + $failed = $pendingEvent->getFailures(); $this->validateProcessedLogs($logs, $success, $failed); + $this->dispatchExecutedEvent($config, $event, $success); + $this->dispatchedFailedEvent($config, $failed); // Dispatch legacy ON_EVENT_EXECUTION event for BC $this->legacyDispatcher->dispatchExecutionEvents($config, $success, $failed); @@ -105,7 +111,9 @@ public function executeActionEvent(ActionAccessor $config, Event $event, ArrayCo // Execute BC eventName or callback. Or support case where the listener has been converted to batchEventName but still wants to execute // eventName for BC support for plugins that could be listening to it's own custom event. - $this->legacyDispatcher->dispatchCustomEvent($config, $logs, ($customEvent)); + $this->legacyDispatcher->dispatchCustomEvent($config, $event, $logs, ($customEvent), $pendingEvent); + + return $pendingEvent; } /** @@ -156,6 +164,7 @@ public function dispatchConditionEvent(ConditionAccessor $config, LeadEventLog $ /** * @param AbstractEventAccessor $config + * @param Event $event * @param ArrayCollection $logs */ public function dispatchExecutedEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 3ac57ab6fa3..97f3ea039c8 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -14,7 +14,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Entity\FailedLeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Event\CampaignDecisionEvent; use Mautic\CampaignBundle\Event\CampaignExecutionEvent; @@ -23,6 +22,7 @@ use Mautic\CampaignBundle\Event\ExecutedBatchEvent; use Mautic\CampaignBundle\Event\ExecutedEvent; use Mautic\CampaignBundle\Event\FailedEvent; +use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Factory\MauticFactory; @@ -79,25 +79,35 @@ public function __construct( LeadModel $leadModel, MauticFactory $factory ) { - $this->dispatcher = $dispatcher; - $this->scheduler = $scheduler; - $this->logger = $logger; - $this->leadModel = $leadModel; - $this->factory = $factory; + $this->dispatcher = $dispatcher; + $this->scheduler = $scheduler; + $this->logger = $logger; + $this->leadModel = $leadModel; + $this->factory = $factory; } /** * @param AbstractEventAccessor $config + * @param Event $event * @param ArrayCollection $logs * @param $wasBatchProcessed + * @param PendingEvent $pendingEvent * * @throws \ReflectionException */ - public function dispatchCustomEvent(AbstractEventAccessor $config, ArrayCollection $logs, $wasBatchProcessed) - { + public function dispatchCustomEvent( + AbstractEventAccessor $config, + Event $event, + ArrayCollection $logs, + $wasBatchProcessed, + PendingEvent $pendingEvent + ) { $settings = $config->getConfig(); if (!isset($settings['eventName']) && !isset($settings['callback'])) { + // Bad plugin + $pendingEvent->failAll('Invalid event configuration'); + return; } @@ -121,7 +131,8 @@ public function dispatchCustomEvent(AbstractEventAccessor $config, ArrayCollecti // Dispatch new events for legacy processed logs if ($this->isFailed($result)) { - $this->processFailedLog($result, $log); + $this->processFailedLog($result, $log, $pendingEvent); + $this->scheduler->rescheduleFailure($log); $this->dispatchFailedEvent($config, $log); @@ -129,8 +140,7 @@ public function dispatchCustomEvent(AbstractEventAccessor $config, ArrayCollecti continue; } - $this->processSuccessLog($log); - + $pendingEvent->pass($log); $this->dispatchExecutedEvent($config, $log); } } @@ -328,15 +338,14 @@ private function isFailed($result) { return false === $result - || (is_array($result) && isset($result['result']) && false === $result['result']) - ; + || (is_array($result) && isset($result['result']) && false === $result['result']); } /** * @param $result * @param LeadEventLog $log */ - private function processFailedLog($result, LeadEventLog $log) + private function processFailedLog($result, LeadEventLog $log, PendingEvent $pendingEvent) { $this->logger->debug( 'CAMPAIGN: '.ucfirst($log->getEvent()->getEventType()).' ID# '.$log->getEvent()->getId().' for contact ID# '.$log->getLead()->getId() @@ -358,32 +367,6 @@ private function processFailedLog($result, LeadEventLog $log) $reason = $metadata['reason']; } - if (!$failedLog = $log->getFailedLog()) { - $failedLog = new FailedLeadEventLog(); - } - - $failedLog->setLog($log) - ->setDateAdded(new \DateTime()) - ->setReason($reason); - - $log->setFailedLog($failedLog); - } - - /** - * @param LeadEventLog $log - */ - private function processSuccessLog(LeadEventLog $log) - { - if ($failedLog = $log->getFailedLog()) { - // Delete existing entries - $failedLog->setLog(null); - $log->setFailedLog(null); - } - - $metadata = $log->getMetadata(); - unset($metadata['errors']); - $log->setMetadata($metadata); - - $log->setIsScheduled(false); + $pendingEvent->fail($log, $reason); } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Action.php b/app/bundles/CampaignBundle/Executioner/Event/Action.php index 1076148ffac..f8d875d6ef9 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Action.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Action.php @@ -61,6 +61,6 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs } // Execute to process the batch of contacts - $this->dispatcher->executeActionEvent($config, $event, $logs); + $this->dispatcher->dispatchActionEvent($config, $event, $logs); } } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 3f0c1a79e71..5d69dd52ee4 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -136,14 +136,14 @@ public function executeForContact(Event $event, Lead $contact, Responses $respon * @param Event $event * @param ArrayCollection $contacts * @param Counter|null $counter - * @param bool $inactive + * @param bool $validatingInaction * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, $inactive = false) + public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, $validatingInaction = false) { if (!$contacts->count()) { $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); @@ -152,7 +152,7 @@ public function executeForContacts(Event $event, ArrayCollection $contacts, Coun } $config = $this->collector->getEventConfig($event); - $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $inactive); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $validatingInaction); $this->executeLogs($event, $logs, $counter); } @@ -281,14 +281,14 @@ public function executeContactsForConditionChildren(Event $event, ArrayCollectio * @param ArrayCollection $children * @param ArrayCollection $contacts * @param Counter $childrenCounter - * @param bool $inactive + * @param bool $validatingInaction * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, $inactive = false) + public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, $validatingInaction = false) { /** @var Event $child */ foreach ($children as $child) { @@ -298,18 +298,21 @@ public function executeContactsForChildren(ArrayCollection $children, ArrayColle continue; } - $executionDate = $this->scheduler->getExecutionDateTime($child, $this->now); + $executionDate = ($validatingInaction) ? $this->scheduler->getExecutionDateTime($child, $this->now) + : $this->scheduler->getExecutionDateTime($child, $this->now); + $this->logger->debug( 'CAMPAIGN: Event ID# '.$child->getId(). ' to be executed on '.$executionDate->format('Y-m-d H:i:s') ); - if ($executionDate > $this->now) { - $this->scheduler->schedule($child, $executionDate, $contacts, $inactive); + if ($this->scheduler->shouldSchedule($executionDate, $this->now)) { + $childrenCounter->advanceTotalScheduled($contacts->count()); + $this->scheduler->schedule($child, $executionDate, $contacts, $validatingInaction); continue; } - $this->executeForContacts($child, $contacts, $childrenCounter, $inactive); + $this->executeForContacts($child, $contacts, $childrenCounter, $validatingInaction); } } diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index ee67f1bef97..505496cf469 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -88,7 +88,7 @@ public function removeDecisionsWithoutNegativeChildren(ArrayCollection $decision /** * @param \DateTime $now * @param ArrayCollection $contacts - * @param array $lastActiveDates + * @param $eventId * @param ArrayCollection $negativeChildren * * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException @@ -117,8 +117,8 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti // We have to loop over all the events till we have a confirmed event that is overdue foreach ($negativeChildren as $event) { - $excuctionDate = $this->scheduler->getExecutionDateTime($event, $now, $lastActiveDates[$contactId]); - if ($excuctionDate <= $now) { + $executionDate = $this->scheduler->getExecutionDateTime($event, $now, $lastActiveDates[$contactId]); + if ($executionDate <= $now) { $isInactive = true; break; } @@ -165,9 +165,9 @@ public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \Date { $earliestDate = $lastActiveDate; foreach ($negativeChildren as $event) { - $excuctionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate); - if ($excuctionDate <= $earliestDate) { - $earliestDate = $excuctionDate; + $executionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate); + if ($executionDate <= $earliestDate) { + $earliestDate = $executionDate; } } diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 5447547291c..7f6e9bb839e 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -283,6 +283,11 @@ private function executeEvents() // Clear contacts from memory $this->inactiveContacts->clear(); + if ($this->limiter->getContactId()) { + // No use making another call + break; + } + // Get the next batch, starting with the max contact ID $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); } diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index 3e805558a0f..ffd310ce58f 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -215,7 +215,8 @@ private function executeOrScheduleEvent() ' compared to '.$now->format('Y-m-d H:i:s') ); - if ($executionDate > $now) { + if ($this->scheduler->shouldSchedule($executionDate, $now)) { + $this->counter->advanceTotalScheduled($contacts->count()); $this->scheduler->schedule($event, $executionDate, $contacts); continue; } @@ -226,6 +227,11 @@ private function executeOrScheduleEvent() $this->kickoffContacts->clear(); + if ($this->limiter->getContactId()) { + // No use making another call + break; + } + // Get the next batch $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->limiter); } diff --git a/app/bundles/CampaignBundle/Executioner/Result/Counter.php b/app/bundles/CampaignBundle/Executioner/Result/Counter.php index 20845fe3cca..30e60cb5569 100644 --- a/app/bundles/CampaignBundle/Executioner/Result/Counter.php +++ b/app/bundles/CampaignBundle/Executioner/Result/Counter.php @@ -39,15 +39,28 @@ class Counter private $totalExecuted = 0; /** - * Counts constructor. + * @var int */ - public function __construct($eventCount = 0, $evaluated = 0, $executed = 0, $totalEvaluated = 0, $totalExecuted = 0) + private $totalScheduled = 0; + + /** + * Counter constructor. + * + * @param int $eventCount + * @param int $evaluated + * @param int $executed + * @param int $totalEvaluated + * @param int $totalExecuted + * @param int $totalScheduled + */ + public function __construct($eventCount = 0, $evaluated = 0, $executed = 0, $totalEvaluated = 0, $totalExecuted = 0, $totalScheduled = 0) { $this->eventCount = $eventCount; $this->evaluated = $evaluated; $this->executed = $executed; $this->totalEvaluated = $totalEvaluated; $this->totalExecuted = $totalExecuted; + $this->totalScheduled = $totalScheduled; } /** @@ -135,4 +148,20 @@ public function advanceTotalExecuted($step = 1) { $this->totalExecuted += $step; } + + /** + * @return int + */ + public function getTotalScheduled() + { + return $this->totalScheduled; + } + + /** + * @param int $step + */ + public function advanceTotalScheduled($step = 1) + { + $this->totalScheduled += $step; + } } diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index d0d5d983fcf..8332f549ac1 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -288,9 +288,9 @@ private function executeOrRecheduleEvent() private function executeScheduled($eventId, \DateTime $now) { $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); - $this->scheduledContacts->hydrateContacts($logs); - while ($logs->count()) { + $this->scheduledContacts->hydrateContacts($logs); + $event = $logs->first()->getEvent(); $this->progressBar->advance($logs->count()); $this->counter->advanceEvaluated($logs->count()); @@ -330,8 +330,9 @@ private function validateSchedule(ArrayCollection $logs, \DateTime $now) ' compared to '.$now->format('Y-m-d H:i:s') ); - if ($executionDate > $now) { + if ($this->scheduler->shouldSchedule($executionDate, $now)) { // The schedule has changed for this event since first scheduled + $this->counter->advanceTotalScheduled(); $this->scheduler->reschedule($log, $executionDate); $logs->remove($key); diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index fd70a5052a2..ee42cec4cbb 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -109,15 +109,15 @@ public function scheduleForContact(Event $event, \DateTime $executionDate, Lead * @param Event $event * @param \DateTime $executionDate * @param ArrayCollection $contacts - * @param bool $inactive + * @param bool $validatingInaction */ - public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts, $inactive = false) + public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts, $validatingInaction = false) { $config = $this->collector->getEventConfig($event); foreach ($contacts as $contact) { // Create the entry - $log = $this->eventLogger->buildLogEntry($event, $contact, $inactive); + $log = $this->eventLogger->buildLogEntry($event, $contact, $validatingInaction); // Schedule it $log->setTriggerDate($executionDate); @@ -178,37 +178,69 @@ public function rescheduleFailure(LeadEventLog $log) } /** - * @param Event $event - * @param \DateTime $now - * - * @return \DateTime + * @param Event $event + * @param \DateTime|null $compareFromDateTime + * @param \DateTime|null $comparedToDateTime + ] * + * @return \DateTime|mixed * * @throws NotSchedulableException */ - public function getExecutionDateTime(Event $event, \DateTime $now = null, \DateTime $comparedToDateTime = null) + public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime = null, \DateTime $comparedToDateTime = null) { - if (null === $now) { - $now = new \DateTime(); + if (null === $compareFromDateTime) { + $compareFromDateTime = new \DateTime(); } if (null === $comparedToDateTime) { - $comparedToDateTime = clone $now; + $comparedToDateTime = clone $compareFromDateTime; } switch ($event->getTriggerMode()) { case Event::TRIGGER_MODE_IMMEDIATE: $this->logger->debug('CAMPAIGN: ('.$event->getId().') Executing immediately'); - return $now; + return $compareFromDateTime; case Event::TRIGGER_MODE_INTERVAL: - return $this->intervalScheduler->getExecutionDateTime($event, $now, $comparedToDateTime); + return $this->intervalScheduler->getExecutionDateTime($event, $compareFromDateTime, $comparedToDateTime); case Event::TRIGGER_MODE_DATE: - return $this->dateTimeScheduler->getExecutionDateTime($event, $now, $comparedToDateTime); + return $this->dateTimeScheduler->getExecutionDateTime($event, $compareFromDateTime, $comparedToDateTime); } throw new NotSchedulableException(); } + /** + * @param Event $event + * @param \DateTime $compareFromDateTime + * @param \DateTime $now + * + * @return \DateTime|mixed + * + * @throws NotSchedulableException + */ + public function getExecutionDateForInactivity(Event $event, \DateTime $compareFromDateTime, \DateTime $now) + { + $executionDate = $this->getExecutionDateTime($event, $compareFromDateTime, $now); + + if ($this->shouldSchedule($executionDate, $compareFromDateTime)) { + return $compareFromDateTime; + } + + return $executionDate; + } + + /** + * @param \DateTime $executionDate + * @param \DateTime $now + * + * @return bool + */ + public function shouldSchedule(\DateTime $executionDate, \DateTime $now) + { + return $executionDate > $now; + } + /** * @param AbstractEventAccessor $config * @param LeadEventLog $log @@ -224,6 +256,7 @@ private function dispatchScheduledEvent(AbstractEventAccessor $config, LeadEvent /** * @param AbstractEventAccessor $config + * @param Event $event * @param ArrayCollection $logs */ private function dispatchBatchScheduledEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php index 9dd87a6ec83..82802d65aeb 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php @@ -22,10 +22,9 @@ class DateTime implements ScheduleModeInterface private $logger; /** - * EventScheduler constructor. + * DateTime constructor. * * @param LoggerInterface $logger - * @param \DateTime $now */ public function __construct(LoggerInterface $logger) { @@ -34,28 +33,28 @@ public function __construct(LoggerInterface $logger) /** * @param Event $event - * @param \DateTime $now + * @param \DateTime $compareFromDateTime * @param \DateTime $comparedToDateTime * * @return \DateTime|mixed */ - public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime) + public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime, \DateTime $comparedToDateTime) { $triggerDate = $event->getTriggerDate(); if (null === $triggerDate) { $this->logger->debug('CAMPAIGN: Trigger date is null'); - return $now; + return $compareFromDateTime; } - if ($now >= $triggerDate) { + if ($compareFromDateTime >= $triggerDate) { $this->logger->debug( 'CAMPAIGN: ('.$event->getId().') Date to execute ('.$triggerDate->format('Y-m-d H:i:s T').') compared to now (' - .$now->format('Y-m-d H:i:s T').') and is thus overdue' + .$compareFromDateTime->format('Y-m-d H:i:s T').') and is thus overdue' ); - return $now; + return $compareFromDateTime; } return $triggerDate; diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php index d06330b64ce..ad8f7fb27a5 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -24,10 +24,9 @@ class Interval implements ScheduleModeInterface private $logger; /** - * EventScheduler constructor. + * Interval constructor. * * @param LoggerInterface $logger - * @param \DateTime $now */ public function __construct(LoggerInterface $logger) { @@ -35,14 +34,15 @@ public function __construct(LoggerInterface $logger) } /** - * @param Event $event - * @param \DateTime|null $comparedToDateTime + * @param Event $event + * @param \DateTime $compareFromDateTime + * @param \DateTime $comparedToDateTime * - * @return \Datetime + * @return \DateTime * * @throws NotSchedulableException */ - public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime) + public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime, \DateTime $comparedToDateTime) { $interval = $event->getTriggerInterval(); $unit = $event->getTriggerIntervalUnit(); @@ -50,11 +50,10 @@ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $co // Prevent comparisons from modifying original object $comparedToDateTime = clone $comparedToDateTime; - $this->logger->debug( - 'CAMPAIGN: ('.$event->getId().') Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format('Y-m-d H:i:s T') - ); - try { + $this->logger->debug( + 'CAMPAIGN: ('.$event->getId().') Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format('Y-m-d H:i:s T') + ); $comparedToDateTime->add((new DateTimeHelper())->buildInterval($interval, $unit)); } catch (\Exception $exception) { $this->logger->error('CAMPAIGN: Determining interval scheduled failed with "'.$exception->getMessage().'"'); @@ -62,16 +61,16 @@ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $co throw new NotSchedulableException(); } - if ($comparedToDateTime > $now) { + if ($comparedToDateTime > $compareFromDateTime) { $this->logger->debug( "CAMPAIGN: Interval of $interval $unit to execute (".$comparedToDateTime->format('Y-m-d H:i:s T').') is later than now (' - .$now->format('Y-m-d H:i:s T') + .$compareFromDateTime->format('Y-m-d H:i:s T') ); //the event is to be scheduled based on the time interval return $comparedToDateTime; } - return $now; + return $compareFromDateTime; } } diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModel.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php index bc2820657e3..b1851ba8614 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModel.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -20,6 +20,7 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; +use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\DecisionExecutioner; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; use Mautic\CampaignBundle\Executioner\EventExecutioner; @@ -151,11 +152,8 @@ public function triggerStartingEvents( $leadId = null, $returnCounts = false ) { - if ($leadId) { - $counter = $this->kickoffExecutioner->executeForContact($campaign, $leadId, $output); - } else { - $counter = $this->kickoffExecutioner->executeForCampaign($campaign, $limit, $output); - } + $limiter = new ContactLimiter($limit, $leadId, null, null); + $counter = $this->kickoffExecutioner->execute($campaign, $limiter, $output); $totalEventCount += $counter->getEventCount(); @@ -192,7 +190,8 @@ public function triggerScheduledEvents( OutputInterface $output = null, $returnCounts = false ) { - $counter = $this->scheduledExecutioner->executeForCampaign($campaign, $limit, $output); + $limiter = new ContactLimiter($limit, null, null, null); + $counter = $this->scheduledExecutioner->execute($campaign, $limiter, $output); $totalEventCount += $counter->getEventCount(); @@ -231,7 +230,8 @@ public function triggerNegativeEvents( ) { defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - $counter = $this->scheduledExecutioner->executeForCampaign($campaign, $limit, $output); + $limiter = new ContactLimiter($limit, null, null, null); + $counter = $this->scheduledExecutioner->execute($campaign, $limiter, $output); $totalEventCount += $counter->getEventCount(); @@ -360,7 +360,7 @@ public function invokeEventCallback($event, $settings, $lead = null, $eventDetai case Event::TYPE_ACTION: $logs = new ArrayCollection([$log]); /* @var ActionAccessor $config */ - $this->eventDispatcher->executeActionEvent($config, $event, $logs); + $this->eventDispatcher->dispatchActionEvent($config, $event, $logs); return !$log->getFailedLog(); case Event::TYPE_CONDITION: diff --git a/app/bundles/CampaignBundle/Translations/en_US/messages.ini b/app/bundles/CampaignBundle/Translations/en_US/messages.ini index 9bd98a5b28a..bcf97a0de7a 100644 --- a/app/bundles/CampaignBundle/Translations/en_US/messages.ini +++ b/app/bundles/CampaignBundle/Translations/en_US/messages.ini @@ -103,7 +103,8 @@ mautic.campaign.rebuild.to_be_added="%leads% total contact(s) to be added in bat mautic.campaign.rebuild.to_be_removed="%leads% total contact(s) to be removed in batches of %batch%" mautic.campaign.scheduled="Campaign event scheduled" mautic.campaign.trigger.event_count="%events% total events(s) to be processed in batches of %batch% contacts" -mautic.campaign.trigger.events_executed="%events% event(s) executed" +mautic.campaign.trigger.events_executed="{0} 0 total events were executed|{1} 1 total event was executed|[2,Inf] %events% total events were executed" +mautic.campaign.trigger.events_scheduled="{0} 0 total events were scheduled|{1} 1 total event was scheduled|[2,Inf] %events% total events were scheduled" mautic.campaign.trigger.decision_count_analyzed="%decisions% total decisions(s) to be analyzed for inactivity for approximately %leads% contacts in batches of %batch%" mautic.campaign.trigger.lead_count_processed="%leads% total contact(s) to be processed in batches of %batch%" mautic.campaign.trigger.negative="Triggering events for inactive contacts" From 5727323799da44da112eba7beff7390c67a005a9 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 7 Mar 2018 18:18:21 -0600 Subject: [PATCH 446/778] Trying to get validation to not schedule in the future --- .../Executioner/EventExecutioner.php | 12 +++-- .../Executioner/Scheduler/EventScheduler.php | 47 +++++++++++++++---- .../Executioner/Scheduler/Mode/Interval.php | 2 +- .../Scheduler/Mode/ScheduleModeInterface.php | 2 +- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 5d69dd52ee4..c8bc23fc8e9 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -290,7 +290,10 @@ public function executeContactsForConditionChildren(Event $event, ArrayCollectio */ public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, $validatingInaction = false) { - /** @var Event $child */ + $eventExecutionDates = $this->scheduler->getSortedExecutionDates($children, $this->now); + /** @var \DateTime $earliestDate */ + $earliestDate = reset($eventExecutionDates); + foreach ($children as $child) { // Ignore decisions if (Event::TYPE_DECISION == $child->getEventType()) { @@ -298,8 +301,11 @@ public function executeContactsForChildren(ArrayCollection $children, ArrayColle continue; } - $executionDate = ($validatingInaction) ? $this->scheduler->getExecutionDateTime($child, $this->now) - : $this->scheduler->getExecutionDateTime($child, $this->now); + /** @var \DateTime $executionDate */ + $executionDate = $eventExecutionDates[$child->getId()]; + if ($validatingInaction) { + $this->scheduler->getExecutionDateForInactivity($earliestDate, $this->now, $executionDate); + } $this->logger->debug( 'CAMPAIGN: Event ID# '.$child->getId(). diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index ee42cec4cbb..1a0bd2f04b0 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -186,7 +186,7 @@ public function rescheduleFailure(LeadEventLog $log) * * @throws NotSchedulableException */ - public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime = null, \DateTime $comparedToDateTime = null) + public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime = null, \DateTime $comparedToDateTime = null, $validatingInaction = false) { if (null === $compareFromDateTime) { $compareFromDateTime = new \DateTime(); @@ -211,20 +211,49 @@ public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTim } /** - * @param Event $event - * @param \DateTime $compareFromDateTime - * @param \DateTime $now + * @param ArrayCollection|Event[] $events + * @param \DateTime $now * - * @return \DateTime|mixed + * @return array * * @throws NotSchedulableException */ - public function getExecutionDateForInactivity(Event $event, \DateTime $compareFromDateTime, \DateTime $now) + public function getSortedExecutionDates(ArrayCollection $events, \DateTime $now) { - $executionDate = $this->getExecutionDateTime($event, $compareFromDateTime, $now); + $eventExecutionDates = []; + + /** @var Event $child */ + foreach ($events as $child) { + $eventExecutionDates[$child->getId()] = $this->getExecutionDateTime($child, $now); + } + uasort($eventExecutionDates, function (\DateTime $a, \DateTime $b) { + if ($a === $b) { + return 0; + } - if ($this->shouldSchedule($executionDate, $compareFromDateTime)) { - return $compareFromDateTime; + return $a < $b ? -1 : 1; + }); + + return $eventExecutionDates; + } + + /** + * @param \DateTime $earliestDate + * @param \DateTime $now + * @param \DateTime $executionDate + * + * @return \DateTime + */ + public function getExecutionDateForInactivity(\DateTime $earliestDate, \DateTime $now, \DateTime $executionDate) + { + if ($earliestDate === $executionDate) { + // Inactivity is based on the past + $executionDate = $now; + } elseif ($executionDate > $earliestDate) { + // Execute based on difference between earliest date of this group of events + $diff = $earliestDate->diff($executionDate); + $executionDate = clone $now; + $executionDate->add($diff); } return $executionDate; diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php index ad8f7fb27a5..8ae5b140a2d 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -38,7 +38,7 @@ public function __construct(LoggerInterface $logger) * @param \DateTime $compareFromDateTime * @param \DateTime $comparedToDateTime * - * @return \DateTime + * @return \DateTime|mixed * * @throws NotSchedulableException */ diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php index 76238dc3c0d..bc1f6012f48 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php @@ -20,7 +20,7 @@ interface ScheduleModeInterface * @param \DateTime $now * @param \DateTime $comparedToDateTime * - * @return \DateTime + * @return mixed */ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime); } From bad5a48f0614f0b6ee40b8e25962f1fef8ad7925 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 8 Mar 2018 17:41:36 -0600 Subject: [PATCH 447/778] Fixed scheduling for inactive paths --- .../Dispatcher/LegacyEventDispatcher.php | 4 +- .../Executioner/EventExecutioner.php | 69 ++++++++++++++----- .../Executioner/Helper/InactiveHelper.php | 39 +++++++---- .../Executioner/InactiveExecutioner.php | 13 ++-- .../Executioner/Logger/EventLogger.php | 12 ++-- .../Executioner/Scheduler/EventScheduler.php | 40 ++++++----- .../Executioner/Scheduler/Mode/Interval.php | 12 ++-- .../CampaignBundle/Model/LegacyEventModel.php | 2 +- .../Command/TriggerCampaignCommandTest.php | 47 ++++++++++--- .../Tests/Command/campaign_schema.sql | 6 +- 10 files changed, 166 insertions(+), 78 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 97f3ea039c8..69b0a9f3428 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -106,7 +106,9 @@ public function dispatchCustomEvent( if (!isset($settings['eventName']) && !isset($settings['callback'])) { // Bad plugin - $pendingEvent->failAll('Invalid event configuration'); + if (!$wasBatchProcessed) { + $pendingEvent->failAll('Invalid event configuration'); + } return; } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index c8bc23fc8e9..8a90e9f72d8 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -143,7 +143,7 @@ public function executeForContact(Event $event, Lead $contact, Responses $respon * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, $validatingInaction = false) + public function executeForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null, $isInactiveEvent = false) { if (!$contacts->count()) { $this->logger->debug('CAMPAIGN: No contacts to process for event ID '.$event->getId()); @@ -152,7 +152,7 @@ public function executeForContacts(Event $event, ArrayCollection $contacts, Coun } $config = $this->collector->getEventConfig($event); - $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $validatingInaction); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); $this->executeLogs($event, $logs, $counter); } @@ -201,12 +201,12 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte /** * @param Event $event * @param ArrayCollection $contacts - * @param bool $inactive + * @param bool $isInactiveEvent */ - public function recordLogsAsExecutedForEvent(Event $event, ArrayCollection $contacts, $inactive = false) + public function recordLogsAsExecutedForEvent(Event $event, ArrayCollection $contacts, $isInactiveEvent = false) { $config = $this->collector->getEventConfig($event); - $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $inactive); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); // Save updated log entries and clear from memory $this->eventLogger->persistCollection($logs) @@ -277,6 +277,42 @@ public function executeContactsForConditionChildren(Event $event, ArrayCollectio } } + /** + * @param ArrayCollection $children + * @param ArrayCollection $contacts + * @param Counter $childrenCounter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter) + { + foreach ($children as $child) { + // Ignore decisions + if (Event::TYPE_DECISION == $child->getEventType()) { + $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$child->getId().' as a decision'); + continue; + } + + $executionDate = $this->scheduler->getExecutionDateTime($child, $this->now); + + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$child->getId(). + ' to be executed on '.$executionDate->format('Y-m-d H:i:s') + ); + + if ($this->scheduler->shouldSchedule($executionDate, $this->now)) { + $childrenCounter->advanceTotalScheduled($contacts->count()); + $this->scheduler->schedule($child, $executionDate, $contacts); + continue; + } + + $this->executeForContacts($child, $contacts, $childrenCounter); + } + } + /** * @param ArrayCollection $children * @param ArrayCollection $contacts @@ -288,11 +324,12 @@ public function executeContactsForConditionChildren(Event $event, ArrayCollectio * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, $validatingInaction = false) + public function executeContactsForInactiveChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, \DateTime $earliestLastActiveDateTime) { - $eventExecutionDates = $this->scheduler->getSortedExecutionDates($children, $this->now); - /** @var \DateTime $earliestDate */ - $earliestDate = reset($eventExecutionDates); + $eventExecutionDates = $this->scheduler->getSortedExecutionDates($children, $earliestLastActiveDateTime); + + /** @var \DateTime $earliestExecutionDate */ + $earliestExecutionDate = reset($eventExecutionDates); foreach ($children as $child) { // Ignore decisions @@ -301,11 +338,11 @@ public function executeContactsForChildren(ArrayCollection $children, ArrayColle continue; } - /** @var \DateTime $executionDate */ - $executionDate = $eventExecutionDates[$child->getId()]; - if ($validatingInaction) { - $this->scheduler->getExecutionDateForInactivity($earliestDate, $this->now, $executionDate); - } + $executionDate = $this->scheduler->getExecutionDateForInactivity( + $eventExecutionDates[$child->getId()], + $earliestExecutionDate, + $this->now + ); $this->logger->debug( 'CAMPAIGN: Event ID# '.$child->getId(). @@ -314,11 +351,11 @@ public function executeContactsForChildren(ArrayCollection $children, ArrayColle if ($this->scheduler->shouldSchedule($executionDate, $this->now)) { $childrenCounter->advanceTotalScheduled($contacts->count()); - $this->scheduler->schedule($child, $executionDate, $contacts, $validatingInaction); + $this->scheduler->schedule($child, $executionDate, $contacts, true); continue; } - $this->executeForContacts($child, $contacts, $childrenCounter, $validatingInaction); + $this->executeForContacts($child, $contacts, $childrenCounter, true); } } diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index 505496cf469..cf991c2bd94 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -91,6 +91,8 @@ public function removeDecisionsWithoutNegativeChildren(ArrayCollection $decision * @param $eventId * @param ArrayCollection $negativeChildren * + * @return \DateTime + * * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollection $contacts, $eventId, ArrayCollection $negativeChildren) @@ -105,27 +107,32 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti $lastActiveDates = $this->inactiveContacts->getDatesAdded(); } + $earliestInactiveDate = $now; + /* @var Event $event */ foreach ($contactIds as $contactId) { if (!isset($lastActiveDates[$contactId])) { // This contact does not have a last active date so likely the event is scheduled $contacts->remove($contactId); + + $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' does not have a last active date'); + continue; } - $isInactive = false; + $earliestContactInactiveDate = $this->getEarliestInactiveDate($negativeChildren, $lastActiveDates[$contactId], $now); + $this->logger->debug( + 'CAMPAIGN: Earliest date for inactivity for contact ID# '.$contactId.' is '. + $earliestContactInactiveDate->format('Y-m-d H:i:s T').' based on last active date of '. + $lastActiveDates[$contactId]->format('Y-m-d H:i:s T') + ); - // We have to loop over all the events till we have a confirmed event that is overdue - foreach ($negativeChildren as $event) { - $executionDate = $this->scheduler->getExecutionDateTime($event, $now, $lastActiveDates[$contactId]); - if ($executionDate <= $now) { - $isInactive = true; - break; - } + if ($earliestInactiveDate < $earliestContactInactiveDate) { + $earliestInactiveDate = $earliestContactInactiveDate; } // If any are found to be inactive, we process or schedule all the events associated with the inactive path of a decision - if (!$isInactive) { + if ($earliestContactInactiveDate > $now) { $contacts->remove($contactId); $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' has been active and thus not applicable'); @@ -134,6 +141,8 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' has not been active'); } + + return $earliestInactiveDate; } /** @@ -157,20 +166,20 @@ public function getCollectionByDecisionId($decisionId) * @param ArrayCollection $negativeChildren * @param \DateTime $lastActiveDate * - * @return \DateTime + * @return \DateTime|null * * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ - public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \DateTime $lastActiveDate) + public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \DateTime $lastActiveDate, \DateTime $now) { - $earliestDate = $lastActiveDate; + $earliestDate = null; foreach ($negativeChildren as $event) { - $executionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate); - if ($executionDate <= $earliestDate) { + $executionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate, $now); + if (!$earliestDate || $executionDate < $earliestDate) { $earliestDate = $executionDate; } } - return $lastActiveDate; + return $earliestDate; } } diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 7f6e9bb839e..35b89af1128 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -268,14 +268,17 @@ private function executeEvents() $this->progressBar->advance($contacts->count()); $this->counter->advanceEvaluated($contacts->count()); - $inactiveEvents = $decisionEvent->getNegativeChildren(); - $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); + $inactiveEvents = $decisionEvent->getNegativeChildren(); + $earliestLastActiveDateTime = $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); + + $this->logger->debug( + 'CAMPAIGN: ('.$decisionEvent->getId().') Earliest date for inactivity for this batch of contacts is '. + $earliestLastActiveDateTime->format('Y-m-d H:i:s T') + ); if ($contacts->count()) { - // For simplicity sake, we're going to execute or schedule from date of evaluation - $this->executioner->setNow($now); // Execute or schedule the events attached to the inactive side of the decision - $this->executioner->executeContactsForChildren($inactiveEvents, $contacts, $this->counter, true); + $this->executioner->executeContactsForInactiveChildren($inactiveEvents, $contacts, $this->counter, $earliestLastActiveDateTime); // Record decision for these contacts $this->executioner->recordLogsAsExecutedForEvent($decisionEvent, $contacts, true); } diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index cb2d1dbccbf..3d5e0b12737 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -87,11 +87,11 @@ public function persistLog(LeadEventLog $log) /** * @param Event $event * @param null $lead - * @param bool $inactive + * @param bool $isInactiveEvent * * @return LeadEventLog */ - public function buildLogEntry(Event $event, $lead = null, $inactive = false) + public function buildLogEntry(Event $event, $lead = null, $isInactiveEvent = false) { $log = new LeadEventLog(); @@ -105,7 +105,7 @@ public function buildLogEntry(Event $event, $lead = null, $inactive = false) } $log->setLead($lead); - if ($inactive) { + if ($isInactiveEvent) { $log->setNonActionPathTaken(true); } @@ -218,15 +218,15 @@ public function extractContactsFromLogs(ArrayCollection $logs) * @param Event $event * @param AbstractEventAccessor $config * @param ArrayCollection $contacts - * @param bool $inactive + * @param bool $isInactiveEvent * * @return ArrayCollection */ - public function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts, $inactive = false) + public function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts, $isInactiveEntry = false) { // Ensure each contact has a log entry to prevent them from being picked up again prematurely foreach ($contacts as $contact) { - $log = $this->buildLogEntry($event, $contact, $inactive); + $log = $this->buildLogEntry($event, $contact, $isInactiveEntry); $log->setIsScheduled(false); $log->setDateTriggered(new \DateTime()); diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index 1a0bd2f04b0..6c1d40d866b 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -109,15 +109,15 @@ public function scheduleForContact(Event $event, \DateTime $executionDate, Lead * @param Event $event * @param \DateTime $executionDate * @param ArrayCollection $contacts - * @param bool $validatingInaction + * @param bool $isInactiveEvent */ - public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts, $validatingInaction = false) + public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $contacts, $isInactiveEvent = false) { $config = $this->collector->getEventConfig($event); foreach ($contacts as $contact) { // Create the entry - $log = $this->eventLogger->buildLogEntry($event, $contact, $validatingInaction); + $log = $this->eventLogger->buildLogEntry($event, $contact, $isInactiveEvent); // Schedule it $log->setTriggerDate($executionDate); @@ -186,14 +186,20 @@ public function rescheduleFailure(LeadEventLog $log) * * @throws NotSchedulableException */ - public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime = null, \DateTime $comparedToDateTime = null, $validatingInaction = false) + public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime = null, \DateTime $comparedToDateTime = null) { if (null === $compareFromDateTime) { $compareFromDateTime = new \DateTime(); + } else { + // Prevent comparisons from modifying original object + $compareFromDateTime = clone $compareFromDateTime; } if (null === $comparedToDateTime) { $comparedToDateTime = clone $compareFromDateTime; + } else { + // Prevent comparisons from modifying original object + $comparedToDateTime = clone $comparedToDateTime; } switch ($event->getTriggerMode()) { @@ -212,20 +218,21 @@ public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTim /** * @param ArrayCollection|Event[] $events - * @param \DateTime $now + * @param \DateTime $lastActiveDate * * @return array * * @throws NotSchedulableException */ - public function getSortedExecutionDates(ArrayCollection $events, \DateTime $now) + public function getSortedExecutionDates(ArrayCollection $events, \DateTime $lastActiveDate) { $eventExecutionDates = []; /** @var Event $child */ foreach ($events as $child) { - $eventExecutionDates[$child->getId()] = $this->getExecutionDateTime($child, $now); + $eventExecutionDates[$child->getId()] = $this->getExecutionDateTime($child, $lastActiveDate); } + uasort($eventExecutionDates, function (\DateTime $a, \DateTime $b) { if ($a === $b) { return 0; @@ -238,25 +245,20 @@ public function getSortedExecutionDates(ArrayCollection $events, \DateTime $now) } /** - * @param \DateTime $earliestDate + * @param \DateTime $eventExecutionDate + * @param \DateTime $earliestExecutionDate * @param \DateTime $now - * @param \DateTime $executionDate * * @return \DateTime */ - public function getExecutionDateForInactivity(\DateTime $earliestDate, \DateTime $now, \DateTime $executionDate) + public function getExecutionDateForInactivity(\DateTime $eventExecutionDate, \DateTime $earliestExecutionDate, \DateTime $now) { - if ($earliestDate === $executionDate) { - // Inactivity is based on the past - $executionDate = $now; - } elseif ($executionDate > $earliestDate) { - // Execute based on difference between earliest date of this group of events - $diff = $earliestDate->diff($executionDate); - $executionDate = clone $now; - $executionDate->add($diff); + if ($earliestExecutionDate->getTimestamp() === $eventExecutionDate->getTimestamp()) { + // Inactivity is based on the "wait" period so execute now + return clone $now; } - return $executionDate; + return $eventExecutionDate; } /** diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php index 8ae5b140a2d..f57e68e85fd 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -47,9 +47,6 @@ public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTim $interval = $event->getTriggerInterval(); $unit = $event->getTriggerIntervalUnit(); - // Prevent comparisons from modifying original object - $comparedToDateTime = clone $comparedToDateTime; - try { $this->logger->debug( 'CAMPAIGN: ('.$event->getId().') Adding interval of '.$interval.$unit.' to '.$comparedToDateTime->format('Y-m-d H:i:s T') @@ -63,14 +60,19 @@ public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTim if ($comparedToDateTime > $compareFromDateTime) { $this->logger->debug( - "CAMPAIGN: Interval of $interval $unit to execute (".$comparedToDateTime->format('Y-m-d H:i:s T').') is later than now (' - .$compareFromDateTime->format('Y-m-d H:i:s T') + 'CAMPAIGN: ('.$event->getId().') '.$comparedToDateTime->format('Y-m-d H:i:s T').' is later than ' + .$compareFromDateTime->format('Y-m-d H:i:s T').' and thus returning '.$comparedToDateTime->format('Y-m-d H:i:s T') ); //the event is to be scheduled based on the time interval return $comparedToDateTime; } + $this->logger->debug( + 'CAMPAIGN: ('.$event->getId().') '.$comparedToDateTime->format('Y-m-d H:i:s T').' is earlier than ' + .$compareFromDateTime->format('Y-m-d H:i:s T').' and thus returning '.$compareFromDateTime->format('Y-m-d H:i:s T') + ); + return $compareFromDateTime; } } diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModel.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php index b1851ba8614..9a5cb125307 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModel.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -637,7 +637,7 @@ public function checkEventTiming($action, \DateTime $parentTriggeredDate = null, if ($triggerOn > $now) { $this->logger->debug( - 'CAMPAIGN: Date to execute ('.$triggerOn->format('Y-m-d H:i:s T').') is later than now ('.$now->format('Y-m-d H:i:s T') + 'CAMPAIGN: Date to execute ('.$triggerOn->format('Y-m-d H:i:s T').') is later than ('.$now->format('Y-m-d H:i:s T') .')'.(($action['decisionPath'] == 'no') ? ' so ignore' : ' so schedule') ); diff --git a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php index 3fc9d12cd2a..0bcb8b6c5f4 100644 --- a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php @@ -31,6 +31,11 @@ class TriggerCampaignCommandTest extends MauticMysqlTestCase */ private $prefix; + /** + * @var \DateTime + */ + private $eventDate; + /** * @throws \Exception */ @@ -56,12 +61,12 @@ public function setUp() // Schedule event date_default_timezone_set('UTC'); - $event = new \DateTime(); - $event->modify('+15 seconds'); - $sql = str_replace('{SEND_EMAIL_1_TIMESTAMP}', $event->format('Y-m-d H:i:s'), $sql); + $this->eventDate = new \DateTime(); + $this->eventDate->modify('+15 seconds'); + $sql = str_replace('{SEND_EMAIL_1_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql); - $event->modify('+15 seconds'); - $sql = str_replace('{CONDITION_TIMESTAMP}', $event->format('Y-m-d H:i:s'), $sql); + $this->eventDate->modify('+15 seconds'); + $sql = str_replace('{CONDITION_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql); // Update the schema $tmpFile = $this->container->getParameter('kernel.cache_dir').'/campaign_schema.sql'; @@ -127,7 +132,7 @@ public function testCampaignExecution() $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); // Send email 1 should no longer be scheduled - $byEvent = $this->getCampaignEventLogs([2]); + $byEvent = $this->getCampaignEventLogs([2, 4]); $this->assertCount(50, $byEvent[2]); foreach ($byEvent[2] as $log) { if (1 === (int) $log['is_scheduled']) { @@ -135,6 +140,9 @@ public function testCampaignExecution() } } + // The non-action events attached to the decision should have no logs entries + $this->assertCount(0, $byEvent[4]); + // Check that the emails actually sent $stats = $this->db->createQueryBuilder() ->select('*') @@ -150,12 +158,13 @@ public function testCampaignExecution() $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent())); } - $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14]); + $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]); // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); $this->assertCount(0, $byEvent[5]); $this->assertCount(0, $byEvent[14]); + $this->assertCount(0, $byEvent[15]); // Those 25 should now have open email decisions logged and the next email sent $this->assertCount(25, $byEvent[3]); @@ -168,7 +177,7 @@ public function testCampaignExecution() $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); // Now we should have 50 email open decisions - $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14]); + $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); $this->assertCount(50, $byEvent[3]); // 25 should be marked as non_action_path_taken @@ -182,12 +191,34 @@ public function testCampaignExecution() // Tag EmailNotOpen should all be scheduled for these 25 contacts because the condition's timeframe was shorter and therefore the // contact was sent down the inaction path $this->assertCount(25, $byEvent[14]); + $this->assertCount(25, $byEvent[15]); + + $utcTimezone = new \DateTimeZone('UTC'); foreach ($byEvent[14] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']); } + + $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); + $diff = $this->eventDate->diff($scheduledFor); + + if (2 !== $diff->i) { + $this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)'); + } } + foreach ($byEvent[15] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']); + } + + $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); + $diff = $this->eventDate->diff($scheduledFor); + + if (6 !== $diff->i) { + $this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)'); + } + } $byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]); $tags = $this->getTagCounts(); diff --git a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql index b3a1c7e6969..5eaeeb6ca47 100644 --- a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql +++ b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql @@ -17,7 +17,8 @@ VALUES INSERT INTO `#__campaigns` (`id`,`category_id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`publish_up`,`publish_down`,`canvas_settings`) VALUES - (1,NULL,1,'2018-01-04 21:41:05',1,'Admin','2018-01-08 19:25:39',1,'Admin User',NULL,NULL,'Admin User','Campaign Test',NULL,NULL,NULL,'a:2:{s:5:\"nodes\";a:15:{i:0;a:3:{s:2:\"id\";s:1:\"1\";s:9:\"positionX\";s:3:\"577\";s:9:\"positionY\";s:3:\"155\";}i:1;a:3:{s:2:\"id\";s:1:\"2\";s:9:\"positionX\";s:3:\"842\";s:9:\"positionY\";s:3:\"164\";}i:2;a:3:{s:2:\"id\";s:1:\"3\";s:9:\"positionX\";s:3:\"842\";s:9:\"positionY\";s:3:\"269\";}i:3;a:3:{s:2:\"id\";s:2:\"11\";s:9:\"positionX\";s:3:\"389\";s:9:\"positionY\";s:3:\"252\";}i:4;a:3:{s:2:\"id\";s:1:\"4\";s:9:\"positionX\";s:4:\"1132\";s:9:\"positionY\";s:3:\"373\";}i:5;a:3:{s:2:\"id\";s:1:\"5\";s:9:\"positionX\";s:3:\"841\";s:9:\"positionY\";s:3:\"378\";}i:6;a:3:{s:2:\"id\";s:2:\"10\";s:9:\"positionX\";s:3:\"597\";s:9:\"positionY\";s:3:\"378\";}i:7;a:3:{s:2:\"id\";s:2:\"12\";s:9:\"positionX\";s:3:\"168\";s:9:\"positionY\";s:3:\"334\";}i:8;a:3:{s:2:\"id\";s:2:\"13\";s:9:\"positionX\";s:3:\"391\";s:9:\"positionY\";s:3:\"335\";}i:9;a:3:{s:2:\"id\";s:1:\"6\";s:9:\"positionX\";s:3:\"649\";s:9:\"positionY\";s:3:\"496\";}i:10;a:3:{s:2:\"id\";s:1:\"7\";s:9:\"positionX\";s:3:\"874\";s:9:\"positionY\";s:3:\"488\";}i:11;a:3:{s:2:\"id\";s:1:\"8\";s:9:\"positionX\";s:4:\"1097\";s:9:\"positionY\";s:3:\"486\";}i:12;a:3:{s:2:\"id\";s:1:\"9\";s:9:\"positionX\";s:4:\"1313\";s:9:\"positionY\";s:3:\"491\";}i:13;a:3:{s:2:\"id\";s:2:\"14\";s:9:\"positionX\";s:4:\"1372\";s:9:\"positionY\";s:3:\"364\";}i:14;a:3:{s:2:\"id\";s:5:\"lists\";s:9:\"positionX\";s:3:\"677\";s:9:\"positionY\";s:2:\"50\";}}s:11:\"connections\";a:14:{i:0;a:3:{s:8:\"sourceId\";s:5:\"lists\";s:8:\"targetId\";s:1:\"1\";s:7:\"anchors\";a:2:{s:6:\"source\";s:10:\"leadsource\";s:6:\"target\";s:3:\"top\";}}i:1;a:3:{s:8:\"sourceId\";s:5:\"lists\";s:8:\"targetId\";s:1:\"2\";s:7:\"anchors\";a:2:{s:6:\"source\";s:10:\"leadsource\";s:6:\"target\";s:3:\"top\";}}i:2;a:3:{s:8:\"sourceId\";s:1:\"2\";s:8:\"targetId\";s:1:\"3\";s:7:\"anchors\";a:2:{s:6:\"source\";s:6:\"bottom\";s:6:\"target\";s:3:\"top\";}}i:3;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:1:\"4\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:4;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:1:\"5\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:5;a:3:{s:8:\"sourceId\";s:1:\"5\";s:8:\"targetId\";s:1:\"6\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:6;a:3:{s:8:\"sourceId\";s:1:\"5\";s:8:\"targetId\";s:1:\"7\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:7;a:3:{s:8:\"sourceId\";s:1:\"4\";s:8:\"targetId\";s:1:\"8\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:8;a:3:{s:8:\"sourceId\";s:1:\"4\";s:8:\"targetId\";s:1:\"9\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:9;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"10\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:10;a:3:{s:8:\"sourceId\";s:1:\"1\";s:8:\"targetId\";s:2:\"11\";s:7:\"anchors\";a:2:{s:6:\"source\";s:6:\"bottom\";s:6:\"target\";s:3:\"top\";}}i:11;a:3:{s:8:\"sourceId\";s:2:\"11\";s:8:\"targetId\";s:2:\"12\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:12;a:3:{s:8:\"sourceId\";s:2:\"11\";s:8:\"targetId\";s:2:\"13\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:13;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"14\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}}}'); + (1, NULL, 1, '2018-01-04 21:41:05', 1, 'Admin', '2018-03-08 23:27:28', 1, 'Admin User', NULL, NULL, 'Admin User', 'Campaign Test', NULL, NULL, NULL, 'a:2:{s:5:\"nodes\";a:16:{i:0;a:3:{s:2:\"id\";s:1:\"1\";s:9:\"positionX\";s:3:\"577\";s:9:\"positionY\";s:3:\"155\";}i:1;a:3:{s:2:\"id\";s:1:\"2\";s:9:\"positionX\";s:3:\"842\";s:9:\"positionY\";s:3:\"164\";}i:2;a:3:{s:2:\"id\";s:1:\"3\";s:9:\"positionX\";s:3:\"842\";s:9:\"positionY\";s:3:\"269\";}i:3;a:3:{s:2:\"id\";s:2:\"11\";s:9:\"positionX\";s:3:\"389\";s:9:\"positionY\";s:3:\"252\";}i:4;a:3:{s:2:\"id\";s:1:\"4\";s:9:\"positionX\";s:4:\"1132\";s:9:\"positionY\";s:3:\"373\";}i:5;a:3:{s:2:\"id\";s:1:\"5\";s:9:\"positionX\";s:3:\"841\";s:9:\"positionY\";s:3:\"378\";}i:6;a:3:{s:2:\"id\";s:2:\"10\";s:9:\"positionX\";s:3:\"597\";s:9:\"positionY\";s:3:\"378\";}i:7;a:3:{s:2:\"id\";s:2:\"12\";s:9:\"positionX\";s:3:\"168\";s:9:\"positionY\";s:3:\"334\";}i:8;a:3:{s:2:\"id\";s:2:\"13\";s:9:\"positionX\";s:3:\"391\";s:9:\"positionY\";s:3:\"335\";}i:9;a:3:{s:2:\"id\";s:2:\"14\";s:9:\"positionX\";s:4:\"1372\";s:9:\"positionY\";s:3:\"364\";}i:10;a:3:{s:2:\"id\";s:1:\"6\";s:9:\"positionX\";s:3:\"649\";s:9:\"positionY\";s:3:\"496\";}i:11;a:3:{s:2:\"id\";s:1:\"7\";s:9:\"positionX\";s:3:\"874\";s:9:\"positionY\";s:3:\"488\";}i:12;a:3:{s:2:\"id\";s:1:\"8\";s:9:\"positionX\";s:4:\"1097\";s:9:\"positionY\";s:3:\"486\";}i:13;a:3:{s:2:\"id\";s:1:\"9\";s:9:\"positionX\";s:4:\"1313\";s:9:\"positionY\";s:3:\"491\";}i:14;a:3:{s:2:\"id\";s:2:\"15\";s:9:\"positionX\";s:4:\"1563\";s:9:\"positionY\";s:3:\"291\";}i:15;a:3:{s:2:\"id\";s:5:\"lists\";s:9:\"positionX\";s:3:\"677\";s:9:\"positionY\";s:2:\"50\";}}s:11:\"connections\";a:15:{i:0;a:3:{s:8:\"sourceId\";s:5:\"lists\";s:8:\"targetId\";s:1:\"1\";s:7:\"anchors\";a:2:{s:6:\"source\";s:10:\"leadsource\";s:6:\"target\";s:3:\"top\";}}i:1;a:3:{s:8:\"sourceId\";s:5:\"lists\";s:8:\"targetId\";s:1:\"2\";s:7:\"anchors\";a:2:{s:6:\"source\";s:10:\"leadsource\";s:6:\"target\";s:3:\"top\";}}i:2;a:3:{s:8:\"sourceId\";s:1:\"2\";s:8:\"targetId\";s:1:\"3\";s:7:\"anchors\";a:2:{s:6:\"source\";s:6:\"bottom\";s:6:\"target\";s:3:\"top\";}}i:3;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:1:\"4\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:4;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:1:\"5\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:5;a:3:{s:8:\"sourceId\";s:1:\"5\";s:8:\"targetId\";s:1:\"6\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:6;a:3:{s:8:\"sourceId\";s:1:\"5\";s:8:\"targetId\";s:1:\"7\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:7;a:3:{s:8:\"sourceId\";s:1:\"4\";s:8:\"targetId\";s:1:\"8\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:8;a:3:{s:8:\"sourceId\";s:1:\"4\";s:8:\"targetId\";s:1:\"9\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:9;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"10\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:10;a:3:{s:8:\"sourceId\";s:1:\"1\";s:8:\"targetId\";s:2:\"11\";s:7:\"anchors\";a:2:{s:6:\"source\";s:6:\"bottom\";s:6:\"target\";s:3:\"top\";}}i:11;a:3:{s:8:\"sourceId\";s:2:\"11\";s:8:\"targetId\";s:2:\"12\";s:7:\"anchors\";a:2:{s:6:\"source\";s:3:\"yes\";s:6:\"target\";s:3:\"top\";}}i:12;a:3:{s:8:\"sourceId\";s:2:\"11\";s:8:\"targetId\";s:2:\"13\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:13;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"14\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}i:14;a:3:{s:8:\"sourceId\";s:1:\"3\";s:8:\"targetId\";s:2:\"15\";s:7:\"anchors\";a:2:{s:6:\"source\";s:2:\"no\";s:6:\"target\";s:3:\"top\";}}}}'); + INSERT INTO `#__campaign_events` (`id`,`campaign_id`,`parent_id`,`name`,`description`,`type`,`event_type`,`event_order`,`properties`,`trigger_date`,`trigger_interval`,`trigger_interval_unit`,`trigger_mode`,`decision_path`,`temp_id`,`channel`,`channel_id`) VALUES @@ -34,7 +35,8 @@ VALUES (11,1,1,'Is US',NULL,'lead.field_value','condition',2,'a:17:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"577\";s:8:\"droppedY\";s:3:\"260\";}s:4:\"name\";s:5:\"Is US\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:6:\"bottom\";s:10:\"properties\";a:3:{s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:13:\"United States\";}s:4:\"type\";s:16:\"lead.field_value\";s:9:\"eventType\";s:9:\"condition\";s:15:\"anchorEventType\";s:6:\"action\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:5:\"field\";s:7:\"country\";s:8:\"operator\";s:1:\"=\";s:5:\"value\";s:13:\"United States\";}',NULL,1,'d','immediate',NULL,'new851108680198a4802062cf78f0d6db86407899a5',NULL,NULL), (12,1,11,'Tag US:Action',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:2:\"12\";s:8:\"droppedY\";s:3:\"357\";}s:4:\"name\";s:13:\"Tag US:Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:3:\"yes\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"6\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:9:\"US:Action\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','yes','new679bfa3e62cb59526de7fd27b556443485a174f0',NULL,NULL), (13,1,11,'Tag NonUS:Action',NULL,'lead.changetags','action',3,'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:3:\"489\";s:8:\"droppedY\";s:3:\"357\";}s:4:\"name\";s:16:\"Tag NonUS:Action\";s:11:\"triggerMode\";s:9:\"immediate\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"7\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:9:\"condition\";s:10:\"campaignId\";s:47:\"mautic_801d9c4d0208e42f6f2bae3f87d0899c3ac45b32\";s:6:\"_token\";s:43:\"uNCD4MZ1GsWRZue4ErJSTW5Tj1CX5R-NYgc5Q_BrVjw\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:12:\"NonUS:Action\";}s:11:\"remove_tags\";a:0:{}}',NULL,1,'d','immediate','no','new62e190055219ebe5beb9df4c4a505bb0860fffd4',NULL,NULL), - (14,1,3,'Tag EmailNotOpen',NULL,'lead.changetags','action',3,'a:19:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1081\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:16:\"Tag EmailNotOpen\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"oRiunE5unGEBGhTql8VkzvtTkMHpwElCu5Ul4-_gd-I\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"settings\";a:4:{s:5:\"label\";s:21:\"Modify contact\'s tags\";s:11:\"description\";s:37:\"Add tag to or remove tag from contact\";s:8:\"formType\";s:16:\"modify_lead_tags\";s:9:\"eventName\";s:38:\"mautic.lead.on_campaign_trigger_action\";}s:6:\"tempId\";s:43:\"newb3e5bfd9cdc154619ca0716b46f4a61328688a26\";s:2:\"id\";s:43:\"newb3e5bfd9cdc154619ca0716b46f4a61328688a26\";s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,2,'i','interval','no','newb3e5bfd9cdc154619ca0716b46f4a61328688a26',NULL,NULL); + (14,1,3,'Tag EmailNotOpen',NULL,'lead.changetags','action',3,'a:19:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1081\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:16:\"Tag EmailNotOpen\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"1\";s:19:\"triggerIntervalUnit\";s:1:\"d\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"oRiunE5unGEBGhTql8VkzvtTkMHpwElCu5Ul4-_gd-I\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"settings\";a:4:{s:5:\"label\";s:21:\"Modify contact\'s tags\";s:11:\"description\";s:37:\"Add tag to or remove tag from contact\";s:8:\"formType\";s:16:\"modify_lead_tags\";s:9:\"eventName\";s:38:\"mautic.lead.on_campaign_trigger_action\";}s:6:\"tempId\";s:43:\"newb3e5bfd9cdc154619ca0716b46f4a61328688a26\";s:2:\"id\";s:43:\"newb3e5bfd9cdc154619ca0716b46f4a61328688a26\";s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}',NULL,2,'i','interval','no','newb3e5bfd9cdc154619ca0716b46f4a61328688a26',NULL,NULL), + (15, 1, 3, 'Tag EmailNotOpen Again', NULL, 'lead.changetags', 'action', 3, 'a:16:{s:14:\"canvasSettings\";a:2:{s:8:\"droppedX\";s:4:\"1612\";s:8:\"droppedY\";s:3:\"374\";}s:4:\"name\";s:22:\"Tag EmailNotOpen Again\";s:11:\"triggerMode\";s:8:\"interval\";s:11:\"triggerDate\";N;s:15:\"triggerInterval\";s:1:\"6\";s:19:\"triggerIntervalUnit\";s:1:\"i\";s:6:\"anchor\";s:2:\"no\";s:10:\"properties\";a:1:{s:8:\"add_tags\";a:1:{i:0;s:1:\"9\";}}s:4:\"type\";s:15:\"lead.changetags\";s:9:\"eventType\";s:6:\"action\";s:15:\"anchorEventType\";s:8:\"decision\";s:10:\"campaignId\";s:1:\"1\";s:6:\"_token\";s:43:\"Wd8bGtv2HJ6Nyf3K90Efoo2Rn2VkDWwXhwzCIPMiD-M\";s:7:\"buttons\";a:1:{s:4:\"save\";s:0:\"\";}s:8:\"add_tags\";a:1:{i:0;s:12:\"EmailNotOpen\";}s:11:\"remove_tags\";a:0:{}}', NULL, 6, 'i', 'interval', 'no', 'newf16dfec5f2a65aa9c527675e7be516020a90daa6', NULL, NULL); INSERT INTO `#__lead_lists` (`id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`alias`,`filters`,`is_global`) VALUES From a7c8caf6c327da4c684b54abee040c4483ae9e85 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 8 Mar 2018 17:52:35 -0600 Subject: [PATCH 448/778] Don't compare earliest inactive date with now --- .../CampaignBundle/Executioner/Helper/InactiveHelper.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index cf991c2bd94..720b7d1fa62 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -120,7 +120,7 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti continue; } - $earliestContactInactiveDate = $this->getEarliestInactiveDate($negativeChildren, $lastActiveDates[$contactId], $now); + $earliestContactInactiveDate = $this->getEarliestInactiveDate($negativeChildren, $lastActiveDates[$contactId]); $this->logger->debug( 'CAMPAIGN: Earliest date for inactivity for contact ID# '.$contactId.' is '. $earliestContactInactiveDate->format('Y-m-d H:i:s T').' based on last active date of '. @@ -170,11 +170,11 @@ public function getCollectionByDecisionId($decisionId) * * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ - public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \DateTime $lastActiveDate, \DateTime $now) + public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \DateTime $lastActiveDate) { $earliestDate = null; foreach ($negativeChildren as $event) { - $executionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate, $now); + $executionDate = $this->scheduler->getExecutionDateTime($event, $lastActiveDate); if (!$earliestDate || $executionDate < $earliestDate) { $earliestDate = $executionDate; } From 2c77be8071ef82130374ab6bf87c259fe76f2d5b Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 13 Mar 2018 17:14:06 -0500 Subject: [PATCH 449/778] Fixed URL type reference --- app/bundles/CoreBundle/Model/AbstractCommonModel.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/bundles/CoreBundle/Model/AbstractCommonModel.php b/app/bundles/CoreBundle/Model/AbstractCommonModel.php index a1619d3583b..131aff048d8 100644 --- a/app/bundles/CoreBundle/Model/AbstractCommonModel.php +++ b/app/bundles/CoreBundle/Model/AbstractCommonModel.php @@ -286,8 +286,9 @@ public function decodeArrayFromUrl($string, $urlDecode = true) */ public function buildUrl($route, $routeParams = [], $absolute = true, $clickthrough = [], $utmTags = []) { - $referenceType = ($absolute) ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::RELATIVE_PATH; + $referenceType = ($absolute) ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH; $url = $this->router->generate($route, $routeParams, $referenceType); + $url .= (!empty($clickthrough)) ? '?ct='.$this->encodeArrayForUrl($clickthrough) : ''; return $url; From 4b368f6138979348cbd5b3b35650a611e7d98558 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 11 Apr 2018 18:31:13 -0600 Subject: [PATCH 450/778] Update marketing messages to use new campaign code --- .../Controller/CampaignController.php | 7 +- app/bundles/CampaignBundle/Entity/Event.php | 6 +- .../Event/AbstractLogCollectionEvent.php | 9 +- .../CampaignBundle/Event/PendingEvent.php | 67 +++- .../Dispatcher/EventDispatcher.php | 2 +- .../Dispatcher/LegacyEventDispatcher.php | 21 +- .../Executioner/Event/Action.php | 8 +- .../Executioner/Scheduler/EventScheduler.php | 1 + app/bundles/ChannelBundle/ChannelEvents.php | 18 +- app/bundles/ChannelBundle/Config/config.php | 8 +- .../EventListener/CampaignSubscriber.php | 287 ++++++++++++------ .../PreferenceBuilder/ChannelPreferences.php | 117 +++++++ .../PreferenceBuilder/PreferenceBuilder.php | 132 ++++++++ .../Translations/en_US/messages.ini | 1 + .../Translations/en_US/messages.ini | 1 + .../EventListener/CampaignSubscriber.php | 3 +- app/bundles/SmsBundle/Model/SmsModel.php | 2 + 17 files changed, 560 insertions(+), 130 deletions(-) create mode 100644 app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php create mode 100644 app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php diff --git a/app/bundles/CampaignBundle/Controller/CampaignController.php b/app/bundles/CampaignBundle/Controller/CampaignController.php index 682387e511f..0e854506e84 100644 --- a/app/bundles/CampaignBundle/Controller/CampaignController.php +++ b/app/bundles/CampaignBundle/Controller/CampaignController.php @@ -12,6 +12,7 @@ namespace Mautic\CampaignBundle\Controller; use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; use Mautic\CampaignBundle\Model\CampaignModel; use Mautic\CampaignBundle\Model\EventModel; @@ -169,8 +170,8 @@ public function viewAction($objectId) } /** - * @param $campaign - * @param $oldCampaign + * @param Campaign $campaign + * @param Campaign $oldCampaign */ protected function afterEntityClone($campaign, $oldCampaign) { @@ -183,10 +184,12 @@ protected function afterEntityClone($campaign, $oldCampaign) $campaign->setIsPublished(false); // Clone the campaign's events + /** @var Event $event */ foreach ($events as $event) { $tempEventId = 'new'.$event->getId(); $clone = clone $event; + $clone->nullId(); $clone->setCampaign($campaign); $clone->setTempId($tempEventId); diff --git a/app/bundles/CampaignBundle/Entity/Event.php b/app/bundles/CampaignBundle/Entity/Event.php index 84b9a650bb6..389edc985ca 100644 --- a/app/bundles/CampaignBundle/Entity/Event.php +++ b/app/bundles/CampaignBundle/Entity/Event.php @@ -152,7 +152,6 @@ public function __construct() */ public function __clone() { - $this->id = null; $this->tempId = null; $this->campaign = null; $this->channel = null; @@ -386,6 +385,11 @@ public function getId() return $this->id; } + public function nullId() + { + $this->id = null; + } + /** * Set order. * diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index 34d29cf1285..322f668fb44 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -15,6 +15,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; use Mautic\LeadBundle\Entity\Lead; abstract class AbstractLogCollectionEvent extends \Symfony\Component\EventDispatcher\Event @@ -98,12 +99,18 @@ public function getContactIds() } /** - * @param int $id + * @param $id * * @return mixed|null + * + * @throws NoContactsFound */ public function findLogByContactId($id) { + if (!isset($this->logContactXref[$id])) { + throw new NoContactsFound(); + } + return $this->logs->get($this->logContactXref[$id]); } diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php index c3d45e987ca..41269a68c17 100644 --- a/app/bundles/CampaignBundle/Event/PendingEvent.php +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -110,6 +110,29 @@ public function failAll($reason) } } + /** + * Fail all that have not passed yet. + */ + public function failRemaining($reason) + { + foreach ($this->logs as $log) { + if (!$this->successful->contains($log)) { + $this->fail($log, $reason); + } + } + } + + /** + * @param ArrayCollection $logs + * @param $reason + */ + public function failLogs(ArrayCollection $logs, $reason) + { + foreach ($logs as $log) { + $this->fail($log, $reason); + } + } + /** * @param LeadEventLog $log */ @@ -152,6 +175,28 @@ public function passAll() } } + /** + * @param ArrayCollection $logs + */ + public function passLogs(ArrayCollection $logs) + { + foreach ($logs as $log) { + $this->pass($log); + } + } + + /** + * Pass all that have not failed yet. + */ + public function passRemaining() + { + foreach ($this->logs as $log) { + if (!$this->failures->contains($log)) { + $this->pass($log); + } + } + } + /** * @return ArrayCollection */ @@ -178,17 +223,6 @@ public function setChannel($channel, $channelId = null) $this->channelId = $channelId; } - /** - * @param LeadEventLog $log - */ - private function logChannel(LeadEventLog $log) - { - if ($this->channel) { - $log->setChannel($this->channel) - ->setChannelId($this->channelId); - } - } - /** * @param LeadEventLog $log */ @@ -205,4 +239,15 @@ private function passLog(LeadEventLog $log) $this->successful->add($log); } + + /** + * @param LeadEventLog $log + */ + private function logChannel(LeadEventLog $log) + { + if ($this->channel) { + $log->setChannel($this->channel) + ->setChannelId($this->channelId); + } + } } diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index f70d6abad4f..a5673b1b90f 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -111,7 +111,7 @@ public function dispatchActionEvent(ActionAccessor $config, Event $event, ArrayC // Execute BC eventName or callback. Or support case where the listener has been converted to batchEventName but still wants to execute // eventName for BC support for plugins that could be listening to it's own custom event. - $this->legacyDispatcher->dispatchCustomEvent($config, $event, $logs, ($customEvent), $pendingEvent); + $this->legacyDispatcher->dispatchCustomEvent($config, $logs, ($customEvent), $pendingEvent); return $pendingEvent; } diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 69b0a9f3428..50281b526b5 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -88,7 +88,6 @@ public function __construct( /** * @param AbstractEventAccessor $config - * @param Event $event * @param ArrayCollection $logs * @param $wasBatchProcessed * @param PendingEvent $pendingEvent @@ -97,7 +96,6 @@ public function __construct( */ public function dispatchCustomEvent( AbstractEventAccessor $config, - Event $event, ArrayCollection $logs, $wasBatchProcessed, PendingEvent $pendingEvent @@ -131,6 +129,10 @@ public function dispatchCustomEvent( if (!$wasBatchProcessed) { $this->dispatchExecutionEvent($config, $log, $result); + if (is_array($result)) { + $log->appendToMetadata($result); + } + // Dispatch new events for legacy processed logs if ($this->isFailed($result)) { $this->processFailedLog($result, $log, $pendingEvent); @@ -142,7 +144,12 @@ public function dispatchCustomEvent( continue; } - $pendingEvent->pass($log); + if (is_array($result) && !empty($result['failed']) && isset($result['reason'])) { + $pendingEvent->passWithError($log, (string) $result['reason']); + } else { + $pendingEvent->pass($log); + } + $this->dispatchExecutedEvent($config, $log); } } @@ -221,10 +228,12 @@ private function dispatchEventName($eventName, array $settings, LeadEventLog $lo $this->dispatcher->dispatch($eventName, $campaignEvent); - $result = $campaignEvent->getResult(); + if ($channel = $campaignEvent->getChannel()) { + $log->setChannel($channel) + ->setChannelId($campaignEvent->getChannelId()); + } - $log->setChannel($campaignEvent->getChannel()) - ->setChannelId($campaignEvent->getChannelId()); + $result = $campaignEvent->getResult(); return $result; } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Action.php b/app/bundles/CampaignBundle/Executioner/Event/Action.php index f8d875d6ef9..22477262ff9 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Action.php +++ b/app/bundles/CampaignBundle/Executioner/Event/Action.php @@ -15,6 +15,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; @@ -34,18 +35,19 @@ class Action implements EventInterface */ public function __construct(EventDispatcher $dispatcher) { - $this->dispatcher = $dispatcher; + $this->dispatcher = $dispatcher; } /** - * @param AbstractEventAccessor $config - * @param ArrayCollection $logs + * @param ActionAccessor $config + * @param ArrayCollection $logs * * @return mixed|void * * @throws CannotProcessEventException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \ReflectionException */ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) { diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index 6c1d40d866b..1da213afba3 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -204,6 +204,7 @@ public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTim switch ($event->getTriggerMode()) { case Event::TRIGGER_MODE_IMMEDIATE: + case null: // decision $this->logger->debug('CAMPAIGN: ('.$event->getId().') Executing immediately'); return $compareFromDateTime; diff --git a/app/bundles/ChannelBundle/ChannelEvents.php b/app/bundles/ChannelBundle/ChannelEvents.php index dfc02b1a27f..5f7108f9946 100644 --- a/app/bundles/ChannelBundle/ChannelEvents.php +++ b/app/bundles/ChannelBundle/ChannelEvents.php @@ -62,14 +62,13 @@ final class ChannelEvents const PROCESS_MESSAGE_QUEUE_BATCH = 'mautic.process_message_queue_batch'; /** - * The mautic.channel.on_campaign_trigger_action event is fired when the campaign action triggers. + * The mautic.channel.on_campaign_batch_action event is dispatched when the campaign action triggers. * - * The event listener receives a - * Mautic\CampaignBundle\Event\CampaignExecutionEvent + * The event listener receives a Mautic\CampaignBundle\Event\PendingEvent * * @var string */ - const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.channel.on_campaign_trigger_action'; + const ON_CAMPAIGN_BATCH_ACTION = 'mautic.channel.on_campaign_batch_action'; /** * The mautic.message_pre_save event is dispatched right before a form is persisted. @@ -110,4 +109,15 @@ final class ChannelEvents * @var string */ const MESSAGE_POST_DELETE = 'mautic.message_post_delete'; + + /** + * @deprecated 2.13.0 to be removed in 3.0; Listen to ON_CAMPAIGN_BATCH_ACTION instead. + * The mautic.channel.on_campaign_trigger_action event is fired when the campaign action triggers. + * + * The event listener receives a + * Mautic\CampaignBundle\Event\CampaignExecutionEvent + * + * @var string + */ + const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.channel.on_campaign_trigger_action'; } diff --git a/app/bundles/ChannelBundle/Config/config.php b/app/bundles/ChannelBundle/Config/config.php index 2fae76f2b24..9ba4fc44fc7 100644 --- a/app/bundles/ChannelBundle/Config/config.php +++ b/app/bundles/ChannelBundle/Config/config.php @@ -61,11 +61,13 @@ 'services' => [ 'events' => [ 'mautic.channel.campaignbundle.subscriber' => [ - 'class' => 'Mautic\ChannelBundle\EventListener\CampaignSubscriber', + 'class' => Mautic\ChannelBundle\EventListener\CampaignSubscriber::class, 'arguments' => [ 'mautic.channel.model.message', - 'mautic.campaign.model.campaign', - 'mautic.campaign.model.event', + 'mautic.campaign.event_dispatcher', + 'mautic.campaign.event_collector', + 'monolog.logger.mautic', + 'translator', ], ], 'mautic.channel.channelbundle.subscriber' => [ diff --git a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php index 61ec582ec2f..42866d9d5f5 100644 --- a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php @@ -11,20 +11,27 @@ namespace Mautic\ChannelBundle\EventListener; +use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\CampaignEvents; +use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Event\CampaignBuilderEvent; -use Mautic\CampaignBundle\Event\CampaignExecutionEvent; -use Mautic\CampaignBundle\Model\CampaignModel; -use Mautic\CampaignBundle\Model\EventModel; +use Mautic\CampaignBundle\Event\PendingEvent; +use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; +use Mautic\CampaignBundle\EventCollector\EventCollector; +use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; use Mautic\ChannelBundle\ChannelEvents; use Mautic\ChannelBundle\Model\MessageModel; -use Mautic\CoreBundle\EventListener\CommonSubscriber; -use Mautic\LeadBundle\Entity\DoNotContact; +use Mautic\ChannelBundle\PreferenceBuilder\PreferenceBuilder; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Translation\TranslatorInterface; /** * Class CampaignSubscriber. */ -class CampaignSubscriber extends CommonSubscriber +class CampaignSubscriber implements EventSubscriberInterface { /** * @var MessageModel @@ -32,14 +39,39 @@ class CampaignSubscriber extends CommonSubscriber protected $messageModel; /** - * @var CampaignModel + * @var EventDispatcher */ - protected $campaignModel; + private $eventDispatcher; /** - * @var EventModel + * @var EventCollector */ - protected $eventModel; + private $eventCollector; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var Event + */ + private $pseudoEvent; + + /** + * @var PendingEvent + */ + private $pendingEvent; + + /** + * @var ArrayCollection + */ + private $mmLogs; /** * @var array @@ -49,15 +81,24 @@ class CampaignSubscriber extends CommonSubscriber /** * CampaignSubscriber constructor. * - * @param MessageModel $messageModel - * @param CampaignModel $campaignModel - * @param EventModel $eventModel + * @param MessageModel $messageModel + * @param EventDispatcher $eventDispatcher + * @param EventCollector $collector + * @param LoggerInterface $logger + * @param TranslatorInterface $translator */ - public function __construct(MessageModel $messageModel, CampaignModel $campaignModel, EventModel $eventModel) - { - $this->messageModel = $messageModel; - $this->campaignModel = $campaignModel; - $this->eventModel = $eventModel; + public function __construct( + MessageModel $messageModel, + EventDispatcher $eventDispatcher, + EventCollector $collector, + LoggerInterface $logger, + TranslatorInterface $translator + ) { + $this->messageModel = $messageModel; + $this->eventDispatcher = $eventDispatcher; + $this->eventCollector = $collector; + $this->logger = $logger; + $this->translator = $translator; } /** @@ -66,8 +107,8 @@ public function __construct(MessageModel $messageModel, CampaignModel $campaignM public static function getSubscribedEvents() { return [ - CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0], - ChannelEvents::ON_CAMPAIGN_TRIGGER_ACTION => ['onCampaignTriggerAction', 0], + CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0], + ChannelEvents::ON_CAMPAIGN_BATCH_ACTION => ['onCampaignTriggerAction', 0], ]; } @@ -88,6 +129,7 @@ public function onCampaignBuild(CampaignBuilderEvent $event) 'label' => 'mautic.channel.message.send.marketing.message', 'description' => 'mautic.channel.message.send.marketing.message.descr', 'eventName' => ChannelEvents::ON_CAMPAIGN_TRIGGER_ACTION, + 'batchEventName' => ChannelEvents::ON_CAMPAIGN_BATCH_ACTION, 'formType' => 'message_send', 'formTheme' => 'MauticChannelBundle:FormTheme\MessageSend', 'channel' => 'channel.message', @@ -97,8 +139,8 @@ public function onCampaignBuild(CampaignBuilderEvent $event) 'decision' => $decisions, ], ], - 'timelineTemplate' => 'MauticChannelBundle:SubscribedEvents\Timeline:index.html.php', - 'timelineTemplateVars' => [ + 'timelineTemplate' => 'MauticChannelBundle:SubscribedEvents\Timeline:index.html.php', + 'timelineTemplateVars' => [ 'messageSettings' => $channels, ], ]; @@ -106,109 +148,162 @@ public function onCampaignBuild(CampaignBuilderEvent $event) } /** - * @param CampaignExecutionEvent $event + * @param PendingEvent $pendingEvent + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \ReflectionException */ - public function onCampaignTriggerAction(CampaignExecutionEvent $event) + public function onCampaignTriggerAction(PendingEvent $pendingEvent) { + $this->pendingEvent = $pendingEvent; + $this->pseudoEvent = clone $pendingEvent->getEvent(); + $this->pseudoEvent->setCampaign($pendingEvent->getEvent()->getCampaign()); + + $this->mmLogs = $pendingEvent->getPending(); + $campaignEvent = $pendingEvent->getEvent(); + $properties = $campaignEvent->getProperties(); $messageSettings = $this->messageModel->getChannels(); - $id = (int) $event->getConfig()['marketingMessage']; + $id = (int) $properties['marketingMessage']; + + // Set channel for the event logs + $pendingEvent->setChannel('channel.message', $id); + if (!isset($this->messageChannels[$id])) { $this->messageChannels[$id] = $this->messageModel->getMessageChannels($id); } - $lead = $event->getLead(); - $channelRules = $lead->getChannelRules(); - $result = false; - $channelResults = []; - - // Use preferred channels first - $tryChannels = $this->messageChannels[$id]; - foreach ($channelRules as $channel => $rule) { - if ($rule['dnc'] !== DoNotContact::IS_CONTACTABLE) { - unset($tryChannels[$channel]); - $channelResults[$channel] = [ - 'failed' => 1, - 'dnc' => $rule['dnc'], - ]; - - continue; - } - if (isset($tryChannels[$channel])) { - $messageChannel = $tryChannels[$channel]; + // organize into preferred channels + $preferenceBuilder = new PreferenceBuilder($this->mmLogs, $this->pseudoEvent, $this->messageChannels[$id], $this->logger); - // Remove this channel so that any non-preferred channels can be used as a last resort - unset($tryChannels[$channel]); + // Loop until we have no more channels + $priority = 1; + $channelPreferences = $preferenceBuilder->getChannelPreferences(); - // Attempt to send the message - if (isset($messageSettings[$channel])) { - $result = $this->sendChannelMessage($channel, $messageChannel, $messageSettings[$channel], $event, $channelResults); + while ($priority <= count($this->messageChannels[$id])) { + foreach ($channelPreferences as $channel => $preferences) { + if (!isset($messageSettings[$channel]['campaignAction'])) { + continue; } - } - } - if (!$result && count($tryChannels)) { - // All preferred channels were a no go so try whatever is left - foreach ($tryChannels as $channel => $messageChannel) { - // Attempt to send the message through other channels - if (isset($messageSettings[$channel])) { - if ($this->sendChannelMessage($channel, $messageChannel, $messageSettings[$channel], $event, $channelResults)) { - break; - } + $channelLogs = $preferences->getLogsByPriority($priority); + if (!$channelLogs->count()) { + continue; } + + // Marketing messages mimick campaign actions so create a pseudo event + $this->pseudoEvent->setEventType(Event::TYPE_ACTION) + ->setType($messageSettings[$channel]['campaignAction']); + + $successfullyExecuted = $this->sendChannelMessage($channelLogs, $channel, $this->messageChannels[$id][$channel]); + + $this->passExecutedLogs($successfullyExecuted, $preferenceBuilder); } + ++$priority; } - return $event->setResult($channelResults); + $pendingEvent->failRemaining($this->translator->trans('mautic.channel.message.failed')); } /** - * @param $channel - * @param $messageChannel - * @param $settings - * @param CampaignExecutionEvent $event - * @param $channelResults + * @param ArrayCollection $logs + * @param string $channel + * @param array $messageChannel * - * @return bool|mixed + * @return bool|ArrayCollection + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \ReflectionException */ - protected function sendChannelMessage($channel, $messageChannel, $settings, CampaignExecutionEvent $event, &$channelResults) + protected function sendChannelMessage(ArrayCollection $logs, $channel, array $messageChannel) { - if (!isset($settings['campaignAction'])) { - return false; + /** @var ActionAccessor $config */ + $config = $this->eventCollector->getEventConfig($this->pseudoEvent); + + // Set the property set as the channel ID with the message ID + if ($channelIdField = $config->getChannelIdField()) { + $messageChannel['properties'][$channelIdField] = $messageChannel['channel_id']; } - $eventSettings = $this->campaignModel->getEvents(); - $campaignAction = $settings['campaignAction']; + $this->pseudoEvent->setProperties($messageChannel['properties']); + + // Dispatch the mimicked campaign action + $pendingEvent = new PendingEvent($config, $this->pseudoEvent, $logs); + $pendingEvent->setChannel('campaign.event', $messageChannel['channel_id']); + + $this->eventDispatcher->dispatchActionEvent( + $config, + $this->pseudoEvent, + $logs, + $pendingEvent + ); + + // Record the channel metadata mainly for debugging + $this->recordChannelMetadata($pendingEvent, $channel); + + // Remove pseudo failures so we can try the next channel + $success = $pendingEvent->getSuccessful(); + $this->removePsuedoFailures($success); + + unset($pendingEvent); - $result = false; - if (isset($eventSettings['action'][$campaignAction])) { - $campaignEventSettings = $eventSettings['action'][$campaignAction]; - $messageEvent = $event->getEvent(); - $messageEvent['type'] = $campaignAction; - $messageEvent['properties'] = $messageChannel['properties']; + return $success; + } - // Set the property set as the channel ID with the message ID - if (isset($campaignEventSettings['channelIdField'])) { - $messageEvent['properties'][$campaignEventSettings['channelIdField']] = $messageChannel['channel_id']; + /** + * @param ArrayCollection $logs + * @param PreferenceBuilder $channelPreferences + */ + private function passExecutedLogs(ArrayCollection $logs, PreferenceBuilder $channelPreferences) + { + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + // Remove those successfully executed from being processed again for lower priorities + $channelPreferences->removeLogFromAllChannels($log); + + // Find the Marketin Message log and pass it + $mmLog = $this->pendingEvent->findLogByContactId($log->getLead()->getId()); + + // Pass these for the MM campaign event + $this->pendingEvent->pass($mmLog); + } + } + + /** + * @param ArrayCollection $success + */ + private function removePsuedoFailures(ArrayCollection $success) + { + /** + * @var int + * @var LeadEventLog $log + */ + foreach ($success as $key => $log) { + if (!empty($log->getMetadata()['failed'])) { + $success->remove($key); } + } + } - $result = $this->eventModel->invokeEventCallback( - $messageEvent, - $campaignEventSettings, - $event->getLead(), - null, - $event->getSystemTriggered() - ); - - $channelResults[$channel] = $result; - if ($result) { - if (is_array($result) && !empty($result['failed'])) { - $result = false; - } elseif (!$event->getChannel()) { - $event->setChannel($channel, $messageChannel['channel_id']); + /** + * @param PendingEvent $pendingEvent + * @param ArrayCollection $mmLogs + * @param $channel + */ + private function recordChannelMetadata(PendingEvent $pendingEvent, $channel) + { + /** @var LeadEventLog $log */ + foreach ($this->mmLogs as $log) { + try { + $channelLog = $pendingEvent->findLogByContactId($log->getLead()->getId()); + + if ($metadata = $channelLog->getMetadata()) { + $log->appendToMetadata([$channel => $metadata]); } + } catch (NoContactsFound $exception) { + continue; } } - - return $result; } } diff --git a/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php b/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php new file mode 100644 index 00000000000..685930316b1 --- /dev/null +++ b/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php @@ -0,0 +1,117 @@ +channel = $channel; + $this->logger = $logger; + $this->event = $event; + } + + /** + * @param $priority + * + * @return $this + */ + public function addPriority($priority) + { + if (!isset($this->organizedByPriority[$priority])) { + $this->organizedByPriority[$priority] = new ArrayCollection(); + } + + return $this; + } + + /** + * @param LeadEventLog $log + * @param $priority + * + * @return $this + */ + public function addLog(LeadEventLog $log, $priority) + { + $this->addPriority($priority); + + // We have to clone the log to not affect the original assocaited with the MM event itself + + // Clone to remove from Doctrine's ORM memory since we're having to apply a pseudo event + $log = clone $log; + $log->setEvent($this->event); + + $this->organizedByPriority[$priority]->set($log->getId(), $log); + + return $this; + } + + /** + * Removes a log from all prioritized groups. + * + * @param LeadEventLog $log + * + * @return $this + */ + public function removeLog(LeadEventLog $log) + { + /** + * @var int + * @var ArrayCollection|LeadEventLog[] $logs + */ + foreach ($this->organizedByPriority as $priority => $logs) { + $logs->remove($log->getId()); + } + + return $this; + } + + /** + * @param $priority + * + * @return ArrayCollection|LeadEventLog[] + */ + public function getLogsByPriority($priority) + { + return isset($this->organizedByPriority[$priority]) ? $this->organizedByPriority[$priority] : new ArrayCollection(); + } +} diff --git a/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php b/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php new file mode 100644 index 00000000000..539ef82511e --- /dev/null +++ b/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php @@ -0,0 +1,132 @@ +logger = $logger; + $this->event = $event; + + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $channelRules = $log->getLead()->getChannelRules(); + $allChannels = $channels; + $priority = 1; + + // Build priority based on channel rules + foreach ($channelRules as $channel => $rule) { + $this->addChannelRule($channel, $rule, $log, $priority); + ++$priority; + unset($allChannels[$channel]); + } + + // Add the rest of the channels as least priority + foreach ($allChannels as $channel => $messageSettings) { + $this->addChannelRule($channel, ['dnc' => DoNotContact::IS_CONTACTABLE], $log, $priority); + ++$priority; + } + } + } + + /** + * @return ChannelPreferences[] + */ + public function getChannelPreferences() + { + return $this->channels; + } + + /** + * @param LeadEventLog $log + */ + public function removeLogFromAllChannels(LeadEventLog $log) + { + foreach ($this->channels as $channelPreferences) { + $channelPreferences->removeLog($log); + } + } + + /** + * @param $channel + * @param array $rule + * @param LeadEventLog $log + * @param $priority + */ + private function addChannelRule($channel, array $rule, LeadEventLog $log, $priority) + { + $channelPreferences = $this->getChannelPreferenceObject($channel, $priority); + + if ($rule['dnc'] !== DoNotContact::IS_CONTACTABLE) { + $log->appendToMetadata( + [ + $channel => [ + 'failed' => 1, + 'dnc' => $rule['dnc'], + ], + ] + ); + + return; + } + + $this->logger->debug("MARKETING MESSAGE: Set $channel as priority $priority for contact ID #".$log->getLead()->getId()); + + $channelPreferences->addLog($log, $priority); + } + + /** + * @param $channel + * + * @return ChannelPreferences + */ + private function getChannelPreferenceObject($channel, $priority) + { + if (!isset($this->channels[$channel])) { + $this->channels[$channel] = new ChannelPreferences($channel, $this->event, $this->logger); + } + + $this->channels[$channel]->addPriority($priority); + + return $this->channels[$channel]; + } +} diff --git a/app/bundles/ChannelBundle/Translations/en_US/messages.ini b/app/bundles/ChannelBundle/Translations/en_US/messages.ini index 013be2cf023..613a9f2ab93 100644 --- a/app/bundles/ChannelBundle/Translations/en_US/messages.ini +++ b/app/bundles/ChannelBundle/Translations/en_US/messages.ini @@ -2,6 +2,7 @@ mautic.channel.messages="Marketing Messages" mautic.channel.message.all_contacts="This message has been sent to the following contacts." mautic.channel.message.channel_contacts="This channel was used for the following contacts during the timeframe selected above:" mautic.channel.message.header.new="New Marketing Message" +mautic.channel.message.failed="No channel was successful in sending the message." mautic.channel.message.form.message="Message" mautic.channel.message.form.enabled="Enabled?" mautic.channel.message.send.attempts="Attempts" diff --git a/app/bundles/NotificationBundle/Translations/en_US/messages.ini b/app/bundles/NotificationBundle/Translations/en_US/messages.ini index a03f38a31c4..9b282cd7166 100644 --- a/app/bundles/NotificationBundle/Translations/en_US/messages.ini +++ b/app/bundles/NotificationBundle/Translations/en_US/messages.ini @@ -1,4 +1,5 @@ mautic.campaign.notification.send_notification="Send Notification" +mautic.channel.mobile_notification="Mobile Push Notification" mautic.notification.notification="Web Notification" mautic.notification.notifications="Web Notifications" diff --git a/app/bundles/SmsBundle/EventListener/CampaignSubscriber.php b/app/bundles/SmsBundle/EventListener/CampaignSubscriber.php index bd3e1c6a8e9..f15868dbca0 100644 --- a/app/bundles/SmsBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/SmsBundle/EventListener/CampaignSubscriber.php @@ -86,12 +86,11 @@ public function onCampaignBuild(CampaignBuilderEvent $event) /** * @param CampaignExecutionEvent $event - * - * @return mixed */ public function onCampaignTriggerAction(CampaignExecutionEvent $event) { $lead = $event->getLead(); + $smsId = (int) $event->getConfig()['sms']; $sms = $this->smsModel->getEntity($smsId); diff --git a/app/bundles/SmsBundle/Model/SmsModel.php b/app/bundles/SmsBundle/Model/SmsModel.php index dc48b229c8a..281666c328d 100644 --- a/app/bundles/SmsBundle/Model/SmsModel.php +++ b/app/bundles/SmsBundle/Model/SmsModel.php @@ -280,6 +280,8 @@ public function sendSms(Sms $sms, $sendTo, $options = []) 'sent' => false, 'status' => 'mautic.sms.campaign.failed.missing_number', ]; + + continue; } $smsEvent = new SmsSendEvent($sms->getMessage(), $lead); From c6e8e306c5b8954f1744a0a8117fc6a7011bd2d8 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 11 Apr 2018 21:55:31 -0600 Subject: [PATCH 451/778] Re-implement #5599 --- app/bundles/CampaignBundle/Config/config.php | 5 ++ .../CampaignBundle/Entity/LeadEventLog.php | 2 +- .../Executioner/EventExecutioner.php | 49 +++++++++++++++---- .../Helper/RemovedContactTracker.php | 49 +++++++++++++++++++ .../CampaignBundle/Model/CampaignModel.php | 39 +++++++++------ 5 files changed, 118 insertions(+), 26 deletions(-) create mode 100644 app/bundles/CampaignBundle/Helper/RemovedContactTracker.php diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 4ba50f67232..92e533b3aa3 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -216,6 +216,7 @@ 'mautic.lead.model.list', 'mautic.form.model.form', 'mautic.campaign.event_collector', + 'mautic.campaign.helper.removed_contact_tracker', ], ], 'mautic.campaign.model.event' => [ @@ -375,6 +376,7 @@ 'mautic.campaign.executioner.decision', 'monolog.logger.mautic', 'mautic.campaign.scheduler', + 'mautic.campaign.helper.removed_contact_tracker', ], ], 'mautic.campaign.executioner.kickoff' => [ @@ -431,6 +433,9 @@ 'monolog.logger.mautic', ], ], + 'mautic.campaign.helper.removed_contact_tracker' => [ + 'class' => \Mautic\CampaignBundle\Helper\RemovedContactTracker::class, + ], // @deprecated 2.13.0 for BC support; to be removed in 3.0 'mautic.campaign.legacy_event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher::class, diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLog.php b/app/bundles/CampaignBundle/Entity/LeadEventLog.php index a2e15eb09dd..d15a8e5d5b2 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLog.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLog.php @@ -365,7 +365,7 @@ public function setTriggerDate(\DateTime $triggerDate = null) } /** - * @return mixed + * @return Campaign */ public function getCampaign() { diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 8a90e9f72d8..9544fbc22d6 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -13,6 +13,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; +use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\TypeNotFoundException; use Mautic\CampaignBundle\EventCollector\EventCollector; @@ -24,6 +25,7 @@ use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\CampaignBundle\Executioner\Result\Responses; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; +use Mautic\CampaignBundle\Helper\RemovedContactTracker; use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; @@ -74,6 +76,11 @@ class EventExecutioner */ private $responses; + /** + * @var RemovedContactTracker + */ + private $removedContactTracker; + /** * EventExecutioner constructor. * @@ -92,16 +99,18 @@ public function __construct( Condition $conditionExecutioner, Decision $decisionExecutioner, LoggerInterface $logger, - EventScheduler $scheduler + EventScheduler $scheduler, + RemovedContactTracker $removedContactTracker ) { - $this->actionExecutioner = $actionExecutioner; - $this->conditionExecutioner = $conditionExecutioner; - $this->decisionExecutioner = $decisionExecutioner; - $this->collector = $eventCollector; - $this->eventLogger = $eventLogger; - $this->logger = $logger; - $this->scheduler = $scheduler; - $this->now = new \DateTime(); + $this->actionExecutioner = $actionExecutioner; + $this->conditionExecutioner = $conditionExecutioner; + $this->decisionExecutioner = $decisionExecutioner; + $this->collector = $eventCollector; + $this->eventLogger = $eventLogger; + $this->logger = $logger; + $this->scheduler = $scheduler; + $this->removedContactTracker = $removedContactTracker; + $this->now = new \DateTime(); } /** @@ -179,6 +188,8 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte $config = $this->collector->getEventConfig($event); + $this->checkForRemovedContacts($logs); + if ($counter) { $counter->advanceExecuted($logs->count()); } @@ -440,4 +451,24 @@ private function persistLogs(ArrayCollection $logs) $this->eventLogger->persistCollection($logs) ->clear(); } + + /** + * @param ArrayCollection $logs + */ + private function checkForRemovedContacts(ArrayCollection $logs) + { + /** + * @var int + * @var LeadEventLog $log + */ + foreach ($logs as $key => $log) { + $contactId = $log->getLead()->getId(); + $campaignId = $log->getCampaign()->getId(); + + if ($this->removedContactTracker->wasContactRemoved($campaignId, $contactId)) { + $this->logger->debug("CAMPAIGN: Contact ID# $contactId has been removed from campaign ID $campaignId"); + $logs->remove($key); + } + } + } } diff --git a/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php b/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php new file mode 100644 index 00000000000..03471456760 --- /dev/null +++ b/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php @@ -0,0 +1,49 @@ +removedContacts[$campaignId])) { + $this->removedContacts[$campaignId] = []; + } + + $this->removedContacts[$campaignId][$contactId] = $contactId; + } + + /** + * @param $campaignId + */ + public function wasContactRemoved($campaignId, $contactId) + { + return !empty($this->removedContacts[$campaignId][$contactId]); + } + + /** + * @return array + */ + public function getRemovedContacts() + { + return $this->removedContacts; + } +} diff --git a/app/bundles/CampaignBundle/Model/CampaignModel.php b/app/bundles/CampaignBundle/Model/CampaignModel.php index ec97cf3a8cc..af27d9a710f 100644 --- a/app/bundles/CampaignBundle/Model/CampaignModel.php +++ b/app/bundles/CampaignBundle/Model/CampaignModel.php @@ -18,6 +18,7 @@ use Mautic\CampaignBundle\Entity\Lead as CampaignLead; use Mautic\CampaignBundle\Event as Events; use Mautic\CampaignBundle\EventCollector\EventCollector; +use Mautic\CampaignBundle\Helper\RemovedContactTracker; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; use Mautic\CoreBundle\Helper\CoreParametersHelper; @@ -69,24 +70,27 @@ class CampaignModel extends CommonFormModel private $eventCollector; /** - * @var array + * @var RemovedContactTracker */ - private $removedLeads = []; + private $removedContactTracker; /** * CampaignModel constructor. * - * @param CoreParametersHelper $coreParametersHelper - * @param LeadModel $leadModel - * @param ListModel $leadListModel - * @param FormModel $formModel + * @param CoreParametersHelper $coreParametersHelper + * @param LeadModel $leadModel + * @param ListModel $leadListModel + * @param FormModel $formModel + * @param EventCollector $eventCollector + * @param RemovedContactTracker $removedContactTracker */ public function __construct( CoreParametersHelper $coreParametersHelper, LeadModel $leadModel, ListModel $leadListModel, FormModel $formModel, - EventCollector $eventCollector + EventCollector $eventCollector, + RemovedContactTracker $removedContactTracker ) { $this->leadModel = $leadModel; $this->leadListModel = $leadListModel; @@ -94,6 +98,7 @@ public function __construct( $this->batchSleepTime = $coreParametersHelper->getParameter('mautic.batch_sleep_time'); $this->batchCampaignSleepTime = $coreParametersHelper->getParameter('mautic.batch_campaign_sleep_time'); $this->eventCollector = $eventCollector; + $this->removedContactTracker = $removedContactTracker; } /** @@ -782,7 +787,7 @@ public function removeLeads(Campaign $campaign, array $leads, $manuallyRemoved = $lead = $this->em->getReference('MauticLeadBundle:Lead', $leadId); } - $this->removedLeads[$campaign->getId()][$leadId] = $leadId; + $this->removedContactTracker->addRemovedContact($campaign->getId(), $leadId); $campaignLead = (!$skipFindOne) ? $this->getCampaignLeadRepository()->findOneBy( @@ -846,14 +851,6 @@ public function removeLeads(Campaign $campaign, array $leads, $manuallyRemoved = } } - /** - * @return array - */ - public function getRemovedLeads() - { - return $this->removedLeads; - } - /** * Get details of leads in a campaign. * @@ -1306,4 +1303,14 @@ public function setChannelFromEventProperties($entity, $properties, &$eventSetti return $channelSet; } + + /** + * @return array + * + * @deprecated 2.14.0 to be removed in 3.0 + */ + public function getRemovedLeads() + { + return $this->removedContactTracker->getRemovedContacts(); + } } From dcf4d178f45c684510118da08e0b1c8f6763c4ab Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 25 Apr 2018 16:14:32 -0500 Subject: [PATCH 452/778] Fixed tests --- .../Tests/CampaignTestAbstract.php | 12 +++++++++++- .../EventListener/CampaignSubscriber.php | 6 +----- .../EventListener/CampaignSubscriberTest.php | 19 ++++++++++++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php b/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php index 7d02e4baee0..23dbbeec359 100644 --- a/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php +++ b/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php @@ -12,6 +12,8 @@ namespace Mautic\CampaignBundle\Tests; use Doctrine\ORM\EntityManager; +use Mautic\CampaignBundle\EventCollector\EventCollector; +use Mautic\CampaignBundle\Helper\RemovedContactTracker; use Mautic\CampaignBundle\Model\CampaignModel; use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\CoreBundle\Helper\UserHelper; @@ -80,7 +82,15 @@ protected function initCampaignModel() ->method('getRepository') ->will($this->returnValue($formRepository)); - $campaignModel = new CampaignModel($coreParametersHelper, $leadModel, $leadListModel, $formModel); + $eventCollector = $this->getMockBuilder(EventCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $removedContactTracker = $this->getMockBuilder(RemovedContactTracker::class) + ->disableOriginalConstructor() + ->getMock(); + + $campaignModel = new CampaignModel($coreParametersHelper, $leadModel, $leadListModel, $formModel, $eventCollector, $removedContactTracker); $leadModel->setEntityManager($entityManager); $leadListModel->setEntityManager($entityManager); diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index ba2c0ff673c..c2ed7856602 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -367,10 +367,8 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) * Triggers the action which sends email to user, contact owner or specified email addresses. * * @param CampaignExecutionEvent $event - * - * @return CampaignExecutionEvent|null */ - public function onCampaignTriggerActionSendEmailToUser(PendingEvent $event) + public function onCampaignTriggerActionSendEmailToUser(CampaignExecutionEvent $event) { if (!$event->checkContext('email.send.to.user')) { return; @@ -385,7 +383,5 @@ public function onCampaignTriggerActionSendEmailToUser(PendingEvent $event) } catch (EmailCouldNotBeSentException $e) { $event->setFailed($e->getMessage()); } - - return $event; } } diff --git a/app/bundles/EmailBundle/Tests/EventListener/CampaignSubscriberTest.php b/app/bundles/EmailBundle/Tests/EventListener/CampaignSubscriberTest.php index ddcba5ff304..e438057bbef 100644 --- a/app/bundles/EmailBundle/Tests/EventListener/CampaignSubscriberTest.php +++ b/app/bundles/EmailBundle/Tests/EventListener/CampaignSubscriberTest.php @@ -20,6 +20,7 @@ use Mautic\EmailBundle\Model\SendEmailToUser; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; +use Symfony\Component\Translation\TranslatorInterface; class CampaignSubscriberTest extends \PHPUnit_Framework_TestCase { @@ -56,7 +57,11 @@ public function testOnCampaignTriggerActionSendEmailToUserWithWrongEventType() ->disableOriginalConstructor() ->getMock(); - $subscriber = new CampaignSubscriber($mockLeadModel, $mockEmailModel, $mockEventModel, $mockMessageQueueModel, $mockSendEmailToUser); + $mockTranslator = $this->getMockBuilder(TranslatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $subscriber = new CampaignSubscriber($mockLeadModel, $mockEmailModel, $mockEventModel, $mockMessageQueueModel, $mockSendEmailToUser, $mockTranslator); $args = [ 'lead' => 64, @@ -98,7 +103,11 @@ public function testOnCampaignTriggerActionSendEmailToUserWithSendingTheEmail() ->disableOriginalConstructor() ->getMock(); - $subscriber = new CampaignSubscriber($mockLeadModel, $mockEmailModel, $mockEventModel, $mockMessageQueueModel, $mockSendEmailToUser); + $mockTranslator = $this->getMockBuilder(TranslatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $subscriber = new CampaignSubscriber($mockLeadModel, $mockEmailModel, $mockEventModel, $mockMessageQueueModel, $mockSendEmailToUser, $mockTranslator); $args = [ 'lead' => $lead, @@ -147,7 +156,11 @@ public function testOnCampaignTriggerActionSendEmailToUserWithError() ->disableOriginalConstructor() ->getMock(); - $subscriber = new CampaignSubscriber($mockLeadModel, $mockEmailModel, $mockEventModel, $mockMessageQueueModel, $mockSendEmailToUser); + $mockTranslator = $this->getMockBuilder(TranslatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $subscriber = new CampaignSubscriber($mockLeadModel, $mockEmailModel, $mockEventModel, $mockMessageQueueModel, $mockSendEmailToUser, $mockTranslator); $args = [ 'lead' => $lead, From 4eb659fcda954b23df45510e8a0304aa8dd1d61f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 09:36:45 -0500 Subject: [PATCH 453/778] Fixed tests --- app/bundles/EmailBundle/EventListener/CampaignSubscriber.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index c2ed7856602..4eb7f8c107f 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -100,6 +100,8 @@ public static function getSubscribedEvents() EmailEvents::EMAIL_ON_OPEN => ['onEmailOpen', 0], EmailEvents::ON_CAMPAIGN_BATCH_ACTION => [ ['onCampaignTriggerActionSendEmailToContact', 0], + ], + EmailEvents::ON_CAMPAIGN_TRIGGER_ACTION => [ ['onCampaignTriggerActionSendEmailToUser', 1], ], EmailEvents::ON_CAMPAIGN_TRIGGER_DECISION => ['onCampaignTriggerDecision', 0], From b2dd61058b0b49d81e3cf1361cfb14a634204637 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 26 Apr 2018 17:20:49 -0500 Subject: [PATCH 454/778] Removed unused trait --- .../Executioner/ContactRangeTrait.php | 17 ----------------- .../Executioner/InactiveExecutioner.php | 2 -- .../Executioner/KickoffExecutioner.php | 2 -- .../Executioner/ScheduledExecutioner.php | 2 -- 4 files changed, 23 deletions(-) delete mode 100644 app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php diff --git a/app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php b/app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php deleted file mode 100644 index 9c20235806a..00000000000 --- a/app/bundles/CampaignBundle/Executioner/ContactRangeTrait.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Wed, 2 May 2018 00:47:31 -0500 Subject: [PATCH 455/778] Renamed exceptions --- .../Executioner/ContactFinder/InactiveContacts.php | 8 ++++---- .../Executioner/ContactFinder/KickoffContacts.php | 8 ++++---- .../{NoEventsFound.php => NoContactsFoundException.php} | 2 +- .../{NoContactsFound.php => NoEventsFoundException.php} | 2 +- .../ChannelBundle/EventListener/CampaignSubscriber.php | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) rename app/bundles/CampaignBundle/Executioner/Exception/{NoEventsFound.php => NoContactsFoundException.php} (84%) rename app/bundles/CampaignBundle/Executioner/Exception/{NoContactsFound.php => NoEventsFoundException.php} (85%) diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php index e19beb7726c..b1713f03773 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php @@ -16,7 +16,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadRepository as CampaignLeadRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\LoggerInterface; @@ -71,7 +71,7 @@ public function __construct(LeadRepository $leadRepository, CampaignRepository $ * * @return ArrayCollection * - * @throws NoContactsFound + * @throws NoContactsFoundException */ public function getContacts($campaignId, Event $decisionEvent, $startAtContactId, ContactLimiter $limiter) { @@ -87,7 +87,7 @@ public function getContacts($campaignId, Event $decisionEvent, $startAtContactId if (empty($this->campaignMemberDatesAdded)) { // No new contacts found in the campaign - throw new NoContactsFound(); + throw new NoContactsFoundException(); } $campaignContacts = array_keys($this->campaignMemberDatesAdded); @@ -100,7 +100,7 @@ public function getContacts($campaignId, Event $decisionEvent, $startAtContactId // Just a precaution in case non-existent contacts are lingering in the campaign leads table $this->logger->debug('CAMPAIGN: No contact entities found.'); - throw new NoContactsFound(); + throw new NoContactsFoundException(); } return $contacts; diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php index c805a4e7f08..91e6a15cec5 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php @@ -14,7 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\CampaignRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\LoggerInterface; @@ -56,7 +56,7 @@ public function __construct(LeadRepository $leadRepository, CampaignRepository $ * * @return ArrayCollection * - * @throws NoContactsFound + * @throws NoContactsFoundException */ public function getContacts($campaignId, ContactLimiter $limiter) { @@ -66,7 +66,7 @@ public function getContacts($campaignId, ContactLimiter $limiter) if (empty($campaignContacts)) { // No new contacts found in the campaign - throw new NoContactsFound(); + throw new NoContactsFoundException(); } $this->logger->debug('CAMPAIGN: Processing the following contacts: '.implode(', ', $campaignContacts)); @@ -78,7 +78,7 @@ public function getContacts($campaignId, ContactLimiter $limiter) // Just a precaution in case non-existent contacts are lingering in the campaign leads table $this->logger->debug('CAMPAIGN: No contact entities found.'); - throw new NoContactsFound(); + throw new NoContactsFoundException(); } return $contacts; diff --git a/app/bundles/CampaignBundle/Executioner/Exception/NoEventsFound.php b/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFoundException.php similarity index 84% rename from app/bundles/CampaignBundle/Executioner/Exception/NoEventsFound.php rename to app/bundles/CampaignBundle/Executioner/Exception/NoContactsFoundException.php index 4bc9fd8d116..51499922c64 100644 --- a/app/bundles/CampaignBundle/Executioner/Exception/NoEventsFound.php +++ b/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFoundException.php @@ -11,6 +11,6 @@ namespace Mautic\CampaignBundle\Executioner\Exception; -class NoEventsFound extends \Exception +class NoContactsFoundException extends \Exception { } diff --git a/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php b/app/bundles/CampaignBundle/Executioner/Exception/NoEventsFoundException.php similarity index 85% rename from app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php rename to app/bundles/CampaignBundle/Executioner/Exception/NoEventsFoundException.php index 58f974bdcbb..863d53f1bab 100644 --- a/app/bundles/CampaignBundle/Executioner/Exception/NoContactsFound.php +++ b/app/bundles/CampaignBundle/Executioner/Exception/NoEventsFoundException.php @@ -11,6 +11,6 @@ namespace Mautic\CampaignBundle\Executioner\Exception; -class NoContactsFound extends \Exception +class NoEventsFoundException extends \Exception { } diff --git a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php index 42866d9d5f5..97a7cd4219b 100644 --- a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php @@ -20,7 +20,7 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; -use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\ChannelBundle\ChannelEvents; use Mautic\ChannelBundle\Model\MessageModel; use Mautic\ChannelBundle\PreferenceBuilder\PreferenceBuilder; @@ -301,7 +301,7 @@ private function recordChannelMetadata(PendingEvent $pendingEvent, $channel) if ($metadata = $channelLog->getMetadata()) { $log->appendToMetadata([$channel => $metadata]); } - } catch (NoContactsFound $exception) { + } catch (NoContactsFoundException $exception) { continue; } } From ae5feeb41bd20a1d1a51cf76cdf4b5e0fcd73bb1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:48:16 -0500 Subject: [PATCH 456/778] Converted to using ContactTracker --- app/bundles/CampaignBundle/Config/config.php | 1 + .../Executioner/DecisionExecutioner.php | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 92e533b3aa3..4b7032d15c8 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -410,6 +410,7 @@ 'mautic.campaign.executioner.decision', 'mautic.campaign.event_collector', 'mautic.campaign.scheduler', + 'mautic.tracker.contact', ], ], 'mautic.campaign.executioner.inactive' => [ diff --git a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php index f59012082a6..992fa4f538d 100644 --- a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php @@ -23,6 +23,7 @@ use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; +use Mautic\LeadBundle\Tracker\ContactTracker; use Psr\Log\LoggerInterface; class DecisionExecutioner @@ -72,6 +73,11 @@ class DecisionExecutioner */ private $scheduler; + /** + * @var ContactTracker + */ + private $contactTracker; + /** * @var Responses */ @@ -87,6 +93,7 @@ class DecisionExecutioner * @param Decision $decisionExecutioner * @param EventCollector $collector * @param EventScheduler $scheduler + * @param ContactTracker $contactTracker */ public function __construct( LoggerInterface $logger, @@ -95,7 +102,8 @@ public function __construct( EventExecutioner $executioner, Decision $decisionExecutioner, EventCollector $collector, - EventScheduler $scheduler + EventScheduler $scheduler, + ContactTracker $contactTracker ) { $this->logger = $logger; $this->leadModel = $leadModel; @@ -104,6 +112,7 @@ public function __construct( $this->decisionExecutioner = $decisionExecutioner; $this->collector = $collector; $this->scheduler = $scheduler; + $this->contactTracker = $contactTracker; } /** @@ -233,7 +242,7 @@ private function evaluateDecisionForContact(Event $event, $passthrough = null, $ */ private function fetchCurrentContact() { - $this->contact = $this->leadModel->getCurrentLead(); + $this->contact = $this->contactTracker->getContact(); if (!$this->contact instanceof Lead || !$this->contact->getId()) { throw new CampaignNotExecutableException('Unidentifiable contact'); } From 5985cec12f636791c2bfc4117e08ac9cbcf351a7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:49:19 -0500 Subject: [PATCH 457/778] Prevented a loop when processing inactive events --- .../Executioner/Helper/InactiveHelper.php | 62 +++++++++----- .../Executioner/InactiveExecutioner.php | 84 ++++++++++++++----- 2 files changed, 104 insertions(+), 42 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index 720b7d1fa62..86c509345ea 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -46,6 +46,11 @@ class InactiveHelper */ private $logger; + /** + * @var \DateTime + */ + private $earliestInactiveDate; + /** * InactiveHelper constructor. * @@ -88,26 +93,20 @@ public function removeDecisionsWithoutNegativeChildren(ArrayCollection $decision /** * @param \DateTime $now * @param ArrayCollection $contacts - * @param $eventId + * @param int $lastActiveEventId * @param ArrayCollection $negativeChildren * - * @return \DateTime - * * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ - public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollection $contacts, $eventId, ArrayCollection $negativeChildren) - { - $contactIds = $contacts->getKeys(); - - // If there is a parent ID, get last active dates based on when that event was executed for the given contact - // Otherwise, use when the contact was added to the campaign for comparison - if ($eventId) { - $lastActiveDates = $this->eventLogRepository->getDatesExecuted($eventId, $contactIds); - } else { - $lastActiveDates = $this->inactiveContacts->getDatesAdded(); - } - - $earliestInactiveDate = $now; + public function removeContactsThatAreNotApplicable( + \DateTime $now, + ArrayCollection $contacts, + $lastActiveEventId, + ArrayCollection $negativeChildren + ) { + $contactIds = $contacts->getKeys(); + $lastActiveDates = $this->getLastActiveDates($lastActiveEventId, $contactIds); + $this->earliestInactiveDate = $now; /* @var Event $event */ foreach ($contactIds as $contactId) { @@ -115,7 +114,7 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti // This contact does not have a last active date so likely the event is scheduled $contacts->remove($contactId); - $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' does not have a last active date'); + $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' does not have a last active date ('.$lastActiveEventId.')'); continue; } @@ -127,8 +126,8 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti $lastActiveDates[$contactId]->format('Y-m-d H:i:s T') ); - if ($earliestInactiveDate < $earliestContactInactiveDate) { - $earliestInactiveDate = $earliestContactInactiveDate; + if ($this->earliestInactiveDate < $earliestContactInactiveDate) { + $this->earliestInactiveDate = $earliestContactInactiveDate; } // If any are found to be inactive, we process or schedule all the events associated with the inactive path of a decision @@ -141,8 +140,14 @@ public function removeContactsThatAreNotApplicable(\DateTime $now, ArrayCollecti $this->logger->debug('CAMPAIGN: Contact ID# '.$contactId.' has not been active'); } + } - return $earliestInactiveDate; + /** + * @return \DateTime + */ + public function getEarliestInactiveDateTime() + { + return $this->earliestInactiveDate; } /** @@ -182,4 +187,21 @@ public function getEarliestInactiveDate(ArrayCollection $negativeChildren, \Date return $earliestDate; } + + /** + * @param $lastActiveEventId + * @param array $contactIds + * + * @return array|ArrayCollection + */ + private function getLastActiveDates($lastActiveEventId, array $contactIds) + { + // If there is a parent ID, get last active dates based on when that event was executed for the given contact + // Otherwise, use when the contact was added to the campaign for comparison + if ($lastActiveEventId) { + return $this->eventLogRepository->getDatesExecuted($lastActiveEventId, $contactIds); + } + + return $this->inactiveContacts->getDatesAdded(); + } } diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index b95ece6f687..c251622f356 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -16,8 +16,8 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; -use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; +use Mautic\CampaignBundle\Executioner\Exception\NoEventsFoundException; use Mautic\CampaignBundle\Executioner\Helper\InactiveHelper; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; @@ -90,6 +90,11 @@ class InactiveExecutioner implements ExecutionerInterface */ private $helper; + /** + * @var int + */ + private $startAtContactId = 0; + /** * InactiveExecutioner constructor. * @@ -140,9 +145,9 @@ public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInter $this->prepareForExecution(); $this->executeEvents(); - } catch (NoContactsFound $exception) { + } catch (NoContactsFoundException $exception) { $this->logger->debug('CAMPAIGN: No more contacts to process'); - } catch (NoEventsFound $exception) { + } catch (NoEventsFoundException $exception) { $this->logger->debug('CAMPAIGN: No events to process'); } finally { if ($this->progressBar) { @@ -174,18 +179,13 @@ public function validate($decisionId, ContactLimiter $limiter, OutputInterface $ try { $this->decisions = $this->helper->getCollectionByDecisionId($decisionId); - if ($this->decisions->count()) { - $this->campaign = $this->decisions->first()->getCampaign(); - if (!$this->campaign->isPublished()) { - throw new NoEventsFound(); - } - } + $this->checkCampaignIsPublished(); $this->prepareForExecution(); $this->executeEvents(); - } catch (NoContactsFound $exception) { + } catch (NoContactsFoundException $exception) { $this->logger->debug('CAMPAIGN: No more contacts to process'); - } catch (NoEventsFound $exception) { + } catch (NoEventsFoundException $exception) { $this->logger->debug('CAMPAIGN: No events to process'); } finally { if ($this->progressBar) { @@ -198,8 +198,23 @@ public function validate($decisionId, ContactLimiter $limiter, OutputInterface $ } /** - * @throws NoContactsFound - * @throws NoEventsFound + * @throws NoEventsFoundException + */ + private function checkCampaignIsPublished() + { + if (!$this->decisions->count()) { + throw new NoEventsFoundException(); + } + + $this->campaign = $this->decisions->first()->getCampaign(); + if (!$this->campaign->isPublished()) { + throw new NoEventsFoundException(); + } + } + + /** + * @throws NoContactsFoundException + * @throws NoEventsFoundException */ private function prepareForExecution() { @@ -209,7 +224,7 @@ private function prepareForExecution() $totalDecisions = $this->decisions->count(); if (!$totalDecisions) { - throw new NoEventsFound(); + throw new NoEventsFoundException(); } $totalContacts = $this->inactiveContacts->getContactCount($this->campaign->getId(), $this->decisions->getKeys(), $this->limiter); @@ -225,7 +240,7 @@ private function prepareForExecution() ); if (!$totalContacts) { - throw new NoContactsFound(); + throw new NoContactsFoundException(); } // Approximate total count because the query to fetch contacts will filter out those that have not arrived to this point in the campaign yet @@ -237,7 +252,7 @@ private function prepareForExecution() * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException - * @throws NoContactsFound + * @throws NoContactsFoundException * @throws Scheduler\Exception\NotSchedulableException */ private function executeEvents() @@ -253,21 +268,22 @@ private function executeEvents() // Because timing may not be appropriate, the starting row of the query may or may not change. // So use the max contact ID to filter/sort results. - $startAtContactId = $this->limiter->getMinContactId() ?: 0; + $this->startAtContactId = $this->limiter->getMinContactId() ?: 0; // Ge the first batch of contacts - $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); + $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $this->startAtContactId, $this->limiter); // Loop over all contacts till we've processed all those applicable for this decision while ($contacts->count()) { // Get the max contact ID before any are removed - $startAtContactId = max($contacts->getKeys()); + $startAtContactId = $this->getStartingContactIdForNextBatch($contacts); $this->progressBar->advance($contacts->count()); $this->counter->advanceEvaluated($contacts->count()); - $inactiveEvents = $decisionEvent->getNegativeChildren(); - $earliestLastActiveDateTime = $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); + $inactiveEvents = $decisionEvent->getNegativeChildren(); + $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); + $earliestLastActiveDateTime = $this->helper->getEarliestInactiveDateTime(); $this->logger->debug( 'CAMPAIGN: ('.$decisionEvent->getId().') Earliest date for inactivity for this batch of contacts is '. @@ -289,9 +305,33 @@ private function executeEvents() break; } + $this->logger->debug('CAMPAIGN: Fetching the next batch of inactive contacts after contact ID '.$startAtContactId); + // Get the next batch, starting with the max contact ID $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); } } } + + /** + * @param ArrayCollection $contacts + * + * @return mixed + * + * @throws NoContactsFoundException + */ + private function getStartingContactIdForNextBatch(ArrayCollection $contacts) + { + $maxId = max($contacts->getKeys()); + + // Prevent a never ending loop if the contact ID never changes due to the last batch of contacts + // getting removed because previously executed events are scheduled + if ($this->startAtContactId === $maxId) { + throw new NoContactsFoundException(); + } + + $this->startAtContactId = $maxId; + + return $maxId; + } } From e50c5990a9cd8b1c7ed4ab3a7ab41c1ebbc768bb Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:50:06 -0500 Subject: [PATCH 458/778] Fixed issue where event name field was not set as system property before the extra field array was hydrated --- .../EventCollector/Accessor/Event/AbstractEventAccessor.php | 3 +++ .../EventCollector/Accessor/Event/ActionAccessor.php | 4 ++-- .../EventCollector/Accessor/Event/ConditionAccessor.php | 4 ++-- .../EventCollector/Accessor/Event/DecisionAccessor.php | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php index 71303186605..7f2dd58df31 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php @@ -149,6 +149,9 @@ protected function getProperty($property, $default = null) return (isset($this->config[$property])) ? $this->config[$property] : $default; } + /** + * Calculate the difference in systemProperties and what was fed to the class. + */ private function filterExtraProperties() { $this->extraProperties = array_diff_key($this->config, array_flip($this->systemProperties)); diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php index 17bcc4a2a7b..c134660d885 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ActionAccessor.php @@ -20,9 +20,9 @@ class ActionAccessor extends AbstractEventAccessor */ public function __construct(array $config) { - parent::__construct($config); - $this->systemProperties[] = 'batchEventName'; + + parent::__construct($config); } /** diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php index dd3d938aace..2476bd2dc45 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php @@ -18,9 +18,9 @@ class ConditionAccessor extends AbstractEventAccessor { public function __construct(array $config) { - parent::__construct($config); - $this->systemProperties[] = 'eventName'; + + parent::__construct($config); } /** diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php index d5b3b421a61..1e3a3e254fb 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php @@ -15,9 +15,9 @@ class DecisionAccessor extends AbstractEventAccessor { public function __construct(array $config) { - parent::__construct($config); - $this->systemProperties[] = 'eventName'; + + parent::__construct($config); } /** From 984c1d4bdd6f0fe698f6c987a202d712032d9cfa Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:50:44 -0500 Subject: [PATCH 459/778] Fixed BC case for way old campaign event settings --- .../EventCollector/Builder/ConnectionBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php b/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php index bd96f1899c3..d901e6b9a93 100644 --- a/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php +++ b/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php @@ -105,12 +105,12 @@ private static function addRestriction($key, $restrictionType, array $restrictio private static function addDeprecatedAnchorRestrictions($eventType, $key, array $event) { switch ($eventType) { - case Event::TYPE_ACTION: + case Event::TYPE_DECISION: if (isset($event['associatedActions'])) { self::$connectionRestrictions[$key]['target']['action'] += $event['associatedActions']; } break; - case Event::TYPE_DECISION: + case Event::TYPE_ACTION: if (isset($event['associatedDecisions'])) { self::$connectionRestrictions[$key]['source']['decision'] += $event['associatedDecisions']; } From df04e26a7d9a3d7056c38a3953c6f61f380c9943 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:52:12 -0500 Subject: [PATCH 460/778] Code cleanup --- .../Event/AbstractLogCollectionEvent.php | 6 ++--- .../Dispatcher/EventDispatcher.php | 23 ++++++++++++------- .../Dispatcher/LegacyEventDispatcher.php | 10 ++------ .../Executioner/KickoffExecutioner.php | 14 +++++------ .../Executioner/Result/Responses.php | 8 +++++++ .../Executioner/ScheduledExecutioner.php | 9 ++++---- 6 files changed, 39 insertions(+), 31 deletions(-) diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index 322f668fb44..fcc81ae4197 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -15,7 +15,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; -use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\LeadBundle\Entity\Lead; abstract class AbstractLogCollectionEvent extends \Symfony\Component\EventDispatcher\Event @@ -103,12 +103,12 @@ public function getContactIds() * * @return mixed|null * - * @throws NoContactsFound + * @throws NoContactsFoundException */ public function findLogByContactId($id) { if (!isset($this->logContactXref[$id])) { - throw new NoContactsFound(); + throw new NoContactsFoundException(); } return $this->logs->get($this->logContactXref[$id]); diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index a5673b1b90f..4159d4427c9 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -28,6 +28,7 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException; +use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -102,8 +103,14 @@ public function dispatchActionEvent(ActionAccessor $config, Event $event, ArrayC $failed = $pendingEvent->getFailures(); $this->validateProcessedLogs($logs, $success, $failed); - $this->dispatchExecutedEvent($config, $event, $success); - $this->dispatchedFailedEvent($config, $failed); + + if ($success) { + $this->dispatchExecutedEvent($config, $event, $success); + } + + if ($failed) { + $this->dispatchedFailedEvent($config, $failed); + } // Dispatch legacy ON_EVENT_EXECUTION event for BC $this->legacyDispatcher->dispatchExecutionEvents($config, $success, $failed); @@ -135,11 +142,11 @@ public function dispatchDecisionEvent(DecisionAccessor $config, LeadEventLog $lo } /** - * @param $config - * @param $logs - * @param $evaluatedContacts + * @param DecisionAccessor $config + * @param ArrayCollection $logs + * @param EvaluatedContacts $evaluatedContacts */ - public function dispatchDecisionResultsEvent($config, $logs, $evaluatedContacts) + public function dispatchDecisionResultsEvent(DecisionAccessor $config, ArrayCollection $logs, EvaluatedContacts $evaluatedContacts) { $this->dispatcher->dispatch( CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, @@ -167,7 +174,7 @@ public function dispatchConditionEvent(ConditionAccessor $config, LeadEventLog $ * @param Event $event * @param ArrayCollection $logs */ - public function dispatchExecutedEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) + private function dispatchExecutedEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) { foreach ($logs as $log) { $this->dispatcher->dispatch( @@ -186,7 +193,7 @@ public function dispatchExecutedEvent(AbstractEventAccessor $config, Event $even * @param AbstractEventAccessor $config * @param ArrayCollection $logs */ - public function dispatchedFailedEvent(AbstractEventAccessor $config, ArrayCollection $logs) + private function dispatchedFailedEvent(AbstractEventAccessor $config, ArrayCollection $logs) { foreach ($logs as $log) { $this->logger->debug( diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 50281b526b5..0315c769a3f 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -103,7 +103,7 @@ public function dispatchCustomEvent( $settings = $config->getConfig(); if (!isset($settings['eventName']) && !isset($settings['callback'])) { - // Bad plugin + // Bad plugin but only fail if the new event didn't already process the log if (!$wasBatchProcessed) { $pendingEvent->failAll('Invalid event configuration'); } @@ -126,6 +126,7 @@ public function dispatchCustomEvent( $result = $this->dispatchCallback($settings, $log); } + // If the new batch event was handled, the $log was already processed so only process legacy logs if false if (!$wasBatchProcessed) { $this->dispatchExecutionEvent($config, $log, $result); @@ -362,14 +363,7 @@ private function processFailedLog($result, LeadEventLog $log, PendingEvent $pend 'CAMPAIGN: '.ucfirst($log->getEvent()->getEventType()).' ID# '.$log->getEvent()->getId().' for contact ID# '.$log->getLead()->getId() ); - if (is_array($result)) { - $log->setMetadata($result); - } - $metadata = $log->getMetadata(); - if (is_array($result)) { - $metadata = array_merge($metadata, $result); - } $reason = null; if (isset($metadata['errors'])) { diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index 1699a051f8e..6b693e9aa25 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -16,8 +16,8 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\Exception\NoContactsFound; -use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; +use Mautic\CampaignBundle\Executioner\Exception\NoEventsFoundException; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException; @@ -135,9 +135,9 @@ public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInter try { $this->prepareForExecution(); $this->executeOrScheduleEvent(); - } catch (NoContactsFound $exception) { + } catch (NoContactsFoundException $exception) { $this->logger->debug('CAMPAIGN: No more contacts to process'); - } catch (NoEventsFound $exception) { + } catch (NoEventsFoundException $exception) { $this->logger->debug('CAMPAIGN: No events to process'); } finally { if ($this->progressBar) { @@ -150,7 +150,7 @@ public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInter } /** - * @throws NoEventsFound + * @throws NoEventsFoundException */ private function prepareForExecution() { @@ -177,7 +177,7 @@ private function prepareForExecution() ); if (!$totalKickoffEvents) { - throw new NoEventsFound(); + throw new NoEventsFoundException(); } $this->progressBar = ProgressBarHelper::init($this->output, $totalKickoffEvents); @@ -188,7 +188,7 @@ private function prepareForExecution() * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException * @throws Exception\CannotProcessEventException - * @throws NoContactsFound + * @throws NoContactsFoundException * @throws NotSchedulableException */ private function executeOrScheduleEvent() diff --git a/app/bundles/CampaignBundle/Executioner/Result/Responses.php b/app/bundles/CampaignBundle/Executioner/Result/Responses.php index bf91cd573f0..ed02128f691 100644 --- a/app/bundles/CampaignBundle/Executioner/Result/Responses.php +++ b/app/bundles/CampaignBundle/Executioner/Result/Responses.php @@ -90,6 +90,14 @@ public function getConditionResponses($type = null) return $this->conditionResponses; } + /** + * @return int + */ + public function containsResponses() + { + return count($this->actionResponses) + count($this->conditionResponses); + } + /** * @deprecated 2.13.0 to be removed in 3.0; used for BC EventModel::triggerEvent() * diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index f6d1b661a95..c3bb5608ae5 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -13,12 +13,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; -use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; -use Mautic\CampaignBundle\Executioner\Exception\NoEventsFound; +use Mautic\CampaignBundle\Executioner\Exception\NoEventsFoundException; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Helper\ProgressBarHelper; @@ -146,7 +145,7 @@ public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInter try { $this->prepareForExecution(); $this->executeOrRecheduleEvent(); - } catch (NoEventsFound $exception) { + } catch (NoEventsFoundException $exception) { $this->logger->debug('CAMPAIGN: No events to process'); } finally { if ($this->progressBar) { @@ -222,7 +221,7 @@ public function executeByIds(array $logIds, OutputInterface $output = null) } /** - * @throws NoEventsFound + * @throws NoEventsFoundException */ private function prepareForExecution() { @@ -246,7 +245,7 @@ private function prepareForExecution() ); if (!$totalScheduledCount) { - throw new NoEventsFound(); + throw new NoEventsFoundException(); } $this->progressBar = ProgressBarHelper::init($this->output, $totalScheduledCount); From 8080f59b6344477e50a9619ea2b62015579ba05e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:53:05 -0500 Subject: [PATCH 461/778] Tests, tests and more tests --- .../Tests/Command/AbstractCampaignCommand.php | 111 +++++ .../Tests/Command/ExecuteEventCommandTest.php | 72 +++ .../Command/TriggerCampaignCommandTest.php | 393 +++++++++++---- .../Command/ValidateEventCommandTest.php | 63 +++ .../Accessor/Event/ActionAccessorTest.php | 38 ++ .../Accessor/Event/ConditionAccessorTest.php | 31 ++ .../Accessor/Event/DecisionAccessorTest.php | 31 ++ .../Accessor/EventAccessorTest.php | 91 ++++ .../Builder/ConnectionBuilderTest.php | 124 +++++ .../Builder/EventBuilderTest.php | 80 +++ .../ContactFinder/InactiveContactsTest.php | 124 +++++ .../ContactFinder/KickoffContactsTest.php | 104 ++++ .../ContactFinder/ScheduledContactsTest.php | 94 ++++ .../Executioner/DecisionExecutionerTest.php | 322 ++++++++++++ .../Dispatcher/EventDispatcherTest.php | 374 ++++++++++++++ .../Dispatcher/LegacyEventDispatcherTest.php | 465 ++++++++++++++++++ .../Executioner/InactiveExecutionerTest.php | 225 +++++++++ .../Executioner/KickoffExecutionerTest.php | 146 ++++++ .../Executioner/ScheduledExecutionerTest.php | 212 ++++++++ .../ChannelPreferencesTest.php | 59 +++ .../PreferenceBuilderTest.php | 167 +++++++ 21 files changed, 3239 insertions(+), 87 deletions(-) create mode 100644 app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php create mode 100644 app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php create mode 100644 app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php create mode 100644 app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php create mode 100644 app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php create mode 100644 app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php create mode 100644 app/bundles/CampaignBundle/Tests/EventCollector/Builder/ConnectionBuilderTest.php create mode 100644 app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php create mode 100644 app/bundles/ChannelBundle/Tests/PreferenceBuilder/ChannelPreferencesTest.php create mode 100644 app/bundles/ChannelBundle/Tests/PreferenceBuilder/PreferenceBuilderTest.php diff --git a/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php b/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php new file mode 100644 index 00000000000..426c1d7faf8 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php @@ -0,0 +1,111 @@ +defaultClientServer = $this->clientServer; + $this->clientServer = []; + + parent::setUp(); + + $this->db = $this->container->get('doctrine.dbal.default_connection'); + $this->prefix = $this->container->getParameter('mautic.db_table_prefix'); + + // Populate contacts + $this->installDatabaseFixtures([dirname(__DIR__).'/../../LeadBundle/DataFixtures/ORM/LoadLeadData.php']); + + // Campaigns are so complex that we are going to load a SQL file rather than build with entities + $sql = file_get_contents(__DIR__.'/campaign_schema.sql'); + + // Update table prefix + $sql = str_replace('#__', $this->container->getParameter('mautic.db_table_prefix'), $sql); + + // Schedule event + date_default_timezone_set('UTC'); + $this->eventDate = new \DateTime(); + $this->eventDate->modify('+15 seconds'); + $sql = str_replace('{SEND_EMAIL_1_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql); + + $this->eventDate->modify('+15 seconds'); + $sql = str_replace('{CONDITION_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql); + + // Update the schema + $tmpFile = $this->container->getParameter('kernel.cache_dir').'/campaign_schema.sql'; + file_put_contents($tmpFile, $sql); + $this->applySqlFromFile($tmpFile); + } + + public function tearDown() + { + parent::tearDown(); + + $this->clientServer = $this->defaultClientServer; + } + + /** + * @param array $ids + * + * @return array + */ + protected function getCampaignEventLogs(array $ids) + { + $logs = $this->db->createQueryBuilder() + ->select('l.email, l.country, event.name, event.event_type, event.type, log.*') + ->from($this->prefix.'campaign_lead_event_log', 'log') + ->join('log', $this->prefix.'campaign_events', 'event', 'event.id = log.event_id') + ->join('log', $this->prefix.'leads', 'l', 'l.id = log.lead_id') + ->where('log.campaign_id = 1') + ->andWhere('log.event_id IN ('.implode(',', $ids).')') + ->execute() + ->fetchAll(); + + $byEvent = []; + foreach ($ids as $id) { + $byEvent[$id] = []; + } + + foreach ($logs as $log) { + $byEvent[$log['event_id']][] = $log; + } + + return $byEvent; + } +} diff --git a/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php new file mode 100644 index 00000000000..48b5ef21be1 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php @@ -0,0 +1,72 @@ +runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); + + // There should be two events scheduled + $byEvent = $this->getCampaignEventLogs([2]); + $this->assertCount(3, $byEvent[2]); + + $logIds = []; + foreach ($byEvent[2] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Event is not scheduled for lead ID '.$log['lead_id']); + } + + $logIds[] = $log['id']; + } + + $this->runCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => implode(',', $logIds)]); + + // There should still be trhee events scheduled + $byEvent = $this->getCampaignEventLogs([2]); + $this->assertCount(3, $byEvent[2]); + + foreach ($byEvent[2] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Event is not scheduled for lead ID '.$log['lead_id']); + } + } + + // Pop off the last so we can test that only the two given are executed + $lastId = array_pop($logIds); + + // Wait 15 seconds to go past scheduled time + sleep(15); + + $this->runCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => implode(',', $logIds)]); + + // The events should have executed + $byEvent = $this->getCampaignEventLogs([2]); + $this->assertCount(3, $byEvent[2]); + + foreach ($byEvent[2] as $log) { + // Lasta + if ($log['id'] === $lastId) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Event is not scheduled when it should be for lead ID '.$log['lead_id']); + } + + continue; + } + + if (1 === (int) $log['is_scheduled']) { + $this->fail('Event is still scheduled for lead ID '.$log['lead_id']); + } + } + } +} diff --git a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php index 0bcb8b6c5f4..596e811ecd7 100644 --- a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php @@ -11,80 +11,12 @@ namespace Mautic\CampaignBundle\Tests\Command; -use Doctrine\DBAL\Connection; -use Mautic\CoreBundle\Test\MauticMysqlTestCase; - -class TriggerCampaignCommandTest extends MauticMysqlTestCase +class TriggerCampaignCommandTest extends AbstractCampaignCommand { - /** - * @var array - */ - private $defaultClientServer = []; - - /** - * @var Connection - */ - private $db; - - /** - * @var - */ - private $prefix; - - /** - * @var \DateTime - */ - private $eventDate; - - /** - * @throws \Exception - */ - public function setUp() - { - // Everything needs to happen anonymously - $this->defaultClientServer = $this->clientServer; - $this->clientServer = []; - - parent::setUp(); - - $this->db = $this->container->get('doctrine.dbal.default_connection'); - $this->prefix = $this->container->getParameter('mautic.db_table_prefix'); - - // Populate contacts - $this->installDatabaseFixtures([dirname(__DIR__).'/../../LeadBundle/DataFixtures/ORM/LoadLeadData.php']); - - // Campaigns are so complex that we are going to load a SQL file rather than build with entities - $sql = file_get_contents(__DIR__.'/campaign_schema.sql'); - - // Update table prefix - $sql = str_replace('#__', $this->container->getParameter('mautic.db_table_prefix'), $sql); - - // Schedule event - date_default_timezone_set('UTC'); - $this->eventDate = new \DateTime(); - $this->eventDate->modify('+15 seconds'); - $sql = str_replace('{SEND_EMAIL_1_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql); - - $this->eventDate->modify('+15 seconds'); - $sql = str_replace('{CONDITION_TIMESTAMP}', $this->eventDate->format('Y-m-d H:i:s'), $sql); - - // Update the schema - $tmpFile = $this->container->getParameter('kernel.cache_dir').'/campaign_schema.sql'; - file_put_contents($tmpFile, $sql); - $this->applySqlFromFile($tmpFile); - } - - public function tearDown() - { - parent::tearDown(); - - $this->clientServer = $this->defaultClientServer; - } - /** * @throws \Exception */ - public function testCampaignExecution() + public function testCampaignExecutionForAll() { $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); @@ -243,32 +175,319 @@ public function testCampaignExecution() } /** - * @param array $ids - * - * @return array + * @throws \Exception */ - private function getCampaignEventLogs(array $ids) + public function testCampaignExecutionForOne() { - $logs = $this->db->createQueryBuilder() - ->select('l.email, l.country, event.name, event.event_type, event.type, log.*') - ->from($this->prefix.'campaign_lead_event_log', 'log') - ->join('log', $this->prefix.'campaign_events', 'event', 'event.id = log.event_id') - ->join('log', $this->prefix.'leads', 'l', 'l.id = log.lead_id') - ->where('log.campaign_id = 1') - ->andWhere('log.event_id IN ('.implode(',', $ids).')') + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); + + // Let's analyze + $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13]); + $tags = $this->getTagCounts(); + + // Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1 + $this->assertCount(1, $byEvent[1]); + $this->assertCount(1, $byEvent[2]); + + // Sending Campaign Test Email 1 should be scheduled + foreach ($byEvent[2] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']); + } + } + + // Everyone should have had the Is US condition processed + $this->assertCount(1, $byEvent[11]); + + // 1 should have been send down the non-action path (red) of the condition + $nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]); + $this->assertEquals(1, $nonActionCount); + + // 0 contacts are from the US and should be labeled with US:Action + $this->assertCount(0, $byEvent[12]); + $this->assertTrue(empty($tags['US:Action'])); + + // The rest (1) contacts are not from the US and should be labeled with NonUS:Action + $this->assertCount(1, $byEvent[13]); + $this->assertEquals(1, $tags['NonUS:Action']); + + // No emails should be sent till after 5 seconds and the command is ran again + $stats = $this->db->createQueryBuilder() + ->select('*') + ->from($this->prefix.'email_stats', 'stat') + ->where('stat.lead_id = 1') + ->execute() + ->fetchAll(); + $this->assertCount(0, $stats); + + // Wait 15 seconds then execute the campaign again to send scheduled events + sleep(15); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); + + // Send email 1 should no longer be scheduled + $byEvent = $this->getCampaignEventLogs([2, 4]); + $this->assertCount(1, $byEvent[2]); + foreach ($byEvent[2] as $log) { + if (1 === (int) $log['is_scheduled']) { + $this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']); + } + } + + // The non-action events attached to the decision should have no logs entries + $this->assertCount(0, $byEvent[4]); + + // Check that the emails actually sent + $stats = $this->db->createQueryBuilder() + ->select('*') + ->from($this->prefix.'email_stats', 'stat') + ->where('stat.lead_id = 1') ->execute() ->fetchAll(); + $this->assertCount(1, $stats); - $byEvent = []; - foreach ($ids as $id) { - $byEvent[$id] = []; + // Now let's simulate email opens + foreach ($stats as $stat) { + $this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif'); + $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent())); } - foreach ($logs as $log) { - $byEvent[$log['event_id']][] = $log; + $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]); + + // The non-action events attached to the decision should have no logs entries + $this->assertCount(0, $byEvent[4]); + $this->assertCount(0, $byEvent[5]); + $this->assertCount(0, $byEvent[14]); + $this->assertCount(0, $byEvent[15]); + + // The 1 should now have open email decisions logged and the next email sent + $this->assertCount(1, $byEvent[3]); + $this->assertCount(1, $byEvent[10]); + + // Wait 15 seconds to go beyond the inaction timeframe + sleep(15); + + // Execute the command again to trigger inaction related events + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); + + // Now we should have 1 email open decisions + $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); + $this->assertCount(1, $byEvent[3]); + + // 0 should be marked as non_action_path_taken + $nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]); + $this->assertEquals(0, $nonActionCount); + + // There should be no inactive events + $this->assertCount(0, $byEvent[4]); + $this->assertCount(0, $byEvent[5]); + $this->assertCount(0, $byEvent[14]); + $this->assertCount(0, $byEvent[15]); + + $utcTimezone = new \DateTimeZone('UTC'); + foreach ($byEvent[14] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']); + } + + $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); + $diff = $this->eventDate->diff($scheduledFor); + + if (2 !== $diff->i) { + $this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)'); + } } - return $byEvent; + foreach ($byEvent[15] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']); + } + + $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); + $diff = $this->eventDate->diff($scheduledFor); + + if (6 !== $diff->i) { + $this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)'); + } + } + $byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]); + $tags = $this->getTagCounts(); + + // Of those that did not open the email, 0 should be tagged US:NotOpen + $this->assertCount(0, $byEvent[6]); + $this->assertTrue(empty($tags['US:NotOpen'])); + + // And 0 should be tagged NonUS:NotOpen + $this->assertCount(0, $byEvent[7]); + $this->assertTrue(empty($tags['NonUS:NotOpen'])); + + // And 0 should be tagged UK:NotOpen + $this->assertCount(0, $byEvent[8]); + $this->assertTrue(empty($tags['UK:NotOpen'])); + + // And 0 should be tagged NonUK:NotOpen + $this->assertCount(0, $byEvent[9]); + $this->assertTrue(empty($tags['NonUK:NotOpen'])); + + // No one should be tagged as EmailNotOpen because the actions are still scheduled + $this->assertTrue(empty($tags['EmailNotOpen'])); + } + + public function testCampaignExecutionForSome() + { + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); + + // Let's analyze + $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13]); + $tags = $this->getTagCounts(); + + // Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1 + $this->assertCount(5, $byEvent[1]); + $this->assertCount(5, $byEvent[2]); + + // Sending Campaign Test Email 1 should be scheduled + foreach ($byEvent[2] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']); + } + } + + // Everyone should have had the Is US condition processed + $this->assertCount(5, $byEvent[11]); + + // 4 should have been send down the non-action path (red) of the condition + $nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]); + $this->assertEquals(4, $nonActionCount); + + // 1 contacts are from the US and should be labeled with US:Action + $this->assertCount(1, $byEvent[12]); + $this->assertEquals(1, $tags['US:Action']); + + // The rest (4) contacts are not from the US and should be labeled with NonUS:Action + $this->assertCount(4, $byEvent[13]); + $this->assertEquals(4, $tags['NonUS:Action']); + + // No emails should be sent till after 5 seconds and the command is ran again + $stats = $this->db->createQueryBuilder() + ->select('*') + ->from($this->prefix.'email_stats', 'stat') + ->where('stat.lead_id <= 2') + ->execute() + ->fetchAll(); + $this->assertCount(0, $stats); + + // Wait 15 seconds then execute the campaign again to send scheduled events + sleep(15); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); + + // Send email 1 should no longer be scheduled + $byEvent = $this->getCampaignEventLogs([2, 4]); + $this->assertCount(5, $byEvent[2]); + foreach ($byEvent[2] as $log) { + if (1 === (int) $log['is_scheduled']) { + $this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']); + } + } + + // The non-action events attached to the decision should have no logs entries + $this->assertCount(0, $byEvent[4]); + + // Check that the emails actually sent + $stats = $this->db->createQueryBuilder() + ->select('*') + ->from($this->prefix.'email_stats', 'stat') + ->where('stat.lead_id <= 2') + ->execute() + ->fetchAll(); + $this->assertCount(2, $stats); + + // Now let's simulate email opens + foreach ($stats as $stat) { + $this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif'); + $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent())); + } + + $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]); + + // The non-action events attached to the decision should have no logs entries + $this->assertCount(0, $byEvent[4]); + $this->assertCount(0, $byEvent[5]); + $this->assertCount(0, $byEvent[14]); + $this->assertCount(0, $byEvent[15]); + + // Those 25 should now have open email decisions logged and the next email sent + $this->assertCount(2, $byEvent[3]); + $this->assertCount(2, $byEvent[10]); + + // Wait 15 seconds to go beyond the inaction timeframe + sleep(15); + + // Execute the command again to trigger inaction related events + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); + + // Now we should have 5 email open decisions + $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); + $this->assertCount(5, $byEvent[3]); + + // 3 should be marked as non_action_path_taken + $nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]); + $this->assertEquals(3, $nonActionCount); + + // A condition should be logged as evaluated for each of the 3 contacts + $this->assertCount(3, $byEvent[4]); + $this->assertCount(3, $byEvent[5]); + + // Tag EmailNotOpen should all be scheduled for these 3 contacts because the condition's timeframe was shorter and therefore the + // contact was sent down the inaction path + $this->assertCount(3, $byEvent[14]); + $this->assertCount(3, $byEvent[15]); + + $utcTimezone = new \DateTimeZone('UTC'); + foreach ($byEvent[14] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']); + } + + $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); + $diff = $this->eventDate->diff($scheduledFor); + + if (2 !== $diff->i) { + $this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)'); + } + } + + foreach ($byEvent[15] as $log) { + if (0 === (int) $log['is_scheduled']) { + $this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']); + } + + $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); + $diff = $this->eventDate->diff($scheduledFor); + + if (6 !== $diff->i) { + $this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)'); + } + } + $byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]); + $tags = $this->getTagCounts(); + + // Of those that did not open the email, 1 should be tagged US:NotOpen + $this->assertCount(1, $byEvent[6]); + $this->assertEquals(1, $tags['US:NotOpen']); + + // And 2 should be tagged NonUS:NotOpen + $this->assertCount(2, $byEvent[7]); + $this->assertEquals(2, $tags['NonUS:NotOpen']); + + // And 2 should be tagged UK:NotOpen + $this->assertCount(2, $byEvent[8]); + $this->assertEquals(2, $tags['UK:NotOpen']); + + // And 1 should be tagged NonUK:NotOpen + $this->assertCount(1, $byEvent[9]); + $this->assertEquals(1, $tags['NonUK:NotOpen']); + + // No one should be tagged as EmailNotOpen because the actions are still scheduled + $this->assertFalse(isset($tags['EmailNotOpen'])); } /** diff --git a/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php new file mode 100644 index 00000000000..8bde63546be --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php @@ -0,0 +1,63 @@ +runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); + + // Wait 15 seconds then execute the campaign again to send scheduled events + sleep(15); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); + + // No open email decisions should be recorded yet + $byEvent = $this->getCampaignEventLogs([3]); + $this->assertCount(0, $byEvent[3]); + + // Wait 15 seconds to go beyond the inaction timeframe + sleep(15); + + // Now they should be inactive + $this->runCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-id' => 1]); + + $byEvent = $this->getCampaignEventLogs([3, 7, 10]); + $this->assertCount(1, $byEvent[3]); // decision recorded + $this->assertCount(1, $byEvent[7]); // inactive event executed + $this->assertCount(0, $byEvent[10]); // the positive path should be 0 + } + + public function testEventsAreExecutedForInactiveEventWithMultipleContact() + { + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); + + // Wait 15 seconds then execute the campaign again to send scheduled events + sleep(15); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); + + // No open email decisions should be recorded yet + $byEvent = $this->getCampaignEventLogs([3]); + $this->assertCount(0, $byEvent[3]); + + // Wait 15 seconds to go beyond the inaction timeframe + sleep(15); + + // Now they should be inactive + $this->runCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-ids' => '1,2,3']); + + $byEvent = $this->getCampaignEventLogs([3, 7, 10]); + $this->assertCount(3, $byEvent[3]); // decision recorded + $this->assertCount(3, $byEvent[7]); // inactive event executed + $this->assertCount(0, $byEvent[10]); // the positive path should be 0 + } +} diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php new file mode 100644 index 00000000000..aaba56566bb --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php @@ -0,0 +1,38 @@ + 'test']); + + $this->assertEmpty($actionAccessor->getExtraProperties()); + } + + public function testBatchNameIsReturned() + { + $actionAccessor = new ActionAccessor(['batchEventName' => 'test']); + + $this->assertEquals('test', $actionAccessor->getBatchEventName()); + } + + public function testExtraParamIsReturned() + { + $actionAccessor = new ActionAccessor(['batchEventName' => 'test', 'foo' => 'bar']); + + $this->assertEquals(['foo' => 'bar'], $actionAccessor->getExtraProperties()); + } +} diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php new file mode 100644 index 00000000000..38b77c775b5 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php @@ -0,0 +1,31 @@ + 'test']); + + $this->assertEquals('test', $accessor->getEventName()); + } + + public function testExtraParamIsReturned() + { + $accessor = new ConditionAccessor(['eventName' => 'test', 'foo' => 'bar']); + + $this->assertEquals(['foo' => 'bar'], $accessor->getExtraProperties()); + } +} diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php new file mode 100644 index 00000000000..168c8f233e7 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php @@ -0,0 +1,31 @@ + 'test']); + + $this->assertEquals('test', $accessor->getEventName()); + } + + public function testExtraParamIsReturned() + { + $accessor = new DecisionAccessor(['eventName' => 'test', 'foo' => 'bar']); + + $this->assertEquals(['foo' => 'bar'], $accessor->getExtraProperties()); + } +} diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php new file mode 100644 index 00000000000..3461da313b4 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php @@ -0,0 +1,91 @@ + [ + 'lead.scorecontactscompanies' => [ + 'label' => 'Add to company\'s score', + 'description' => 'This action will add the specified value to the company\'s existing score', + 'formType' => 'scorecontactscompanies_action', + 'batchEventName' => 'mautic.lead.on_campaign_trigger_action', + ], + ], + Event::TYPE_CONDITION => [ + 'lead.campaigns' => [ + 'label' => 'Contact campaigns', + 'description' => 'Condition based on a contact campaigns.', + 'formType' => 'campaignevent_lead_campaigns', + 'formTheme' => 'MauticLeadBundle:FormTheme\\ContactCampaignsCondition', + 'eventName' => 'mautic.lead.on_campaign_trigger_condition', + ], + ], + Event::TYPE_DECISION => [ + 'email.click' => [ + 'label' => 'Clicks email', + 'description' => 'Trigger actions when an email is clicked. Connect a "Send Email" action to the top of this decision.', + 'eventName' => 'mautic.email.on_campaign_trigger_decision', + 'formType' => 'email_click_decision', + 'connectionRestrictions' => [ + 'source' => [ + 'action' => [ + 'email.send', + ], + ], + ], + ], + ], + ]; + + public function testEventsArrayIsBuiltWithAccessors() + { + $eventAccessor = new EventAccessor($this->events); + + // Actions + $this->assertCount(1, $eventAccessor->getActions()); + $accessor = $eventAccessor->getAction('lead.scorecontactscompanies'); + $this->assertInstanceOf(ActionAccessor::class, $accessor); + $this->assertEquals( + $this->events[Event::TYPE_ACTION]['lead.scorecontactscompanies']['batchEventName'], + $accessor->getBatchEventName() + ); + + // Conditions + $this->assertCount(1, $eventAccessor->getConditions()); + $accessor = $eventAccessor->getCondition('lead.campaigns'); + $this->assertInstanceOf(ConditionAccessor::class, $accessor); + $this->assertEquals( + $this->events[Event::TYPE_CONDITION]['lead.campaigns']['eventName'], + $accessor->getEventName() + ); + + // Decisions + $this->assertCount(1, $eventAccessor->getDecisions()); + $accessor = $eventAccessor->getDecision('email.click'); + $this->assertInstanceOf(DecisionAccessor::class, $accessor); + $this->assertEquals( + $this->events[Event::TYPE_DECISION]['email.click']['eventName'], + $accessor->getEventName() + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Builder/ConnectionBuilderTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Builder/ConnectionBuilderTest.php new file mode 100644 index 00000000000..43648650a72 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Builder/ConnectionBuilderTest.php @@ -0,0 +1,124 @@ + [ + 'action1' => [ + 'connectionRestrictions' => [ + 'anchor' => ['decision1.inaction'], + 'source' => [ + 'decision' => [ + 'decision1', + ], + ], + ], + ], + 'action2' => [ + // BC from way back + 'associatedDecisions' => [ + 'decision1', + ], + ], + 'action3' => [ + // BC from way back + 'anchorRestrictions' => [ + 'decision2.top', + ], + ], + ], + Event::TYPE_DECISION => [ + 'decision1' => [ + 'connectionRestrictions' => ['source' => ['action' => ['action1']]], + ], + 'decision2' => [ + // BC From way back + 'associatedActions' => [ + 'some.decision', + ], + ], + ], + ]; + + $results = ConnectionBuilder::buildRestrictionsArray($eventsArray); + + $expected = [ + 'anchor' => [ + 'decision1' => [ + 'action1' => ['inaction'], + ], + 'action3' => [ + 'decision2' => ['top'], + ], + ], + 'action1' => [ + 'source' => [ + 'action' => [], + 'decision' => ['decision1'], + ], + 'target' => [ + 'action' => [], + 'decision' => [], + ], + ], + 'action2' => [ + 'source' => [ + 'action' => [], + 'decision' => ['decision1'], + ], + 'target' => [ + 'action' => [], + 'decision' => [], + ], + ], + 'action3' => [ + 'source' => [ + 'action' => [], + 'decision' => [], + ], + 'target' => [ + 'action' => [], + 'decision' => [], + ], + ], + 'decision1' => [ + 'source' => [ + 'action' => ['action1'], + 'decision' => [], + ], + 'target' => [ + 'action' => [], + 'decision' => [], + ], + ], + 'decision2' => [ + 'source' => [ + 'action' => [], + 'decision' => [], + ], + 'target' => [ + 'action' => ['some.decision'], + 'decision' => [], + ], + ], + ]; + + $this->assertEquals($expected, $results); + } +} diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php new file mode 100644 index 00000000000..45a8e2311ea --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php @@ -0,0 +1,80 @@ + [ + 'batchEventName' => 'some.action', + ], + 'other.action' => [ + 'batchEventName' => 'other.action', + ], + ]; + + $converted = EventBuilder::buildActions($array); + + $this->assertCount(2, $converted); + $this->assertInstanceOf(ActionAccessor::class, $converted['some.action']); + $this->assertEquals('some.action', $converted['some.action']->getBatchEventName()); + $this->assertInstanceOf(ActionAccessor::class, $converted['other.action']); + $this->assertEquals('other.action', $converted['other.action']->getBatchEventName()); + } + + public function testConditionsAreConvertedToAccessor() + { + $array = [ + 'some.condition' => [ + 'eventName' => 'some.condition', + ], + 'other.condition' => [ + 'eventName' => 'other.condition', + ], + ]; + + $converted = EventBuilder::buildconditions($array); + + $this->assertCount(2, $converted); + $this->assertInstanceOf(ConditionAccessor::class, $converted['some.condition']); + $this->assertEquals('some.condition', $converted['some.condition']->getEventName()); + $this->assertInstanceOf(ConditionAccessor::class, $converted['other.condition']); + $this->assertEquals('other.condition', $converted['other.condition']->getEventName()); + } + + public function testDecisionsAreConvertedToAccessor() + { + $array = [ + 'some.decision' => [ + 'eventName' => 'some.decision', + ], + 'other.decision' => [ + 'eventName' => 'other.decision', + ], + ]; + + $converted = EventBuilder::builddecisions($array); + + $this->assertCount(2, $converted); + $this->assertInstanceOf(DecisionAccessor::class, $converted['some.decision']); + $this->assertEquals('some.decision', $converted['some.decision']->getEventName()); + $this->assertInstanceOf(DecisionAccessor::class, $converted['other.decision']); + $this->assertEquals('other.decision', $converted['other.decision']->getEventName()); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php new file mode 100644 index 00000000000..c05c5b988b3 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php @@ -0,0 +1,124 @@ +leadRepository = $this->getMockBuilder(LeadRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->campaignRepository = $this->getMockBuilder(CampaignRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->campaignLeadRepository = $this->getMockBuilder(CampaignLeadRepository::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testNoContactsFoundExceptionIsThrown() + { + $this->campaignLeadRepository->expects($this->once()) + ->method('getInactiveContacts') + ->willReturn([]); + + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $this->getContactFinder()->getContacts(1, new Event(), 0, $limiter); + } + + public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound() + { + $contactMemberDates = [ + 1 => new \DateTime(), + ]; + + $this->campaignLeadRepository->expects($this->once()) + ->method('getInactiveContacts') + ->willReturn($contactMemberDates); + + $this->leadRepository->expects($this->once()) + ->method('getContactCollection') + ->willReturn([]); + + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $this->getContactFinder()->getContacts(1, new Event(), 0, $limiter); + } + + public function testContactsAreFoundAndStoredInCampaignMemberDatesAdded() + { + $contactMemberDates = [ + 1 => new \DateTime(), + ]; + + $this->campaignLeadRepository->expects($this->once()) + ->method('getInactiveContacts') + ->willReturn($contactMemberDates); + + $this->leadRepository->expects($this->once()) + ->method('getContactCollection') + ->willReturn(new ArrayCollection([new Lead()])); + + $contactFinder = $this->getContactFinder(); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $contacts = $contactFinder->getContacts(1, new Event(), 0, $limiter); + $this->assertCount(1, $contacts); + + $this->assertEquals($contactMemberDates, $contactFinder->getDatesAdded()); + } + + /** + * @return InactiveContacts + */ + private function getContactFinder() + { + return new InactiveContacts( + $this->leadRepository, + $this->campaignRepository, + $this->campaignLeadRepository, + new NullLogger() + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php new file mode 100644 index 00000000000..972c585102c --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php @@ -0,0 +1,104 @@ +leadRepository = $this->getMockBuilder(LeadRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->campaignRepository = $this->getMockBuilder(CampaignRepository::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testNoContactsFoundExceptionIsThrown() + { + $this->campaignRepository->expects($this->once()) + ->method('getPendingContactIds') + ->willReturn([]); + + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $this->getContactFinder()->getContacts(1, $limiter); + } + + public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound() + { + $contactIds = [1, 2]; + + $this->campaignRepository->expects($this->once()) + ->method('getPendingContactIds') + ->willReturn($contactIds); + + $this->leadRepository->expects($this->once()) + ->method('getContactCollection') + ->willReturn([]); + + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $this->getContactFinder()->getContacts(1, $limiter); + } + + public function testArrayCollectionIsReturnedForFoundContacts() + { + $contactIds = [1, 2]; + + $this->campaignRepository->expects($this->once()) + ->method('getPendingContactIds') + ->willReturn($contactIds); + + $foundContacts = new ArrayCollection([new Lead(), new Lead()]); + $this->leadRepository->expects($this->once()) + ->method('getContactCollection') + ->willReturn($foundContacts); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $this->assertEquals($foundContacts, $this->getContactFinder()->getContacts(1, $limiter)); + } + + /** + * @return KickoffContacts + */ + private function getContactFinder() + { + return new KickoffContacts( + $this->leadRepository, + $this->campaignRepository, + new NullLogger() + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php new file mode 100644 index 00000000000..8b49bf7d743 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php @@ -0,0 +1,94 @@ +leadRepository = $this->getMockBuilder(LeadRepository::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testHydratedLeadsFromRepositoryAreFoundAndPushedIntoLogs() + { + $lead1 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead1->expects($this->exactly(2)) + ->method('getId') + ->willReturn(1); + + $lead2 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead2->expects($this->exactly(2)) + ->method('getId') + ->willReturn(2); + + $log1 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log1->expects($this->exactly(2)) + ->method('getLead') + ->willReturn($lead1); + $log1->expects($this->once()) + ->method('setLead'); + + $log2 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log2->expects($this->exactly(2)) + ->method('getLead') + ->willReturn($lead2); + $log2->expects($this->once()) + ->method('setLead'); + + $logs = new ArrayCollection( + [ + 1 => $log1, + 2 => $log2, + ] + ); + + $contacs = new ArrayCollection( + [ + 1 => $lead1, + 2 => $lead2, + ] + ); + + $this->leadRepository->expects($this->once()) + ->method('getContactCollection') + ->willReturn($contacs); + + $this->getContactFinder()->hydrateContacts($logs); + } + + /** + * @return ScheduledContacts + */ + private function getContactFinder() + { + return new ScheduledContacts( + $this->leadRepository + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php new file mode 100644 index 00000000000..232f43680b0 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php @@ -0,0 +1,322 @@ +leadModel = $this->getMockBuilder(LeadModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventRepository = $this->getMockBuilder(EventRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->executioner = $this->getMockBuilder(EventExecutioner::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->decisionExecutioner = $this->getMockBuilder(Decision::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventCollector = $this->getMockBuilder(EventCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventScheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contactTracker = $this->getMockBuilder(ContactTracker::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testContactNotFoundResultsInEmptyResponses() + { + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn(null); + + $this->eventRepository->expects($this->never()) + ->method('getContactPendingEvents'); + + $responses = $this->getDecisionExecutioner()->execute('something'); + + $this->assertEquals(0, $responses->containsResponses()); + } + + public function testNoRelatedEventsResultInEmptyResponses() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->exactly(3)) + ->method('getId') + ->willReturn(10); + + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn($lead); + + $this->eventRepository->expects($this->once()) + ->method('getContactPendingEvents') + ->willReturn([]); + + $this->eventCollector->expects($this->never()) + ->method('getEventConfig'); + + $responses = $this->getDecisionExecutioner()->execute('something'); + + $this->assertEquals(0, $responses->containsResponses()); + } + + public function testChannelMisMatchResultsInEmptyResponses() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->exactly(5)) + ->method('getId') + ->willReturn(10); + + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn($lead); + + $event = $this->getMockBuilder(Event::class) + ->getMock(); + $event->expects($this->exactly(2)) + ->method('getChannel') + ->willReturn('email'); + + $this->eventRepository->expects($this->once()) + ->method('getContactPendingEvents') + ->willReturn([$event]); + + $this->eventCollector->expects($this->never()) + ->method('getEventConfig'); + + $responses = $this->getDecisionExecutioner()->execute('something', null, 'page'); + + $this->assertEquals(0, $responses->containsResponses()); + } + + public function testChannelIdMisMatchResultsInEmptyResponses() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->exactly(5)) + ->method('getId') + ->willReturn(10); + + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn($lead); + + $event = $this->getMockBuilder(Event::class) + ->getMock(); + $event->expects($this->exactly(2)) + ->method('getChannel') + ->willReturn('email'); + $event->expects($this->exactly(2)) + ->method('getChannelId') + ->willReturn(3); + + $this->eventRepository->expects($this->once()) + ->method('getContactPendingEvents') + ->willReturn([$event]); + + $this->eventCollector->expects($this->never()) + ->method('getEventConfig'); + + $responses = $this->getDecisionExecutioner()->execute('something', null, 'email', 1); + + $this->assertEquals(0, $responses->containsResponses()); + } + + public function testEmptyPositiveactionsResultsInEmptyResponses() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->exactly(5)) + ->method('getId') + ->willReturn(10); + + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn($lead); + + $event = $this->getMockBuilder(Event::class) + ->getMock(); + $event->expects($this->exactly(2)) + ->method('getChannel') + ->willReturn('email'); + $event->expects($this->exactly(2)) + ->method('getChannelId') + ->willReturn(3); + $event->expects($this->once()) + ->method('getPositiveChildren') + ->willReturn(new ArrayCollection()); + + $this->eventRepository->expects($this->once()) + ->method('getContactPendingEvents') + ->willReturn([$event]); + + $this->eventCollector->expects($this->once()) + ->method('getEventConfig') + ->willReturn(new DecisionAccessor([])); + + $this->decisionExecutioner->expects($this->once()) + ->method('evaluateForContact'); + + $responses = $this->getDecisionExecutioner()->execute('something', null, 'email', 3); + + $this->assertEquals(0, $responses->containsResponses()); + } + + public function testAssociatedEventsAreExecuted() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->exactly(5)) + ->method('getId') + ->willReturn(10); + $lead->expects($this->once()) + ->method('getChanges') + ->willReturn(['notempty' => true]); + + $this->leadModel->expects($this->once()) + ->method('saveEntity'); + + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn($lead); + + $action1 = $this->getMockBuilder(Event::class) + ->getMock(); + $action2 = $this->getMockBuilder(Event::class) + ->getMock(); + + $event = $this->getMockBuilder(Event::class) + ->getMock(); + $event->expects($this->exactly(2)) + ->method('getChannel') + ->willReturn('email'); + $event->expects($this->exactly(2)) + ->method('getChannelId') + ->willReturn(3); + $event->expects($this->once()) + ->method('getPositiveChildren') + ->willReturn(new ArrayCollection([$action1, $action2])); + + $this->eventRepository->expects($this->once()) + ->method('getContactPendingEvents') + ->willReturn([$event]); + + $this->eventCollector->expects($this->once()) + ->method('getEventConfig') + ->willReturn(new DecisionAccessor([])); + + $this->decisionExecutioner->expects($this->once()) + ->method('evaluateForContact'); + + $this->eventScheduler->expects($this->exactly(2)) + ->method('getExecutionDateTime') + ->willReturn(new \DateTime()); + + $this->eventScheduler->expects($this->at(1)) + ->method('shouldSchedule') + ->willReturn(true); + + $this->eventScheduler->expects($this->once()) + ->method('scheduleForContact'); + + $this->eventScheduler->expects($this->at(3)) + ->method('shouldSchedule') + ->willReturn(false); + + $this->executioner->expects($this->once()) + ->method('executeForContact'); + + $responses = $this->getDecisionExecutioner()->execute('something', null, 'email', 3); + + $this->assertEquals(0, $responses->containsResponses()); + } + + /** + * @return DecisionExecutioner + */ + private function getDecisionExecutioner() + { + return new DecisionExecutioner( + new NullLogger(), + $this->leadModel, + $this->eventRepository, + $this->executioner, + $this->decisionExecutioner, + $this->eventCollector, + $this->eventScheduler, + $this->contactTracker + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php new file mode 100644 index 00000000000..4793221034f --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php @@ -0,0 +1,374 @@ +dispatcher = $this->getMockBuilder(EventDispatcherInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->legacyDispatcher = $this->getMockBuilder(LegacyEventDispatcher::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testActionBatchEventIsDispatchedWithSuccessAndFailedLogs() + { + $event = new Event(); + + $lead1 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead1->expects($this->exactly(2)) + ->method('getId') + ->willReturn(1); + + $lead2 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead2->expects($this->exactly(2)) + ->method('getId') + ->willReturn(2); + + $log1 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log1->expects($this->exactly(2)) + ->method('getLead') + ->willReturn($lead1); + $log1->method('setIsScheduled') + ->willReturn($log1); + $log1->method('getEvent') + ->willReturn($event); + + $log2 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log2->expects($this->exactly(2)) + ->method('getLead') + ->willReturn($lead2); + $log2->method('getMetadata') + ->willReturn([]); + $log2->method('getEvent') + ->willReturn($event); + + $logs = new ArrayCollection( + [ + 1 => $log1, + 2 => $log2, + ] + ); + + $config = $this->getMockBuilder(ActionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getBatchEventName') + ->willReturn('something'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->willReturnCallback( + function ($eventName, PendingEvent $pendingEvent) use ($logs) { + $pendingEvent->pass($logs->get(1)); + $pendingEvent->fail($logs->get(2), 'just because'); + } + ); + + $this->dispatcher->expects($this->at(1)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_EXECUTED, $this->isInstanceOf(ExecutedEvent::class)); + + $this->dispatcher->expects($this->at(2)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_EXECUTED_BATCH, $this->isInstanceOf(ExecutedBatchEvent::class)); + + $this->dispatcher->expects($this->at(3)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_FAILED, $this->isInstanceOf(FailedEvent::class)); + + $this->scheduler->expects($this->once()) + ->method('rescheduleFailure') + ->with($logs->get(2)); + + $this->legacyDispatcher->expects($this->once()) + ->method('dispatchExecutionEvents'); + + $this->getEventDispatcher()->dispatchActionEvent($config, $event, $logs); + } + + public function testActionLogNotProcessedExceptionIsThrownIfLogNotProcessedWithSuccess() + { + $this->expectException(LogNotProcessedException::class); + + $event = new Event(); + + $lead1 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead1->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $lead2 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead2->expects($this->once()) + ->method('getId') + ->willReturn(2); + + $log1 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log1->expects($this->once()) + ->method('getLead') + ->willReturn($lead1); + $log1->method('setIsScheduled') + ->willReturn($log1); + $log1->method('getEvent') + ->willReturn($event); + + $log2 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log2->expects($this->once()) + ->method('getLead') + ->willReturn($lead2); + $log2->method('getMetadata') + ->willReturn([]); + $log2->method('getEvent') + ->willReturn($event); + + $logs = new ArrayCollection( + [ + 1 => $log1, + 2 => $log2, + ] + ); + + $config = $this->getMockBuilder(ActionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getBatchEventName') + ->willReturn('something'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->willReturnCallback( + function ($eventName, PendingEvent $pendingEvent) use ($logs) { + $pendingEvent->pass($logs->get(1)); + + // One log is not processed so the exception should be thrown + } + ); + + $this->getEventDispatcher()->dispatchActionEvent($config, $event, $logs); + } + + public function testActionLogNotProcessedExceptionIsThrownIfLogNotProcessedWithFailed() + { + $this->expectException(LogNotProcessedException::class); + + $event = new Event(); + + $lead1 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead1->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $lead2 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead2->expects($this->once()) + ->method('getId') + ->willReturn(2); + + $log1 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log1->expects($this->once()) + ->method('getLead') + ->willReturn($lead1); + $log1->method('setIsScheduled') + ->willReturn($log1); + $log1->method('getEvent') + ->willReturn($event); + + $log2 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log2->expects($this->once()) + ->method('getLead') + ->willReturn($lead2); + $log2->method('getMetadata') + ->willReturn([]); + $log2->method('getEvent') + ->willReturn($event); + + $logs = new ArrayCollection( + [ + 1 => $log1, + 2 => $log2, + ] + ); + + $config = $this->getMockBuilder(ActionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getBatchEventName') + ->willReturn('something'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->willReturnCallback( + function ($eventName, PendingEvent $pendingEvent) use ($logs) { + $pendingEvent->fail($logs->get(2), 'something'); + + // One log is not processed so the exception should be thrown + } + ); + + $this->getEventDispatcher()->dispatchActionEvent($config, $event, $logs); + } + + public function testActionBatchEventIsIgnoredWithLegacy() + { + $event = new Event(); + + $config = $this->getMockBuilder(ActionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getBatchEventName') + ->willReturn(null); + + $this->dispatcher->expects($this->never()) + ->method('dispatch'); + + $this->legacyDispatcher->expects($this->once()) + ->method('dispatchCustomEvent'); + + $this->getEventDispatcher()->dispatchActionEvent($config, $event, new ArrayCollection()); + } + + public function testDecisionEventIsDispatched() + { + $config = $this->getMockBuilder(DecisionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getEventName') + ->willReturn('something'); + + $this->legacyDispatcher->expects($this->once()) + ->method('dispatchDecisionEvent'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(DecisionEvent::class)); + + $this->dispatcher->expects($this->at(1)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $this->isInstanceOf(DecisionEvent::class)); + + $this->getEventDispatcher()->dispatchDecisionEvent($config, new LeadEventLog(), null); + } + + public function testDecisionResultsEventIsDispatched() + { + $config = $this->getMockBuilder(DecisionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, $this->isInstanceOf(DecisionResultsEvent::class)); + + $this->getEventDispatcher()->dispatchDecisionResultsEvent($config, new ArrayCollection(), new EvaluatedContacts()); + } + + public function testConditionEventIsDispatched() + { + $config = $this->getMockBuilder(ConditionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getEventName') + ->willReturn('something'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(ConditionEvent::class)); + + $this->dispatcher->expects($this->at(1)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $this->isInstanceOf(ConditionEvent::class)); + + $this->getEventDispatcher()->dispatchConditionEvent($config, new LeadEventLog()); + } + + /** + * @return EventDispatcher + */ + private function getEventDispatcher() + { + return new EventDispatcher( + $this->dispatcher, + new NullLogger(), + $this->scheduler, + $this->legacyDispatcher + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php new file mode 100644 index 00000000000..15138f525e6 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php @@ -0,0 +1,465 @@ +dispatcher = $this->getMockBuilder(EventDispatcherInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->leadModel = $this->getMockBuilder(LeadModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->mauticFactory = $this->getMockBuilder(MauticFactory::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testAllEventsAreFailedWithBadConfig() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getConfig') + ->willReturn([]); + + $logs = new ArrayCollection([new LeadEventLog()]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + $pendingEvent->expects($this->once()) + ->method('failAll'); + + $this->leadModel->expects($this->never()) + ->method('setSystemCurrentLead'); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent, $this->mauticFactory); + } + + public function testPrimayLegacyEventsAreProcessed() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // BC default is to have pass + $pendingEvent->expects($this->once()) + ->method('pass'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Legacy custom event should dispatch + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)); + + // Legacy execution event should dispatch + $this->dispatcher->expects($this->at(1)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_EXECUTION, $this->isInstanceOf(CampaignExecutionEvent::class)); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + } + + public function testPrimaryCallbackIsProcessed() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['callback' => [self::class, 'bogusCallback']]); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // BC default is to have pass + $pendingEvent->expects($this->once()) + ->method('pass'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Legacy execution event should dispatch + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_EXECUTION, $this->isInstanceOf(CampaignExecutionEvent::class)); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + } + + public function testArrayResultAppendedToMetadata() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $leadEventLog->setMetadata(['bar' => 'foo']); + + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // BC default is to have pass + $pendingEvent->expects($this->once()) + ->method('pass'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Legacy custom event should dispatch + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)) + ->willReturnCallback(function ($eventName, CampaignExecutionEvent $event) { + $event->setResult(['foo' => 'bar']); + }); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + + $this->assertEquals(['bar' => 'foo', 'foo' => 'bar'], $leadEventLog->getMetadata()); + } + + public function testFailedResultAsFalseIsProcessed() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $leadEventLog->setMetadata(['bar' => 'foo']); + + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // Should fail because we're returning false + $pendingEvent->expects($this->once()) + ->method('fail'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Legacy custom event should dispatch + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)) + ->willReturnCallback(function ($eventName, CampaignExecutionEvent $event) { + $event->setResult(false); + }); + + $this->dispatcher->expects($this->at(2)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_FAILED, $this->isInstanceOf(FailedEvent::class)); + + $this->scheduler->expects($this->once()) + ->method('rescheduleFailure'); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + } + + public function testFailedResultAsArrayIsProcessed() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // Should fail because we're returning false + $pendingEvent->expects($this->once()) + ->method('fail'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Legacy custom event should dispatch + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)) + ->willReturnCallback(function ($eventName, CampaignExecutionEvent $event) { + $event->setResult(['result' => false, 'foo' => 'bar']); + }); + + $this->dispatcher->expects($this->at(2)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_FAILED, $this->isInstanceOf(FailedEvent::class)); + + $this->scheduler->expects($this->once()) + ->method('rescheduleFailure'); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + } + + public function testPassWithErrorIsHandled() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $leadEventLog->setMetadata(['bar' => 'foo']); + + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // Should pass but with an error logged + $pendingEvent->expects($this->once()) + ->method('passWithError'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Legacy custom event should dispatch + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)) + ->willReturnCallback(function ($eventName, CampaignExecutionEvent $event) { + $event->setResult(['failed' => 1, 'reason' => 'because']); + }); + + $this->scheduler->expects($this->never()) + ->method('rescheduleFailure'); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + } + + public function testLogIsPassed() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(2)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $leadEventLog->setMetadata(['bar' => 'foo']); + + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // Should fail because we're returning false + $pendingEvent->expects($this->once()) + ->method('pass'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + // Should pass + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)) + ->willReturnCallback(function ($eventName, CampaignExecutionEvent $event) { + $event->setResult(true); + }); + + $this->scheduler->expects($this->never()) + ->method('rescheduleFailure'); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); + } + + public function testLegacyEventDispatchedForConvertedBatchActions() + { + $config = $this->getMockBuilder(AbstractEventAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->exactly(1)) + ->method('getConfig') + ->willReturn(['eventName' => 'something']); + + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $leadEventLog = new LeadEventLog(); + $leadEventLog->setEvent($event); + $leadEventLog->setLead(new Lead()); + $leadEventLog->setMetadata(['bar' => 'foo']); + + $logs = new ArrayCollection([$leadEventLog]); + + $pendingEvent = $this->getMockBuilder(PendingEvent::class) + ->disableOriginalConstructor() + ->getMock(); + + // Should never be called + $pendingEvent->expects($this->never()) + ->method('pass'); + + $this->leadModel->expects($this->exactly(2)) + ->method('setSystemCurrentLead'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(CampaignExecutionEvent::class)) + ->willReturnCallback(function ($eventName, CampaignExecutionEvent $event) { + $event->setResult(true); + }); + + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, true, $pendingEvent); + } + + /** + * @return LegacyEventDispatcher + */ + private function getLegacyEventDispatcher() + { + return new LegacyEventDispatcher( + $this->dispatcher, + $this->scheduler, + new NullLogger(), + $this->leadModel, + $this->mauticFactory + ); + } + + public static function bogusCallback() + { + return true; + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php new file mode 100644 index 00000000000..9944b55a5e9 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php @@ -0,0 +1,225 @@ +inactiveContactFinder = $this->getMockBuilder(InactiveContacts::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->translator = $this->getMockBuilder(Translator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventScheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->inactiveHelper = $this->getMockBuilder(InactiveHelper::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventExecutioner = $this->getMockBuilder(EventExecutioner::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testNoContactsFoundResultsInNothingExecuted() + { + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('getEventsByType') + ->willReturn(new ArrayCollection()); + + $this->inactiveContactFinder->expects($this->never()) + ->method('getContactCount'); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $counter = $this->getExecutioner()->execute($campaign, $limiter); + + $this->assertEquals(0, $counter->getEvaluated()); + } + + public function testNoEventsFoundResultsInNothingExecuted() + { + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('getEventsByType') + ->willReturn(new ArrayCollection([new Event()])); + + $this->inactiveContactFinder->expects($this->once()) + ->method('getContactCount') + ->willReturn(0); + + $limiter = new ContactLimiter(0, 0, 0, 0); + $counter = $this->getExecutioner()->execute($campaign, $limiter); + + $this->assertEquals(0, $counter->getTotalEvaluated()); + } + + public function testNextBatchOfContactsAreExecuted() + { + $decision = new Event(); + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('getEventsByType') + ->willReturn(new ArrayCollection([$decision])); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $this->inactiveContactFinder->expects($this->once()) + ->method('getContactCount') + ->willReturn(2); + + $this->inactiveContactFinder->expects($this->exactly(3)) + ->method('getContacts') + ->withConsecutive( + [null, $decision, 0, $limiter], + [null, $decision, 3, $limiter], + [null, $decision, 10, $limiter] + ) + ->willReturnOnConsecutiveCalls( + new ArrayCollection([3 => new Lead()]), + new ArrayCollection([10 => new Lead()]), + new ArrayCollection([]) + ); + + $this->inactiveHelper->expects($this->exactly(2)) + ->method('removeContactsThatAreNotApplicable') + ->willReturn(new \DateTime()); + + $this->getExecutioner()->execute($campaign, $limiter); + } + + public function testValidationExecutesNothingIfCampaignUnpublished() + { + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('isPublished') + ->willReturn(false); + + $event = new Event(); + $event->setCampaign($campaign); + + $this->inactiveHelper->expects($this->once()) + ->method('getCollectionByDecisionId') + ->with(1) + ->willReturn(new ArrayCollection([$event])); + + $this->inactiveContactFinder->expects($this->never()) + ->method('getContacts'); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $counter = $this->getExecutioner()->validate(1, $limiter); + $this->assertEquals(0, $counter->getTotalEvaluated()); + } + + public function testValidationEvaluatesFoundEvents() + { + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('isPublished') + ->willReturn(true); + + $decision = new Event(); + $decision->setCampaign($campaign); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $this->inactiveHelper->expects($this->once()) + ->method('getCollectionByDecisionId') + ->with(1) + ->willReturn(new ArrayCollection([$decision])); + + $this->inactiveContactFinder->expects($this->once()) + ->method('getContactCount') + ->willReturn(2); + + $this->inactiveContactFinder->expects($this->exactly(3)) + ->method('getContacts') + ->withConsecutive( + [null, $decision, 0, $limiter], + [null, $decision, 3, $limiter], + [null, $decision, 10, $limiter] + ) + ->willReturnOnConsecutiveCalls( + new ArrayCollection([3 => new Lead()]), + new ArrayCollection([10 => new Lead()]), + new ArrayCollection([]) + ); + + $this->inactiveHelper->expects($this->exactly(2)) + ->method('removeContactsThatAreNotApplicable') + ->willReturn(new \DateTime()); + + $this->getExecutioner()->validate(1, $limiter); + } + + private function getExecutioner() + { + return new InactiveExecutioner( + $this->inactiveContactFinder, + new NullLogger(), + $this->translator, + $this->eventScheduler, + $this->inactiveHelper, + $this->eventExecutioner + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php new file mode 100644 index 00000000000..c13eedff880 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php @@ -0,0 +1,146 @@ +kickoffContacts = $this->getMockBuilder(KickoffContacts::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->translator = $this->getMockBuilder(Translator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->executioner = $this->getMockBuilder(EventExecutioner::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testNoContactsResultInEmptyResults() + { + $this->kickoffContacts->expects($this->once()) + ->method('getContactCount') + ->willReturn(0); + + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('getRootEvents') + ->willReturn(new ArrayCollection()); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $counter = $this->getExecutioner()->execute($campaign, $limiter); + + $this->assertEquals(0, $counter->getTotalEvaluated()); + } + + public function testEventsAreScheduledAndExecuted() + { + $this->kickoffContacts->expects($this->once()) + ->method('getContactCount') + ->willReturn(2); + + $this->kickoffContacts->expects($this->exactly(3)) + ->method('getContacts') + ->willReturnOnConsecutiveCalls( + new ArrayCollection([3 => new Lead()]), + new ArrayCollection([10 => new Lead()]), + new ArrayCollection([]) + ); + + $event = new Event(); + $event2 = new Event(); + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->expects($this->once()) + ->method('getRootEvents') + ->willReturn(new ArrayCollection([$event, $event2])); + $event->setCampaign($campaign); + $event2->setCampaign($campaign); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $this->scheduler->expects($this->exactly(4)) + ->method('getExecutionDateTime') + ->willReturn(new \DateTime()); + + // Schedule one event and execute the other + $this->scheduler->expects($this->exactly(4)) + ->method('shouldSchedule') + ->willReturnOnConsecutiveCalls(true, true, false, false); + + $this->scheduler->expects($this->exactly(2)) + ->method('schedule'); + + $this->executioner->expects($this->exactly(2)) + ->method('executeForContacts'); + + $counter = $this->getExecutioner()->execute($campaign, $limiter); + + $this->assertEquals(4, $counter->getTotalEvaluated()); + $this->assertEquals(2, $counter->getTotalScheduled()); + } + + /** + * @return KickoffExecutioner + */ + private function getExecutioner() + { + return new KickoffExecutioner( + new NullLogger(), + $this->kickoffContacts, + $this->translator, + $this->executioner, + $this->scheduler + ); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php new file mode 100644 index 00000000000..1c8b10fd687 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php @@ -0,0 +1,212 @@ +repository = $this->getMockBuilder(LeadEventLogRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->translator = $this->getMockBuilder(Translator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->executioner = $this->getMockBuilder(EventExecutioner::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contactFinder = $this->getMockBuilder(ScheduledContacts::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testNoEventsResultInEmptyResults() + { + $this->repository->expects($this->once()) + ->method('getScheduledCounts') + ->willReturn(['nada' => 0]); + + $this->repository->expects($this->never()) + ->method('getScheduled'); + + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $counter = $this->getExecutioner()->execute($campaign, $limiter); + + $this->assertEquals(0, $counter->getTotalEvaluated()); + } + + public function testEventsAreExecuted() + { + $this->repository->expects($this->once()) + ->method('getScheduledCounts') + ->willReturn([1 => 2, 2 => 2]); + + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + + $event = new Event(); + $event->setCampaign($campaign); + + $log1 = new LeadEventLog(); + $log1->setEvent($event); + $log1->setCampaign($campaign); + + $log2 = new LeadEventLog(); + $log2->setEvent($event); + $log2->setCampaign($campaign); + + $event2 = new Event(); + $event2->setCampaign($campaign); + + $log3 = new LeadEventLog(); + $log3->setEvent($event2); + $log3->setCampaign($campaign); + + $log4 = new LeadEventLog(); + $log4->setEvent($event2); + $log4->setCampaign($campaign); + + $this->repository->expects($this->exactly(4)) + ->method('getScheduled') + ->willReturnOnConsecutiveCalls( + new ArrayCollection( + [ + $log1, + $log2, + ] + ), + new ArrayCollection(), + new ArrayCollection( + [ + $log3, + $log4, + ] + ), + new ArrayCollection() + ); + + $this->executioner->expects($this->exactly(2)) + ->method('executeLogs'); + + $limiter = new ContactLimiter(0, 0, 0, 0); + + $counter = $this->getExecutioner()->execute($campaign, $limiter); + + $this->assertEquals(4, $counter->getTotalEvaluated()); + } + + public function testSpecificEventsAreExecuted() + { + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + + $event = $this->getMockBuilder(Event::class) + ->getMock(); + $event->method('getId') + ->willReturn(1); + $event->method('getCampaign') + ->willReturn($campaign); + + $log1 = new LeadEventLog(); + $log1->setEvent($event); + $log1->setCampaign($campaign); + $log1->setDateTriggered(new \DateTime()); + + $log2 = new LeadEventLog(); + $log2->setEvent($event); + $log2->setCampaign($campaign); + $log2->setDateTriggered(new \DateTime()); + + $logs = new ArrayCollection([$log1, $log2]); + + $this->repository->expects($this->once()) + ->method('getScheduledById') + ->with([1, 2]) + ->willReturn($logs); + + $this->scheduler->method('getExecutionDateTime') + ->willReturn(new \DateTime()); + + // Should only be executed once because the two logs were grouped by event ID + $this->executioner->expects($this->exactly(1)) + ->method('executeLogs'); + + $counter = $this->getExecutioner()->executeByIds([1, 2]); + + // Two events were evaluated + $this->assertEquals(2, $counter->getTotalEvaluated()); + } + + /** + * @return ScheduledExecutioner + */ + private function getExecutioner() + { + return new ScheduledExecutioner( + $this->repository, + new NullLogger(), + $this->translator, + $this->executioner, + $this->scheduler, + $this->contactFinder + ); + } +} diff --git a/app/bundles/ChannelBundle/Tests/PreferenceBuilder/ChannelPreferencesTest.php b/app/bundles/ChannelBundle/Tests/PreferenceBuilder/ChannelPreferencesTest.php new file mode 100644 index 00000000000..92e52fb0810 --- /dev/null +++ b/app/bundles/ChannelBundle/Tests/PreferenceBuilder/ChannelPreferencesTest.php @@ -0,0 +1,59 @@ +setCampaign($campaign); + + $channelPreferences = $this->getChannelPreference('email', $event); + + $log1 = new LeadEventLog(); + $log1->setEvent($event); + $log1->setCampaign($campaign); + $log1->setMetadata(['log' => 1]); + $channelPreferences->addLog($log1, 1); + + $log2 = new LeadEventLog(); + $log2->setEvent($event); + $log2->setCampaign($campaign); + $log2->setMetadata(['log' => 2]); + $channelPreferences->addLog($log2, 2); + + $organized = $channelPreferences->getLogsByPriority(1); + $this->assertEquals($organized->first()->getMetadata()['log'], 1); + + $organized = $channelPreferences->getLogsByPriority(2); + $this->assertEquals($organized->first()->getMetadata()['log'], 2); + } + + /** + * @param $channel + * @param Event $event + * + * @return ChannelPreferences + */ + private function getChannelPreference($channel, Event $event) + { + return new ChannelPreferences($channel, $event, new NullLogger()); + } +} diff --git a/app/bundles/ChannelBundle/Tests/PreferenceBuilder/PreferenceBuilderTest.php b/app/bundles/ChannelBundle/Tests/PreferenceBuilder/PreferenceBuilderTest.php new file mode 100644 index 00000000000..95d0b1f5afa --- /dev/null +++ b/app/bundles/ChannelBundle/Tests/PreferenceBuilder/PreferenceBuilderTest.php @@ -0,0 +1,167 @@ +getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->once()) + ->method('getChannelRules') + ->willReturn( + [ + 'sms' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + 'email' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + ] + ); + + $log = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log->method('getLead') + ->willReturn($lead); + $log->method('getId') + ->willReturn(1); + + $lead2 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead2->expects($this->once()) + ->method('getChannelRules') + ->willReturn( + [ + 'email' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + 'sms' => [ + 'dnc' => DoNotContact::UNSUBSCRIBED, + ], + ] + ); + + $log2 = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log2->method('getLead') + ->willReturn($lead2); + $log2->method('getId') + ->willReturn(2); + + $logs = new ArrayCollection([$log, $log2]); + + $event = new Event(); + + $builder = new PreferenceBuilder($logs, $event, ['email' => [], 'sms' => [], 'push' => []], new NullLogger()); + + $preferences = $builder->getChannelPreferences(); + + $this->assertCount(3, $preferences); + $this->assertTrue(isset($preferences['email'])); + $this->assertTrue(isset($preferences['sms'])); + $this->assertTrue(isset($preferences['push'])); + + /** @var ChannelPreferences $emailLogs */ + $email = $preferences['email']; + + // First priority + $emailLogs = $email->getLogsByPriority(1); + $this->assertCount(1, $emailLogs); + $this->assertEquals(2, $emailLogs->first()->getId()); + + // Second priority + $emailLogs = $email->getLogsByPriority(2); + $this->assertCount(1, $emailLogs); + $this->assertEquals(1, $emailLogs->first()->getId()); + + // First priority for SMS which should just be one + /** @var ChannelPreferences $smsLogs */ + $sms = $preferences['sms']; + $smsLogs = $sms->getLogsByPriority(1); + $this->assertCount(1, $smsLogs); + $this->assertEquals(1, $smsLogs->first()->getId()); + + // None for second priority because of DNC + $smsLogs = $sms->getLogsByPriority(2); + $this->assertCount(0, $smsLogs); + + // No one had push enabled but it should be defined + $push = $preferences['push']; + $pushLogs = $push->getLogsByPriority(1); + $this->assertCount(0, $pushLogs); + } + + public function testLogIsRemovedFromAllChannels() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->once()) + ->method('getChannelRules') + ->willReturn( + [ + 'sms' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + 'email' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + ] + ); + + $log = $this->getMockBuilder(LeadEventLog::class) + ->getMock(); + $log->method('getLead') + ->willReturn($lead); + $log->method('getId') + ->willReturn(1); + + $logs = new ArrayCollection([$log]); + + $event = new Event(); + $builder = new PreferenceBuilder($logs, $event, ['email' => [], 'sms' => [], 'push' => []], new NullLogger()); + + $preferences = $builder->getChannelPreferences(); + /** @var ChannelPreferences $sms */ + $sms = $preferences['sms']; + $smsLogs = $sms->getLogsByPriority(1); + $this->assertCount(1, $smsLogs); + + /** @var ChannelPreferences $email */ + $email = $preferences['email']; + $emailLogs = $email->getLogsByPriority(2); + $this->assertCount(1, $emailLogs); + + $builder->removeLogFromAllChannels($log); + + $preferences = $builder->getChannelPreferences(); + /** @var ChannelPreferences $sms */ + $sms = $preferences['sms']; + $smsLogs = $sms->getLogsByPriority(1); + $this->assertCount(0, $smsLogs); + + /** @var ChannelPreferences $email */ + $email = $preferences['email']; + $emailLogs = $email->getLogsByPriority(2); + $this->assertCount(0, $emailLogs); + } +} From defc1fe7342e7baa5bf0c2ace0997b4ee4d23bba Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 00:54:16 -0500 Subject: [PATCH 462/778] Make Travis happy --- app/bundles/CampaignBundle/Model/LegacyEventModel.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModel.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php index 9a5cb125307..48b4cc9dad9 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModel.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -32,6 +32,7 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Model\FormModel as CommonFormModel; +use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; use Symfony\Component\Console\Output\OutputInterface; From f7002eedafd0b9f35e38920d7fc7c5d0a607ac81 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 11:57:14 -0500 Subject: [PATCH 463/778] Fixed tests --- .../EventCollector/Builder/ConnectionBuilder.php | 4 ++++ .../Tests/Executioner/InactiveExecutionerTest.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php b/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php index d901e6b9a93..dbc8e8cdab2 100644 --- a/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php +++ b/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php @@ -34,6 +34,10 @@ class ConnectionBuilder */ public static function buildRestrictionsArray(array $events) { + // Reset restrictions + self::$connectionRestrictions = ['anchor' => []]; + + // Build the restrictions self::$eventTypes = array_fill_keys(array_keys($events), []); foreach ($events as $eventType => $typeEvents) { foreach ($typeEvents as $key => $event) { diff --git a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php index 9944b55a5e9..1d6a30ea4d9 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php @@ -138,7 +138,7 @@ public function testNextBatchOfContactsAreExecuted() ); $this->inactiveHelper->expects($this->exactly(2)) - ->method('removeContactsThatAreNotApplicable') + ->method('getEarliestInactiveDateTime') ->willReturn(new \DateTime()); $this->getExecutioner()->execute($campaign, $limiter); @@ -205,7 +205,7 @@ public function testValidationEvaluatesFoundEvents() ); $this->inactiveHelper->expects($this->exactly(2)) - ->method('removeContactsThatAreNotApplicable') + ->method('getEarliestInactiveDateTime') ->willReturn(new \DateTime()); $this->getExecutioner()->validate(1, $limiter); From 4ac53689664f4c2621018ca22ee2d1e38927bd8c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 12:08:43 -0500 Subject: [PATCH 464/778] Fixed tests --- .../CampaignBundle/Tests/Command/AbstractCampaignCommand.php | 1 + .../EventCollector/Accessor/Event/ActionAccessorTest.php | 2 +- .../EventCollector/Accessor/Event/ConditionAccessorTest.php | 2 +- .../EventCollector/Accessor/Event/DecisionAccessorTest.php | 2 +- .../Tests/EventCollector/Accessor/EventAccessorTest.php | 2 +- .../Tests/EventCollector/Builder/EventBuilderTest.php | 4 ++-- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php b/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php index 426c1d7faf8..c712bda75bf 100644 --- a/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php +++ b/app/bundles/CampaignBundle/Tests/Command/AbstractCampaignCommand.php @@ -11,6 +11,7 @@ namespace Mautic\CampaignBundle\Tests\Command; +use Doctrine\DBAL\Connection; use Mautic\CoreBundle\Test\MauticMysqlTestCase; class AbstractCampaignCommand extends MauticMysqlTestCase diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php index aaba56566bb..7f166d61ca0 100644 --- a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ActionAccessorTest.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\CampaignBundle\Tests\EventCollector\Event; +namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor\Event; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php index 38b77c775b5..58cc4a644c7 100644 --- a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/ConditionAccessorTest.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\CampaignBundle\Tests\EventCollector\Event; +namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor\Event; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php index 168c8f233e7..b87a5f360c8 100644 --- a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/Event/DecisionAccessorTest.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\CampaignBundle\Tests\EventCollector\Event; +namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor\Event; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php index 3461da313b4..84b1a94f79a 100644 --- a/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Accessor/EventAccessorTest.php @@ -9,7 +9,7 @@ * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html */ -namespace Mautic\CampaignBundle\Tests\EventCollector; +namespace Mautic\CampaignBundle\Tests\EventCollector\Accessor; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; diff --git a/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php b/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php index 45a8e2311ea..0d836235641 100644 --- a/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php +++ b/app/bundles/CampaignBundle/Tests/EventCollector/Builder/EventBuilderTest.php @@ -49,7 +49,7 @@ public function testConditionsAreConvertedToAccessor() ], ]; - $converted = EventBuilder::buildconditions($array); + $converted = EventBuilder::buildConditions($array); $this->assertCount(2, $converted); $this->assertInstanceOf(ConditionAccessor::class, $converted['some.condition']); @@ -69,7 +69,7 @@ public function testDecisionsAreConvertedToAccessor() ], ]; - $converted = EventBuilder::builddecisions($array); + $converted = EventBuilder::buildDecisions($array); $this->assertCount(2, $converted); $this->assertInstanceOf(DecisionAccessor::class, $converted['some.decision']); From 5231858d292b534dff6a436c0a6def4841c63a5c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 2 May 2018 18:14:13 -0500 Subject: [PATCH 465/778] Added marketing message campaign subscriber test --- .../Event/AbstractLogCollectionEvent.php | 21 +- .../CampaignBundle/Event/PendingEvent.php | 4 +- .../EventListener/CampaignSubscriber.php | 2 +- .../EventListener/CampaignSubscriberTest.php | 404 ++++++++++++++++++ 4 files changed, 421 insertions(+), 10 deletions(-) create mode 100644 app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index fcc81ae4197..4e85125c9f4 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -36,9 +36,9 @@ abstract class AbstractLogCollectionEvent extends \Symfony\Component\EventDispat protected $logs; /** - * @var array + * @var ArrayCollection|Lead[] */ - private $contacts = []; + private $contacts; /** * @var array @@ -54,9 +54,11 @@ abstract class AbstractLogCollectionEvent extends \Symfony\Component\EventDispat */ public function __construct(AbstractEventAccessor $config, Event $event, ArrayCollection $logs) { - $this->config = $config; - $this->event = $event; - $this->logs = $logs; + $this->config = $config; + $this->event = $event; + $this->logs = $logs; + $this->contacts = new ArrayCollection(); + $this->extractContacts(); } @@ -108,7 +110,11 @@ public function getContactIds() public function findLogByContactId($id) { if (!isset($this->logContactXref[$id])) { - throw new NoContactsFoundException(); + throw new NoContactsFoundException("$id not found"); + } + + if (!$this->logs->offsetExists($this->logContactXref[$id])) { + throw new NoContactsFoundException("$id was found in the xref table but no log was found"); } return $this->logs->get($this->logContactXref[$id]); @@ -119,8 +125,9 @@ private function extractContacts() /** @var LeadEventLog $log */ foreach ($this->logs as $log) { $contact = $log->getLead(); - $this->contacts[$log->getId()] = $contact; $this->logContactXref[$contact->getId()] = $log->getId(); + + $this->contacts->set($log->getId(), $contact); } } } diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php index 41269a68c17..1b6ed074050 100644 --- a/app/bundles/CampaignBundle/Event/PendingEvent.php +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -97,7 +97,7 @@ public function fail(LeadEventLog $log, $reason) $this->logChannel($log); - $this->failures->add($log); + $this->failures->set($log->getId(), $log); } /** @@ -237,7 +237,7 @@ private function passLog(LeadEventLog $log) $log->setIsScheduled(false) ->setDateTriggered($this->now); - $this->successful->add($log); + $this->successful->set($log->getId(), $log); } /** diff --git a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php index 97a7cd4219b..59d003bcdb4 100644 --- a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php @@ -262,7 +262,7 @@ private function passExecutedLogs(ArrayCollection $logs, PreferenceBuilder $chan // Remove those successfully executed from being processed again for lower priorities $channelPreferences->removeLogFromAllChannels($log); - // Find the Marketin Message log and pass it + // Find the Marketing Message log and pass it $mmLog = $this->pendingEvent->findLogByContactId($log->getLead()->getId()); // Pass these for the MM campaign event diff --git a/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php b/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php new file mode 100644 index 00000000000..f322689a7b7 --- /dev/null +++ b/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php @@ -0,0 +1,404 @@ +dispatcher = new EventDispatcher(); + + $this->messageModel = $this->getMockBuilder(MessageModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->messageModel->method('getChannels') + ->willReturn( + [ + 'email' => [ + 'campaignAction' => 'email.send', + 'campaignDecisionsSupported' => [ + 'email.open', + 'page.pagehit', + 'asset.download', + 'form.submit', + ], + 'lookupFormType' => 'email_list', + ], + 'sms' => [ + 'campaignAction' => 'sms.send_text_sms', + 'campaignDecisionsSupported' => [ + 'page.pagehit', + 'asset.download', + 'form.submit', + ], + 'lookupFormType' => 'sms_list', + 'repository' => 'MauticSmsBundle:Sms', + ], + ] + ); + + $this->messageModel->method('getMessageChannels') + ->willReturn( + [ + 'email' => [ + 'id' => 2, + 'channel' => 'email', + 'channel_id' => 2, + 'properties' => [], + ], + 'sms' => [ + 'id' => 1, + 'channel' => 'sms', + 'channel_id' => 1, + 'properties' => [], + ], + ] + ); + + $this->scheduler = $this->getMockBuilder(EventScheduler::class) + ->disableOriginalConstructor() + ->getMock(); + + $leadModel = $this->getMockBuilder(LeadModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $factory = $this->getMockBuilder(MauticFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->legacyDispatcher = new LegacyEventDispatcher( + $this->dispatcher, + $this->scheduler, + new NullLogger(), + $leadModel, + $factory + ); + + $this->eventDispatcher = new CampaignEventDispatcher( + $this->dispatcher, + new NullLogger(), + $this->scheduler, + $this->legacyDispatcher + ); + + $this->eventCollector = $this->getMockBuilder(EventCollector::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventCollector->method('getEventConfig') + ->willReturnCallback( + function (Event $event) { + switch ($event->getType()) { + case 'email.send': + return new ActionAccessor( + [ + 'label' => 'mautic.email.campaign.event.send', + 'description' => 'mautic.email.campaign.event.send_descr', + 'batchEventName' => EmailEvents::ON_CAMPAIGN_BATCH_ACTION, + 'formType' => 'emailsend_list', + 'formTypeOptions' => ['update_select' => 'campaignevent_properties_email', 'with_email_types' => true], + 'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList', + 'channel' => 'email', + 'channelIdField' => 'email', + ] + ); + + case 'sms.send_text_sms': + return new ActionAccessor( + [ + 'label' => 'mautic.campaign.sms.send_text_sms', + 'description' => 'mautic.campaign.sms.send_text_sms.tooltip', + 'eventName' => SmsEvents::ON_CAMPAIGN_TRIGGER_ACTION, + 'formType' => 'smssend_list', + 'formTypeOptions' => ['update_select' => 'campaignevent_properties_sms'], + 'formTheme' => 'MauticSmsBundle:FormTheme\SmsSendList', + 'timelineTemplate' => 'MauticSmsBundle:SubscribedEvents\Timeline:index.html.php', + 'channel' => 'sms', + 'channelIdField' => 'sms', + ] + ); + } + } + ); + + $this->translator = $this->getMockBuilder(Translator::class) + ->disableOriginalConstructor() + ->getMock(); + + $campaignSubscriber = new CampaignSubscriber( + $this->messageModel, + $this->eventDispatcher, + $this->eventCollector, + new NullLogger(), + $this->translator + ); + + $this->dispatcher->addSubscriber($campaignSubscriber); + $this->dispatcher->addListener(EmailEvents::ON_CAMPAIGN_BATCH_ACTION, [$this, 'sendMarketingMessageEmail']); + $this->dispatcher->addListener(SmsEvents::ON_CAMPAIGN_TRIGGER_ACTION, [$this, 'sendMarketingMessageSms']); + } + + public function testCorrectChannelIsUsed() + { + $event = $this->getEvent(); + $config = new ActionAccessor( + [ + 'label' => 'mautic.channel.message.send.marketing.message', + 'description' => 'mautic.channel.message.send.marketing.message.descr', + 'eventName' => ChannelEvents::ON_CAMPAIGN_TRIGGER_ACTION, + 'batchEventName' => ChannelEvents::ON_CAMPAIGN_BATCH_ACTION, + 'formType' => 'message_send', + 'formTheme' => 'MauticChannelBundle:FormTheme\MessageSend', + 'channel' => 'channel.message', + 'channelIdField' => 'marketingMessage', + 'connectionRestrictions' => [ + 'target' => [ + 'decision' => [ + 'email.open', + 'page.pagehit', + 'asset.download', + 'form.submit', + ], + ], + ], + 'timelineTemplate' => 'MauticChannelBundle:SubscribedEvents\Timeline:index.html.php', + 'timelineTemplateVars' => [ + 'messageSettings' => [], + ], + ] + ); + $logs = $this->getLogs(); + + $pendingEvent = new PendingEvent($config, $event, $logs); + + $this->dispatcher->dispatch(ChannelEvents::ON_CAMPAIGN_BATCH_ACTION, $pendingEvent); + + $this->assertCount(0, $pendingEvent->getFailures()); + + $successful = $pendingEvent->getSuccessful(); + + // SMS should be noted as DNC + $this->assertFalse(empty($successful->get(2)->getMetadata()['sms']['dnc'])); + + // Nothing recorded for success + $this->assertTrue(empty($successful->get(1)->getMetadata())); + } + + public function sendMarketingMessageEmail(PendingEvent $event) + { + $contacts = $event->getContacts(); + $logs = $event->getPending(); + $this->assertCount(1, $logs); + + if (1 === $contacts->first()->getId()) { + // Processing priority 1 for contact 1, let's fail this one so that SMS is used + $event->fail($logs->first(), 'just because'); + + return; + } + + if (2 === $contacts->first()->getId()) { + // Processing priority 1 for contact 2 so let's pass it + $event->pass($logs->first()); + + return; + } + } + + /** + * BC support for old campaign. + * + * @param CampaignExecutionEvent $event + */ + public function sendMarketingMessageSms(CampaignExecutionEvent $event) + { + $lead = $event->getLead(); + if (1 === $lead->getId()) { + $event->setResult(true); + + return; + } + + if (2 === $lead->getId()) { + $this->fail('Lead ID 2 is unsubscribed from SMS so this shouldn not have happened.'); + } + } + + /** + * @return Event|\PHPUnit_Framework_MockObject_MockObject + */ + private function getEvent() + { + $event = $this->getMockBuilder(Event::class) + ->setMethods(['getId']) + ->getMock(); + $event->method('getId') + ->willReturn(1); + $event->setEventType(Event::TYPE_ACTION); + $event->setType('message.send'); + $event->setChannel('channel.message'); + $event->setChannelId(1); + $event->setProperties( + [ + 'canvasSettings' => [ + 'droppedX' => '337', + 'droppedY' => '155', + ], + 'name' => '', + 'triggerMode' => 'immediate', + 'triggerDate' => null, + 'triggerInterval' => '1', + 'triggerIntervalUnit' => 'd', + 'anchor' => 'leadsource', + 'properties' => [ + 'marketingMessage' => '1', + ], + 'type' => 'message.send', + 'eventType' => 'action', + 'anchorEventType' => 'source', + 'campaignId' => '1', + '_token' => 'q7FpcDX7iye6fBuBzsqMvQWKqW75lcD77jSmuNAEDXg', + 'buttons' => [ + 'save' => '', + ], + 'marketingMessage' => '1', + ] + ); + $campaign = $this->getMockBuilder(Campaign::class) + ->getMock(); + $campaign->method('getId') + ->willReturn(1); + + $event->setCampaign($campaign); + + return $event; + } + + /** + * @return ArrayCollection + */ + private function getLogs() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->method('getId') + ->willReturn(1); + $lead->expects($this->once()) + ->method('getChannelRules') + ->willReturn( + [ + 'sms' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + 'email' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + ] + ); + + $log = $this->getMockBuilder(LeadEventLog::class) + ->setMethods(['getLead', 'getId']) + ->getMock(); + $log->method('getLead') + ->willReturn($lead); + $log->method('getId') + ->willReturn(1); + + $lead2 = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead2->method('getId') + ->willReturn(2); + $lead2->expects($this->once()) + ->method('getChannelRules') + ->willReturn( + [ + 'email' => [ + 'dnc' => DoNotContact::IS_CONTACTABLE, + ], + 'sms' => [ + 'dnc' => DoNotContact::UNSUBSCRIBED, + ], + ] + ); + + $log2 = $this->getMockBuilder(LeadEventLog::class) + ->setMethods(['getLead', 'getId']) + ->getMock(); + $log2->method('getLead') + ->willReturn($lead2); + $log2->method('getId') + ->willReturn(2); + + return new ArrayCollection([1 => $log, 2 => $log2]); + } +} From 3dd18a8923fc5763e6c94e1c54b7bf75a30a8fe3 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 09:38:57 -0500 Subject: [PATCH 466/778] Renamed class --- app/bundles/CampaignBundle/Config/config.php | 2 +- .../ContactFinder/InactiveContacts.php | 4 ++-- .../Executioner/Helper/InactiveHelper.php | 8 ++++---- .../Executioner/InactiveExecutioner.php | 18 +++++++++--------- .../ContactFinder/InactiveContactsTest.php | 8 ++++---- .../Executioner/InactiveExecutionerTest.php | 6 +++--- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 4b7032d15c8..9687aa57fe1 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -291,7 +291,7 @@ ], ], 'mautic.campaign.contact_finder.inactive' => [ - 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts::class, + 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder::class, 'arguments' => [ 'mautic.lead.repository.lead', 'mautic.campaign.repository.campaign', diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php index b1713f03773..0cfc4405149 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php @@ -20,7 +20,7 @@ use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\LoggerInterface; -class InactiveContacts +class InactiveContactFinder { /** * @var LeadRepository @@ -48,7 +48,7 @@ class InactiveContacts private $campaignMemberDatesAdded; /** - * InactiveContacts constructor. + * InactiveContactFinder constructor. * * @param LeadRepository $leadRepository * @param CampaignRepository $campaignRepository diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index 86c509345ea..af5d5a1fa94 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -15,7 +15,7 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\EventRepository; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; -use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Psr\Log\LoggerInterface; @@ -27,7 +27,7 @@ class InactiveHelper private $scheduler; /** - * @var InactiveContacts + * @var InactiveContactFinder */ private $inactiveContacts; @@ -55,13 +55,13 @@ class InactiveHelper * InactiveHelper constructor. * * @param EventScheduler $scheduler - * @param InactiveContacts $inactiveContacts + * @param InactiveContactFinder $inactiveContacts * @param LeadEventLogRepository $eventLogRepository * @param LoggerInterface $logger */ public function __construct( EventScheduler $scheduler, - InactiveContacts $inactiveContacts, + InactiveContactFinder $inactiveContacts, LeadEventLogRepository $eventLogRepository, EventRepository $eventRepository, LoggerInterface $logger diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index c251622f356..71dd74eb735 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -14,7 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFoundException; @@ -76,7 +76,7 @@ class InactiveExecutioner implements ExecutionerInterface private $counter; /** - * @var InactiveContacts + * @var InactiveContactFinder */ private $inactiveContacts; @@ -98,15 +98,15 @@ class InactiveExecutioner implements ExecutionerInterface /** * InactiveExecutioner constructor. * - * @param InactiveContacts $inactiveContacts - * @param LoggerInterface $logger - * @param TranslatorInterface $translator - * @param EventScheduler $scheduler - * @param InactiveHelper $helper - * @param EventExecutioner $executioner + * @param InactiveContactFinder $inactiveContacts + * @param LoggerInterface $logger + * @param TranslatorInterface $translator + * @param EventScheduler $scheduler + * @param InactiveHelper $helper + * @param EventExecutioner $executioner */ public function __construct( - InactiveContacts $inactiveContacts, + InactiveContactFinder $inactiveContacts, LoggerInterface $logger, TranslatorInterface $translator, EventScheduler $scheduler, diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php index c05c5b988b3..e226661f74e 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php @@ -15,14 +15,14 @@ use Mautic\CampaignBundle\Entity\CampaignRepository; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadRepository as CampaignLeadRepository; -use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\NullLogger; -class InactiveContactsTest extends \PHPUnit_Framework_TestCase +class InactiveContactFinderTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPUnit_Framework_MockObject_MockObject|LeadRepository @@ -110,11 +110,11 @@ public function testContactsAreFoundAndStoredInCampaignMemberDatesAdded() } /** - * @return InactiveContacts + * @return InactiveContactFinder */ private function getContactFinder() { - return new InactiveContacts( + return new InactiveContactFinder( $this->leadRepository, $this->campaignRepository, $this->campaignLeadRepository, diff --git a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php index 1d6a30ea4d9..30a9a0f8b3f 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php @@ -14,7 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\InactiveContactFinder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\EventExecutioner; use Mautic\CampaignBundle\Executioner\Helper\InactiveHelper; @@ -27,7 +27,7 @@ class InactiveExecutionerTest extends \PHPUnit_Framework_TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject|InactiveContacts + * @var \PHPUnit_Framework_MockObject_MockObject|InactiveContactFinder */ private $inactiveContactFinder; @@ -53,7 +53,7 @@ class InactiveExecutionerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->inactiveContactFinder = $this->getMockBuilder(InactiveContacts::class) + $this->inactiveContactFinder = $this->getMockBuilder(InactiveContactFinder::class) ->disableOriginalConstructor() ->getMock(); From 9480c133fa497c94b71318eac5637c320aa06bc4 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 09:40:59 -0500 Subject: [PATCH 467/778] Renamed class --- app/bundles/CampaignBundle/Config/config.php | 2 +- ...iveContacts.php => InactiveContactFinder.php} | 0 ...koffContacts.php => KickoffContactFinder.php} | 4 ++-- .../Executioner/KickoffExecutioner.php | 16 ++++++++-------- ...ctsTest.php => InactiveContactFinderTest.php} | 0 ...actsTest.php => KickoffContactFinderTest.php} | 8 ++++---- .../Tests/Executioner/KickoffExecutionerTest.php | 6 +++--- 7 files changed, 18 insertions(+), 18 deletions(-) rename app/bundles/CampaignBundle/Executioner/ContactFinder/{InactiveContacts.php => InactiveContactFinder.php} (100%) rename app/bundles/CampaignBundle/Executioner/ContactFinder/{KickoffContacts.php => KickoffContactFinder.php} (97%) rename app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/{InactiveContactsTest.php => InactiveContactFinderTest.php} (100%) rename app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/{KickoffContactsTest.php => KickoffContactFinderTest.php} (95%) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 9687aa57fe1..b23f6b4887f 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -277,7 +277,7 @@ ], 'execution' => [ 'mautic.campaign.contact_finder.kickoff' => [ - 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts::class, + 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContactFinder::class, 'arguments' => [ 'mautic.lead.repository.lead', 'mautic.campaign.repository.campaign', diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php similarity index 100% rename from app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContacts.php rename to app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php similarity index 97% rename from app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php rename to app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php index 91e6a15cec5..9a31f14df68 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php @@ -19,7 +19,7 @@ use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\LoggerInterface; -class KickoffContacts +class KickoffContactFinder { /** * @var LeadRepository @@ -37,7 +37,7 @@ class KickoffContacts private $logger; /** - * KickoffContacts constructor. + * KickoffContactFinder constructor. * * @param LeadRepository $leadRepository * @param CampaignRepository $campaignRepository diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index 6b693e9aa25..4dafd13fa17 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -14,7 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContactFinder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFoundException; @@ -51,7 +51,7 @@ class KickoffExecutioner implements ExecutionerInterface private $logger; /** - * @var KickoffContacts + * @var KickoffContactFinder */ private $kickoffContacts; @@ -93,15 +93,15 @@ class KickoffExecutioner implements ExecutionerInterface /** * KickoffExecutioner constructor. * - * @param LoggerInterface $logger - * @param KickoffContacts $kickoffContacts - * @param TranslatorInterface $translator - * @param EventExecutioner $executioner - * @param EventScheduler $scheduler + * @param LoggerInterface $logger + * @param KickoffContactFinder $kickoffContacts + * @param TranslatorInterface $translator + * @param EventExecutioner $executioner + * @param EventScheduler $scheduler */ public function __construct( LoggerInterface $logger, - KickoffContacts $kickoffContacts, + KickoffContactFinder $kickoffContacts, TranslatorInterface $translator, EventExecutioner $executioner, EventScheduler $scheduler diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactFinderTest.php similarity index 100% rename from app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactsTest.php rename to app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactFinderTest.php diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactFinderTest.php similarity index 95% rename from app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php rename to app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactFinderTest.php index 972c585102c..5788aa1ec93 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactsTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/KickoffContactFinderTest.php @@ -13,14 +13,14 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\CampaignRepository; -use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContactFinder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\NullLogger; -class KickoffContactsTest extends \PHPUnit_Framework_TestCase +class KickoffContactFinderTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPUnit_Framework_MockObject_MockObject|LeadRepository @@ -91,11 +91,11 @@ public function testArrayCollectionIsReturnedForFoundContacts() } /** - * @return KickoffContacts + * @return KickoffContactFinder */ private function getContactFinder() { - return new KickoffContacts( + return new KickoffContactFinder( $this->leadRepository, $this->campaignRepository, new NullLogger() diff --git a/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php index c13eedff880..85f4bed5a8e 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php @@ -14,7 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\Event; -use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\KickoffContactFinder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\EventExecutioner; use Mautic\CampaignBundle\Executioner\KickoffExecutioner; @@ -26,7 +26,7 @@ class KickoffExecutionerTest extends \PHPUnit_Framework_TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject|KickoffContacts + * @var \PHPUnit_Framework_MockObject_MockObject|KickoffContactFinder */ private $kickoffContacts; @@ -47,7 +47,7 @@ class KickoffExecutionerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->kickoffContacts = $this->getMockBuilder(KickoffContacts::class) + $this->kickoffContacts = $this->getMockBuilder(KickoffContactFinder::class) ->disableOriginalConstructor() ->getMock(); From bf8e744c2bb19575b7bfba5bd991bab5d22c0ff3 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 09:42:09 -0500 Subject: [PATCH 468/778] Renamed class --- app/bundles/CampaignBundle/Config/config.php | 2 +- .../{ScheduledContacts.php => ScheduledContactFinder.php} | 4 ++-- .../CampaignBundle/Executioner/ScheduledExecutioner.php | 8 ++++---- ...heduledContactsTest.php => ScheduledContactFinder.php} | 8 ++++---- .../Tests/Executioner/ScheduledExecutionerTest.php | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename app/bundles/CampaignBundle/Executioner/ContactFinder/{ScheduledContacts.php => ScheduledContactFinder.php} (95%) rename app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/{ScheduledContactsTest.php => ScheduledContactFinder.php} (93%) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index b23f6b4887f..fa6f96a7416 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -285,7 +285,7 @@ ], ], 'mautic.campaign.contact_finder.scheduled' => [ - 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts::class, + 'class' => \Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContactFinder::class, 'arguments' => [ 'mautic.lead.repository.lead', ], diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContactFinder.php similarity index 95% rename from app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php rename to app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContactFinder.php index a00e261d17c..5a74f221d94 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContacts.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/ScheduledContactFinder.php @@ -15,7 +15,7 @@ use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\LeadBundle\Entity\LeadRepository; -class ScheduledContacts +class ScheduledContactFinder { /** * @var LeadRepository @@ -23,7 +23,7 @@ class ScheduledContacts private $leadRepository; /** - * ScheduledContacts constructor. + * ScheduledContactFinder constructor. * * @param LeadRepository $leadRepository */ diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index c3bb5608ae5..74a53c7c28e 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -16,7 +16,7 @@ use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContactFinder; use Mautic\CampaignBundle\Executioner\Exception\NoEventsFoundException; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; @@ -55,7 +55,7 @@ class ScheduledExecutioner implements ExecutionerInterface private $scheduler; /** - * @var ScheduledContacts + * @var ScheduledContactFinder */ private $scheduledContacts; @@ -102,7 +102,7 @@ class ScheduledExecutioner implements ExecutionerInterface * @param TranslatorInterface $translator * @param EventExecutioner $executioner * @param EventScheduler $scheduler - * @param ScheduledContacts $scheduledContacts + * @param ScheduledContactFinder $scheduledContacts */ public function __construct( LeadEventLogRepository $repository, @@ -110,7 +110,7 @@ public function __construct( TranslatorInterface $translator, EventExecutioner $executioner, EventScheduler $scheduler, - ScheduledContacts $scheduledContacts + ScheduledContactFinder $scheduledContacts ) { $this->repo = $repository; $this->logger = $logger; diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinder.php similarity index 93% rename from app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php rename to app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinder.php index 8b49bf7d743..8f75b9b89cf 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactsTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinder.php @@ -13,11 +13,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\LeadEventLog; -use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContactFinder; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; -class ScheduledContactsTest extends \PHPUnit_Framework_TestCase +class ScheduledContactFinderTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPUnit_Framework_MockObject_MockObject|LeadRepository @@ -83,11 +83,11 @@ public function testHydratedLeadsFromRepositoryAreFoundAndPushedIntoLogs() } /** - * @return ScheduledContacts + * @return ScheduledContactFinder */ private function getContactFinder() { - return new ScheduledContacts( + return new ScheduledContactFinder( $this->leadRepository ); } diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php index 1c8b10fd687..72f751908b2 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php @@ -17,7 +17,7 @@ use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContacts; +use Mautic\CampaignBundle\Executioner\ContactFinder\ScheduledContactFinder; use Mautic\CampaignBundle\Executioner\EventExecutioner; use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; @@ -47,7 +47,7 @@ class ScheduledExecutionerTest extends \PHPUnit_Framework_TestCase private $scheduler; /** - * @var \PHPUnit_Framework_MockObject_MockObject|ScheduledContacts + * @var \PHPUnit_Framework_MockObject_MockObject|ScheduledContactFinder */ private $contactFinder; @@ -69,7 +69,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->contactFinder = $this->getMockBuilder(ScheduledContacts::class) + $this->contactFinder = $this->getMockBuilder(ScheduledContactFinder::class) ->disableOriginalConstructor() ->getMock(); } From dea6fb5a3c6b21329cbcc1494f6b97e1d7256fc7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 09:43:38 -0500 Subject: [PATCH 469/778] Renamed variable --- .../Executioner/Helper/InactiveHelper.php | 18 ++++++------- .../Executioner/InactiveExecutioner.php | 26 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index af5d5a1fa94..806cd7aa12b 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -29,7 +29,7 @@ class InactiveHelper /** * @var InactiveContactFinder */ - private $inactiveContacts; + private $inactiveContactFinder; /** * @var LeadEventLogRepository @@ -55,22 +55,22 @@ class InactiveHelper * InactiveHelper constructor. * * @param EventScheduler $scheduler - * @param InactiveContactFinder $inactiveContacts + * @param InactiveContactFinder $inactiveContactFinder * @param LeadEventLogRepository $eventLogRepository * @param LoggerInterface $logger */ public function __construct( EventScheduler $scheduler, - InactiveContactFinder $inactiveContacts, + InactiveContactFinder $inactiveContactFinder, LeadEventLogRepository $eventLogRepository, EventRepository $eventRepository, LoggerInterface $logger ) { - $this->scheduler = $scheduler; - $this->inactiveContacts = $inactiveContacts; - $this->eventLogRepository = $eventLogRepository; - $this->eventRepository = $eventRepository; - $this->logger = $logger; + $this->scheduler = $scheduler; + $this->inactiveContactFinder = $inactiveContactFinder; + $this->eventLogRepository = $eventLogRepository; + $this->eventRepository = $eventRepository; + $this->logger = $logger; } /** @@ -202,6 +202,6 @@ private function getLastActiveDates($lastActiveEventId, array $contactIds) return $this->eventLogRepository->getDatesExecuted($lastActiveEventId, $contactIds); } - return $this->inactiveContacts->getDatesAdded(); + return $this->inactiveContactFinder->getDatesAdded(); } } diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 71dd74eb735..6dacf22ffab 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -78,7 +78,7 @@ class InactiveExecutioner implements ExecutionerInterface /** * @var InactiveContactFinder */ - private $inactiveContacts; + private $inactiveContactFinder; /** * @var ArrayCollection @@ -98,7 +98,7 @@ class InactiveExecutioner implements ExecutionerInterface /** * InactiveExecutioner constructor. * - * @param InactiveContactFinder $inactiveContacts + * @param InactiveContactFinder $inactiveContactFinder * @param LoggerInterface $logger * @param TranslatorInterface $translator * @param EventScheduler $scheduler @@ -106,19 +106,19 @@ class InactiveExecutioner implements ExecutionerInterface * @param EventExecutioner $executioner */ public function __construct( - InactiveContactFinder $inactiveContacts, + InactiveContactFinder $inactiveContactFinder, LoggerInterface $logger, TranslatorInterface $translator, EventScheduler $scheduler, InactiveHelper $helper, EventExecutioner $executioner ) { - $this->inactiveContacts = $inactiveContacts; - $this->logger = $logger; - $this->translator = $translator; - $this->scheduler = $scheduler; - $this->helper = $helper; - $this->executioner = $executioner; + $this->inactiveContactFinder = $inactiveContactFinder; + $this->logger = $logger; + $this->translator = $translator; + $this->scheduler = $scheduler; + $this->helper = $helper; + $this->executioner = $executioner; } /** @@ -227,7 +227,7 @@ private function prepareForExecution() throw new NoEventsFoundException(); } - $totalContacts = $this->inactiveContacts->getContactCount($this->campaign->getId(), $this->decisions->getKeys(), $this->limiter); + $totalContacts = $this->inactiveContactFinder->getContactCount($this->campaign->getId(), $this->decisions->getKeys(), $this->limiter); $this->output->writeln( $this->translator->trans( 'mautic.campaign.trigger.decision_count_analyzed', @@ -271,7 +271,7 @@ private function executeEvents() $this->startAtContactId = $this->limiter->getMinContactId() ?: 0; // Ge the first batch of contacts - $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $this->startAtContactId, $this->limiter); + $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->startAtContactId, $this->limiter); // Loop over all contacts till we've processed all those applicable for this decision while ($contacts->count()) { @@ -298,7 +298,7 @@ private function executeEvents() } // Clear contacts from memory - $this->inactiveContacts->clear(); + $this->inactiveContactFinder->clear(); if ($this->limiter->getContactId()) { // No use making another call @@ -308,7 +308,7 @@ private function executeEvents() $this->logger->debug('CAMPAIGN: Fetching the next batch of inactive contacts after contact ID '.$startAtContactId); // Get the next batch, starting with the max contact ID - $contacts = $this->inactiveContacts->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); + $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); } } } From 8421869cfa7ae037b8febb8141e3193f57a8d6b1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 09:45:47 -0500 Subject: [PATCH 470/778] Renamed variable --- .../Executioner/KickoffExecutioner.php | 24 +++++++++---------- .../Executioner/KickoffExecutionerTest.php | 12 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index 4dafd13fa17..a56b759df5d 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -53,7 +53,7 @@ class KickoffExecutioner implements ExecutionerInterface /** * @var KickoffContactFinder */ - private $kickoffContacts; + private $kickoffContactFinder; /** * @var TranslatorInterface @@ -94,23 +94,23 @@ class KickoffExecutioner implements ExecutionerInterface * KickoffExecutioner constructor. * * @param LoggerInterface $logger - * @param KickoffContactFinder $kickoffContacts + * @param KickoffContactFinder $kickoffContactFinder * @param TranslatorInterface $translator * @param EventExecutioner $executioner * @param EventScheduler $scheduler */ public function __construct( LoggerInterface $logger, - KickoffContactFinder $kickoffContacts, + KickoffContactFinder $kickoffContactFinder, TranslatorInterface $translator, EventExecutioner $executioner, EventScheduler $scheduler ) { - $this->logger = $logger; - $this->kickoffContacts = $kickoffContacts; - $this->translator = $translator; - $this->executioner = $executioner; - $this->scheduler = $scheduler; + $this->logger = $logger; + $this->kickoffContactFinder = $kickoffContactFinder; + $this->translator = $translator; + $this->executioner = $executioner; + $this->scheduler = $scheduler; } /** @@ -163,7 +163,7 @@ private function prepareForExecution() $totalRootEvents = $this->rootEvents->count(); $this->logger->debug('CAMPAIGN: Processing the following events: '.implode(', ', $this->rootEvents->getKeys())); - $totalContacts = $this->kickoffContacts->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->limiter); + $totalContacts = $this->kickoffContactFinder->getContactCount($this->campaign->getId(), $this->rootEvents->getKeys(), $this->limiter); $totalKickoffEvents = $totalRootEvents * $totalContacts; $this->output->writeln( @@ -198,7 +198,7 @@ private function executeOrScheduleEvent() $this->counter->advanceEventCount($this->rootEvents->count()); // Loop over contacts until the entire campaign is executed - $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->limiter); + $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); while ($contacts->count()) { /** @var Event $event */ foreach ($this->rootEvents as $event) { @@ -223,7 +223,7 @@ private function executeOrScheduleEvent() $this->executioner->executeForContacts($event, $contacts, $this->counter); } - $this->kickoffContacts->clear(); + $this->kickoffContactFinder->clear(); if ($this->limiter->getContactId()) { // No use making another call @@ -231,7 +231,7 @@ private function executeOrScheduleEvent() } // Get the next batch - $contacts = $this->kickoffContacts->getContacts($this->campaign->getId(), $this->limiter); + $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); } } } diff --git a/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php index 85f4bed5a8e..a57937ed239 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/KickoffExecutionerTest.php @@ -28,7 +28,7 @@ class KickoffExecutionerTest extends \PHPUnit_Framework_TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject|KickoffContactFinder */ - private $kickoffContacts; + private $kickoffContactFinder; /** * @var \PHPUnit_Framework_MockObject_MockObject|Translator @@ -47,7 +47,7 @@ class KickoffExecutionerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->kickoffContacts = $this->getMockBuilder(KickoffContactFinder::class) + $this->kickoffContactFinder = $this->getMockBuilder(KickoffContactFinder::class) ->disableOriginalConstructor() ->getMock(); @@ -66,7 +66,7 @@ protected function setUp() public function testNoContactsResultInEmptyResults() { - $this->kickoffContacts->expects($this->once()) + $this->kickoffContactFinder->expects($this->once()) ->method('getContactCount') ->willReturn(0); @@ -85,11 +85,11 @@ public function testNoContactsResultInEmptyResults() public function testEventsAreScheduledAndExecuted() { - $this->kickoffContacts->expects($this->once()) + $this->kickoffContactFinder->expects($this->once()) ->method('getContactCount') ->willReturn(2); - $this->kickoffContacts->expects($this->exactly(3)) + $this->kickoffContactFinder->expects($this->exactly(3)) ->method('getContacts') ->willReturnOnConsecutiveCalls( new ArrayCollection([3 => new Lead()]), @@ -137,7 +137,7 @@ private function getExecutioner() { return new KickoffExecutioner( new NullLogger(), - $this->kickoffContacts, + $this->kickoffContactFinder, $this->translator, $this->executioner, $this->scheduler From 9c5bf4c500576421333661739c80cbadcc837056 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 09:46:21 -0500 Subject: [PATCH 471/778] Renamed variable --- .../Executioner/ScheduledExecutioner.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index 74a53c7c28e..7819b13ab4c 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -57,7 +57,7 @@ class ScheduledExecutioner implements ExecutionerInterface /** * @var ScheduledContactFinder */ - private $scheduledContacts; + private $scheduledContactFinder; /** * @var Campaign @@ -102,7 +102,7 @@ class ScheduledExecutioner implements ExecutionerInterface * @param TranslatorInterface $translator * @param EventExecutioner $executioner * @param EventScheduler $scheduler - * @param ScheduledContactFinder $scheduledContacts + * @param ScheduledContactFinder $scheduledContactFinder */ public function __construct( LeadEventLogRepository $repository, @@ -110,14 +110,14 @@ public function __construct( TranslatorInterface $translator, EventExecutioner $executioner, EventScheduler $scheduler, - ScheduledContactFinder $scheduledContacts + ScheduledContactFinder $scheduledContactFinder ) { - $this->repo = $repository; - $this->logger = $logger; - $this->translator = $translator; - $this->executioner = $executioner; - $this->scheduler = $scheduler; - $this->scheduledContacts = $scheduledContacts; + $this->repo = $repository; + $this->logger = $logger; + $this->translator = $translator; + $this->executioner = $executioner; + $this->scheduler = $scheduler; + $this->scheduledContactFinder = $scheduledContactFinder; } /** @@ -286,7 +286,7 @@ private function executeScheduled($eventId, \DateTime $now) { $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); while ($logs->count()) { - $this->scheduledContacts->hydrateContacts($logs); + $this->scheduledContactFinder->hydrateContacts($logs); $event = $logs->first()->getEvent(); $this->progressBar->advance($logs->count()); @@ -299,7 +299,7 @@ private function executeScheduled($eventId, \DateTime $now) $this->executioner->executeLogs($event, $logs, $this->counter); // Get next batch - $this->scheduledContacts->clear(); + $this->scheduledContactFinder->clear(); $logs = $this->repo->getScheduled($eventId, $this->now, $this->limiter); } } From 6b30e1b21f634994b6c026e169891b6aaf54186e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 11:07:39 -0500 Subject: [PATCH 472/778] Fixed filename --- ...{ScheduledContactFinder.php => ScheduledContactFinderTest.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/{ScheduledContactFinder.php => ScheduledContactFinderTest.php} (100%) diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinder.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinderTest.php similarity index 100% rename from app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinder.php rename to app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/ScheduledContactFinderTest.php From 68053d33c864353aaa5a520068ae9347e3a2fd24 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 14:44:56 -0500 Subject: [PATCH 473/778] Added back notifying user of failed campaign action --- app/bundles/CampaignBundle/Config/config.php | 11 + .../Dispatcher/EventDispatcher.php | 21 +- .../Dispatcher/LegacyEventDispatcher.php | 20 +- .../Executioner/Helper/NotificationHelper.php | 112 +++++++++ .../CampaignBundle/Model/EventModel.php | 105 +-------- .../CampaignBundle/Model/LegacyEventModel.php | 66 +++++- .../Dispatcher/EventDispatcherTest.php | 17 +- .../Dispatcher/LegacyEventDispatcherTest.php | 18 +- .../Tests/Helper/NotificationHelperTest.php | 220 ++++++++++++++++++ app/bundles/UserBundle/Model/UserModel.php | 15 ++ 10 files changed, 489 insertions(+), 116 deletions(-) create mode 100644 app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php create mode 100644 app/bundles/CampaignBundle/Tests/Helper/NotificationHelperTest.php diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index fa6f96a7416..44321a84b71 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -305,6 +305,7 @@ 'event_dispatcher', 'monolog.logger.mautic', 'mautic.campaign.scheduler', + 'mautic.campaign.helper.notification', 'mautic.campaign.legacy_event_dispatcher', ], ], @@ -437,6 +438,15 @@ 'mautic.campaign.helper.removed_contact_tracker' => [ 'class' => \Mautic\CampaignBundle\Helper\RemovedContactTracker::class, ], + 'mautic.campaign.helper.notification' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Helper\NotificationHelper::class, + 'arguments' => [ + 'mautic.user.model.user', + 'mautic.core.model.notification', + 'translator', + 'router', + ], + ], // @deprecated 2.13.0 for BC support; to be removed in 3.0 'mautic.campaign.legacy_event_dispatcher' => [ 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher::class, @@ -445,6 +455,7 @@ 'mautic.campaign.scheduler', 'monolog.logger.mautic', 'mautic.lead.model.lead', + 'mautic.campaign.helper.notification', 'mautic.factory', ], ], diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php index 4159d4427c9..dc86d4cc863 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php @@ -28,6 +28,7 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException; +use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Psr\Log\LoggerInterface; @@ -53,6 +54,11 @@ class EventDispatcher */ private $scheduler; + /** + * @var NotificationHelper + */ + private $notificationHelper; + /** * @var LegacyEventDispatcher */ @@ -63,18 +69,22 @@ class EventDispatcher * * @param EventDispatcherInterface $dispatcher * @param LoggerInterface $logger + * @param EventScheduler $scheduler + * @param NotificationHelper $notificationHelper * @param LegacyEventDispatcher $legacyDispatcher */ public function __construct( EventDispatcherInterface $dispatcher, LoggerInterface $logger, EventScheduler $scheduler, + NotificationHelper $notificationHelper, LegacyEventDispatcher $legacyDispatcher ) { - $this->dispatcher = $dispatcher; - $this->logger = $logger; - $this->scheduler = $scheduler; - $this->legacyDispatcher = $legacyDispatcher; + $this->dispatcher = $dispatcher; + $this->logger = $logger; + $this->scheduler = $scheduler; + $this->notificationHelper = $notificationHelper; + $this->legacyDispatcher = $legacyDispatcher; } /** @@ -195,6 +205,7 @@ private function dispatchExecutedEvent(AbstractEventAccessor $config, Event $eve */ private function dispatchedFailedEvent(AbstractEventAccessor $config, ArrayCollection $logs) { + /** @var LeadEventLog $log */ foreach ($logs as $log) { $this->logger->debug( 'CAMPAIGN: '.ucfirst($log->getEvent()->getEventType()).' ID# '.$log->getEvent()->getId().' for contact ID# '.$log->getLead()->getId() @@ -206,6 +217,8 @@ private function dispatchedFailedEvent(AbstractEventAccessor $config, ArrayColle CampaignEvents::ON_EVENT_FAILED, new FailedEvent($config, $log) ); + + $this->notificationHelper->notifyOfFailure($log->getLead(), $log->getEvent()); } } diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 0315c769a3f..3234c718c2f 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -24,6 +24,7 @@ use Mautic\CampaignBundle\Event\FailedEvent; use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Factory\MauticFactory; use Mautic\LeadBundle\Model\LeadModel; @@ -59,6 +60,11 @@ class LegacyEventDispatcher */ private $leadModel; + /** + * @var NotificationHelper + */ + private $notificationHelper; + /** * @var MauticFactory */ @@ -77,13 +83,15 @@ public function __construct( EventScheduler $scheduler, LoggerInterface $logger, LeadModel $leadModel, + NotificationHelper $notificationHelper, MauticFactory $factory ) { - $this->dispatcher = $dispatcher; - $this->scheduler = $scheduler; - $this->logger = $logger; - $this->leadModel = $leadModel; - $this->factory = $factory; + $this->dispatcher = $dispatcher; + $this->scheduler = $scheduler; + $this->logger = $logger; + $this->leadModel = $leadModel; + $this->notificationHelper = $notificationHelper; + $this->factory = $factory; } /** @@ -339,6 +347,8 @@ private function dispatchFailedEvent(AbstractEventAccessor $config, LeadEventLog CampaignEvents::ON_EVENT_FAILED, new FailedEvent($config, $log) ); + + $this->notificationHelper->notifyOfFailure($log->getLead(), $log->getEvent()); } /** diff --git a/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php new file mode 100644 index 00000000000..220493185a6 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php @@ -0,0 +1,112 @@ +userModel = $userModel; + $this->notificationModel = $notificationModel; + $this->translator = $translator; + $this->router = $router; + } + + /** + * @param Lead $contact + * @param Event $event + * @param $header + */ + public function notifyOfFailure(Lead $contact, Event $event) + { + $user = $this->getUser($contact, $event); + if (!$user || !$user->getId()) { + return; + } + + $this->notificationModel->addNotification( + $event->getCampaign()->getName().' / '.$event->getName(), + 'error', + false, + $this->translator->trans( + 'mautic.campaign.event.failed', + [ + '%contact%' => ''.$contact->getPrimaryIdentifier().'', + ] + ), + null, + null, + $user + ); + } + + /** + * @param Lead $contact + * @param Event $event + * + * @return User|null|object + */ + private function getUser(Lead $contact, Event $event) + { + // Default is to notify the contact owner + if ($owner = $contact->getOwner()) { + return $owner; + } + + // If the contact doesn't have an owner, notify the one that created the campaign + if ($campaignCreator = $event->getCampaign()->getCreatedBy()) { + if ($owner = $this->userModel->getEntity($campaignCreator)) { + return $owner; + } + } + + // If all else fails, notifiy a system admins + return $this->userModel->getSystemAdministrator(); + } +} diff --git a/app/bundles/CampaignBundle/Model/EventModel.php b/app/bundles/CampaignBundle/Model/EventModel.php index 3edea970197..acf2d5babb3 100644 --- a/app/bundles/CampaignBundle/Model/EventModel.php +++ b/app/bundles/CampaignBundle/Model/EventModel.php @@ -13,83 +13,14 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLogRepository; -use Mautic\CampaignBundle\EventCollector\EventCollector; -use Mautic\CampaignBundle\Executioner\DecisionExecutioner; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; -use Mautic\CampaignBundle\Executioner\EventExecutioner; -use Mautic\CampaignBundle\Executioner\InactiveExecutioner; -use Mautic\CampaignBundle\Executioner\KickoffExecutioner; -use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CoreBundle\Helper\Chart\ChartQuery; use Mautic\CoreBundle\Helper\Chart\LineChart; -use Mautic\CoreBundle\Helper\IpLookupHelper; -use Mautic\CoreBundle\Model\NotificationModel; -use Mautic\LeadBundle\Entity\Lead; -use Mautic\LeadBundle\Model\LeadModel; -use Mautic\UserBundle\Model\UserModel; /** - * Class EventModel - * {@inheritdoc} + * Class EventModel. */ class EventModel extends LegacyEventModel { - /** - * @var UserModel - */ - protected $userModel; - - /** - * @var NotificationModel - */ - protected $notificationModel; - - /** - * EventModel constructor. - * - * @param IpLookupHelper $ipLookupHelper - * @param LeadModel $leadModel - * @param UserModel $userModel - * @param NotificationModel $notificationModel - * @param CampaignModel $campaignModel - * @param DecisionExecutioner $decisionExecutioner - * @param KickoffExecutioner $kickoffExecutioner - * @param ScheduledExecutioner $scheduledExecutioner - * @param InactiveExecutioner $inactiveExecutioner - * @param EventExecutioner $eventExecutioner - * @param EventDispatcher $eventDispatcher - */ - public function __construct( - UserModel $userModel, - NotificationModel $notificationModel, - CampaignModel $campaignModel, - LeadModel $leadModel, - IpLookupHelper $ipLookupHelper, - DecisionExecutioner $decisionExecutioner, - KickoffExecutioner $kickoffExecutioner, - ScheduledExecutioner $scheduledExecutioner, - InactiveExecutioner $inactiveExecutioner, - EventExecutioner $eventExecutioner, - EventDispatcher $eventDispatcher, - EventCollector $eventCollector - ) { - $this->userModel = $userModel; - $this->notificationModel = $notificationModel; - - parent::__construct( - $campaignModel, - $leadModel, - $ipLookupHelper, - $decisionExecutioner, - $kickoffExecutioner, - $scheduledExecutioner, - $inactiveExecutioner, - $eventExecutioner, - $eventDispatcher, - $eventCollector - ); - } - /** * {@inheritdoc} * @@ -211,38 +142,4 @@ public function getEventLineChartData($unit, \DateTime $dateFrom, \DateTime $dat return $chart->render(); } - - /** - * @param Lead $lead - * @param $campaignCreatedBy - * @param $header - */ - public function notifyOfFailure(Lead $lead, $campaignCreatedBy, $header) - { - // Notify the lead owner if there is one otherwise campaign creator that there was a failure - if (!$owner = $lead->getOwner()) { - $ownerId = (int) $campaignCreatedBy; - $owner = $this->userModel->getEntity($ownerId); - } - - if ($owner && $owner->getId()) { - $this->notificationModel->addNotification( - $header, - 'error', - false, - $this->translator->trans( - 'mautic.campaign.event.failed', - [ - '%contact%' => ''.$lead->getPrimaryIdentifier().'', - ] - ), - null, - null, - $owner - ); - } - } } diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModel.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php index 48b4cc9dad9..db25be5c238 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModel.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -24,6 +24,7 @@ use Mautic\CampaignBundle\Executioner\DecisionExecutioner; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; use Mautic\CampaignBundle\Executioner\EventExecutioner; +use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\InactiveExecutioner; use Mautic\CampaignBundle\Executioner\KickoffExecutioner; use Mautic\CampaignBundle\Executioner\Result\Counter; @@ -32,8 +33,10 @@ use Mautic\CoreBundle\Helper\DateTimeHelper; use Mautic\CoreBundle\Helper\IpLookupHelper; use Mautic\CoreBundle\Model\FormModel as CommonFormModel; +use Mautic\CoreBundle\Model\NotificationModel; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; +use Mautic\UserBundle\Model\UserModel; use Symfony\Component\Console\Output\OutputInterface; /** @@ -76,6 +79,11 @@ class LegacyEventModel extends CommonFormModel */ private $eventCollector; + /** + * @var NotificationHelper + */ + private $notificationHelper; + /** * @var */ @@ -97,15 +105,33 @@ class LegacyEventModel extends CommonFormModel protected $ipLookupHelper; /** - * LegacyEventModel constructor. + * @var UserModel + */ + protected $userModel; + + /** + * @var NotificationModel + */ + protected $notificationModel; + + /** + * EventModel constructor. * + * @param IpLookupHelper $ipLookupHelper + * @param LeadModel $leadModel + * @param UserModel $userModel + * @param NotificationModel $notificationModel + * @param CampaignModel $campaignModel * @param DecisionExecutioner $decisionExecutioner * @param KickoffExecutioner $kickoffExecutioner * @param ScheduledExecutioner $scheduledExecutioner * @param InactiveExecutioner $inactiveExecutioner * @param EventExecutioner $eventExecutioner + * @param EventDispatcher $eventDispatcher */ public function __construct( + UserModel $userModel, + NotificationModel $notificationModel, CampaignModel $campaignModel, LeadModel $leadModel, IpLookupHelper $ipLookupHelper, @@ -117,6 +143,8 @@ public function __construct( EventDispatcher $eventDispatcher, EventCollector $eventCollector ) { + $this->userModel = $userModel; + $this->notificationModel = $notificationModel; $this->campaignModel = $campaignModel; $this->leadModel = $leadModel; $this->ipLookupHelper = $ipLookupHelper; @@ -737,6 +765,42 @@ public function getLogEntity($event, $campaign, $lead = null, $ipAddress = null, return $log; } + /** + * @deprecated 2.13.0 to be removed in 3.0 + * + * @param Lead $lead + * @param $campaignCreatedBy + * @param $header + */ + public function notifyOfFailure(Lead $lead, $campaignCreatedBy, $header) + { + // Notify the lead owner if there is one otherwise campaign creator that there was a failure + if (!$owner = $lead->getOwner()) { + $ownerId = (int) $campaignCreatedBy; + $owner = $this->userModel->getEntity($ownerId); + } + + if ($owner && $owner->getId()) { + $this->notificationModel->addNotification( + $header, + 'error', + false, + $this->translator->trans( + 'mautic.campaign.event.failed', + [ + '%contact%' => ''.$lead->getPrimaryIdentifier().'', + ] + ), + null, + null, + $owner + ); + } + } + /** * Batch sleep according to settings. * diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php index 4793221034f..64ac37111d4 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php @@ -28,6 +28,7 @@ use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher; +use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; @@ -51,6 +52,11 @@ class EventDispatcherTest extends \PHPUnit_Framework_TestCase */ private $legacyDispatcher; + /** + * @var \PHPUnit_Framework_MockObject_MockBuilder|NotificationHelper + */ + private $notificationHelper; + protected function setUp() { $this->dispatcher = $this->getMockBuilder(EventDispatcherInterface::class) @@ -61,6 +67,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->notificationHelper = $this->getMockBuilder(NotificationHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->legacyDispatcher = $this->getMockBuilder(LegacyEventDispatcher::class) ->disableOriginalConstructor() ->getMock(); @@ -94,7 +104,7 @@ public function testActionBatchEventIsDispatchedWithSuccessAndFailedLogs() $log2 = $this->getMockBuilder(LeadEventLog::class) ->getMock(); - $log2->expects($this->exactly(2)) + $log2->expects($this->exactly(3)) ->method('getLead') ->willReturn($lead2); $log2->method('getMetadata') @@ -142,6 +152,10 @@ function ($eventName, PendingEvent $pendingEvent) use ($logs) { ->method('rescheduleFailure') ->with($logs->get(2)); + $this->notificationHelper->expects($this->once()) + ->method('notifyOfFailure') + ->with($lead2, $event); + $this->legacyDispatcher->expects($this->once()) ->method('dispatchExecutionEvents'); @@ -368,6 +382,7 @@ private function getEventDispatcher() $this->dispatcher, new NullLogger(), $this->scheduler, + $this->notificationHelper, $this->legacyDispatcher ); } diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php index 15138f525e6..a5c6b8a6e93 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/LegacyEventDispatcherTest.php @@ -21,6 +21,7 @@ use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher; +use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\CoreBundle\Factory\MauticFactory; use Mautic\LeadBundle\Entity\Lead; @@ -45,6 +46,11 @@ class LegacyEventDispatcherTest extends \PHPUnit_Framework_TestCase */ private $leadModel; + /** + * @var \PHPUnit_Framework_MockObject_MockBuilder|NotificationHelper + */ + private $notificationHelper; + /** * @var \PHPUnit_Framework_MockObject_MockBuilder|MauticFactory */ @@ -64,6 +70,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->notificationHelper = $this->getMockBuilder(NotificationHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->mauticFactory = $this->getMockBuilder(MauticFactory::class) ->disableOriginalConstructor() ->getMock(); @@ -226,12 +236,13 @@ public function testFailedResultAsFalseIsProcessed() ->method('getConfig') ->willReturn(['eventName' => 'something']); + $lead = new Lead(); $event = new Event(); $campaign = new Campaign(); $event->setCampaign($campaign); $leadEventLog = new LeadEventLog(); $leadEventLog->setEvent($event); - $leadEventLog->setLead(new Lead()); + $leadEventLog->setLead($lead); $leadEventLog->setMetadata(['bar' => 'foo']); $logs = new ArrayCollection([$leadEventLog]); @@ -262,6 +273,10 @@ public function testFailedResultAsFalseIsProcessed() $this->scheduler->expects($this->once()) ->method('rescheduleFailure'); + $this->notificationHelper->expects($this->once()) + ->method('notifyOfFailure') + ->with($lead, $event); + $this->getLegacyEventDispatcher()->dispatchCustomEvent($config, $logs, false, $pendingEvent); } @@ -454,6 +469,7 @@ private function getLegacyEventDispatcher() $this->scheduler, new NullLogger(), $this->leadModel, + $this->notificationHelper, $this->mauticFactory ); } diff --git a/app/bundles/CampaignBundle/Tests/Helper/NotificationHelperTest.php b/app/bundles/CampaignBundle/Tests/Helper/NotificationHelperTest.php new file mode 100644 index 00000000000..c35fd46b103 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Helper/NotificationHelperTest.php @@ -0,0 +1,220 @@ +userModel = $this->getMockBuilder(UserModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->notificationModel = $this->getMockBuilder(NotificationModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->router = $this->getMockBuilder(Router::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->translator = $this->getMockBuilder(Translator::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testContactOwnerIsNotified() + { + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + + $user = $this->getMockBuilder(User::class) + ->getMock(); + $user->method('getId') + ->willReturn('1'); + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->once()) + ->method('getOwner') + ->willReturn($user); + + $this->userModel->expects($this->never()) + ->method('getEntity'); + + $this->userModel->expects($this->never()) + ->method('getSystemAdministrator'); + + $this->notificationModel->expects($this->once()) + ->method('addNotification') + ->with( + ' / ', + 'error', + false, + $this->anything(), + null, + null, + $user + ); + + $this->getNotificationHelper()->notifyOfFailure($lead, $event); + } + + public function testCampaignCreatorIsNotified() + { + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $campaign->setCreatedBy(1); + + $user = $this->getMockBuilder(User::class) + ->getMock(); + $user->method('getId') + ->willReturn('1'); + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->once()) + ->method('getOwner') + ->willReturn(null); + + $this->userModel->expects($this->once()) + ->method('getEntity') + ->willReturn($user); + + $this->userModel->expects($this->never()) + ->method('getSystemAdministrator'); + + $this->notificationModel->expects($this->once()) + ->method('addNotification') + ->with( + ' / ', + 'error', + false, + $this->anything(), + null, + null, + $user + ); + + $this->getNotificationHelper()->notifyOfFailure($lead, $event); + } + + public function testSystemAdminIsNotified() + { + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $campaign->setCreatedBy(2); + + $user = $this->getMockBuilder(User::class) + ->getMock(); + $user->method('getId') + ->willReturn('1'); + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->once()) + ->method('getOwner') + ->willReturn(null); + + $this->userModel->expects($this->once()) + ->method('getEntity') + ->willReturn(null); + + $this->userModel->expects($this->once()) + ->method('getSystemAdministrator') + ->willReturn($user); + + $this->notificationModel->expects($this->once()) + ->method('addNotification') + ->with( + ' / ', + 'error', + false, + $this->anything(), + null, + null, + $user + ); + + $this->getNotificationHelper()->notifyOfFailure($lead, $event); + } + + public function testNotificationIgnoredIfUserNotFound() + { + $event = new Event(); + $campaign = new Campaign(); + $event->setCampaign($campaign); + $campaign->setCreatedBy(2); + + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->once()) + ->method('getOwner') + ->willReturn(null); + + $this->userModel->expects($this->once()) + ->method('getEntity') + ->willReturn(null); + + $this->userModel->expects($this->once()) + ->method('getSystemAdministrator') + ->willReturn(null); + + $this->notificationModel->expects($this->never()) + ->method('addNotification'); + + $this->getNotificationHelper()->notifyOfFailure($lead, $event); + } + + /** + * @return NotificationHelper + */ + private function getNotificationHelper() + { + return new NotificationHelper( + $this->userModel, + $this->notificationModel, + $this->translator, + $this->router + ); + } +} diff --git a/app/bundles/UserBundle/Model/UserModel.php b/app/bundles/UserBundle/Model/UserModel.php index 3ebf0518dfe..1cb250d99a5 100644 --- a/app/bundles/UserBundle/Model/UserModel.php +++ b/app/bundles/UserBundle/Model/UserModel.php @@ -176,6 +176,21 @@ public function getEntity($id = null) return $entity; } + /** + * @return User + */ + public function getSystemAdministrator() + { + $adminRole = $this->em->getRepository('MauticUserBundle:Role')->findOneBy(['isAdmin' => true]); + + return $this->getRepository()->findOneBy( + [ + 'role' => $adminRole, + 'isPublished' => true, + ] + ); + } + /** * {@inheritdoc} * From e261cdae8ce090172bb8f3a37ba08d5488627c2a Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 3 May 2018 16:28:26 -0500 Subject: [PATCH 474/778] Fixed tests --- .../Tests/EventListener/CampaignSubscriberTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php b/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php index f322689a7b7..d0f10fceedb 100644 --- a/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php +++ b/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php @@ -21,6 +21,7 @@ use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher as CampaignEventDispatcher; use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher; +use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\ChannelBundle\ChannelEvents; use Mautic\ChannelBundle\EventListener\CampaignSubscriber; @@ -136,11 +137,16 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $notificationHelper = $this->getMockBuilder(NotificationHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->legacyDispatcher = new LegacyEventDispatcher( $this->dispatcher, $this->scheduler, new NullLogger(), $leadModel, + $notificationHelper, $factory ); @@ -148,6 +154,7 @@ protected function setUp() $this->dispatcher, new NullLogger(), $this->scheduler, + $notificationHelper, $this->legacyDispatcher ); From 40a6c8a2b9f396adbf096a7be1369b568d72d01b Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 8 May 2018 18:59:40 -0500 Subject: [PATCH 475/778] Ranamed some classes and futher broke down some classes --- app/bundles/CampaignBundle/Config/config.php | 54 ++- ...entDispatcher.php => ActionDispatcher.php} | 63 +--- .../Dispatcher/ConditionDispatcher.php | 51 +++ .../Dispatcher/DecisionDispatcher.php | 79 ++++ .../{Action.php => ActionExecutioner.php} | 43 ++- ...Condition.php => ConditionExecutioner.php} | 22 +- .../{Decision.php => DecisionExecutioner.php} | 73 ++-- .../Executioner/Event/EventInterface.php | 5 +- .../Executioner/EventExecutioner.php | 337 ++++++------------ .../Executioner/Helper/DecisionTreeHelper.php | 56 +++ .../Executioner/Helper/InactiveHelper.php | 1 + .../Executioner/InactiveExecutioner.php | 50 ++- ...xecutioner.php => RealTimeExecutioner.php} | 12 +- .../Executioner/Result/EvaluatedContacts.php | 11 +- .../CampaignBundle/Model/LegacyEventModel.php | 63 ++-- ...tcherTest.php => ActionDispatcherTest.php} | 71 +--- .../Dispatcher/ConditionDispatcherTest.php | 63 ++++ .../Dispatcher/DecisionDispatcherTest.php | 92 +++++ ...erTest.php => RealTimeExecutionerTest.php} | 27 +- app/bundles/ChannelBundle/Config/config.php | 2 +- .../EventListener/CampaignSubscriber.php | 22 +- 21 files changed, 709 insertions(+), 488 deletions(-) rename app/bundles/CampaignBundle/Executioner/Dispatcher/{EventDispatcher.php => ActionDispatcher.php} (73%) create mode 100644 app/bundles/CampaignBundle/Executioner/Dispatcher/ConditionDispatcher.php create mode 100644 app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php rename app/bundles/CampaignBundle/Executioner/Event/{Action.php => ActionExecutioner.php} (51%) rename app/bundles/CampaignBundle/Executioner/Event/{Condition.php => ConditionExecutioner.php} (76%) rename app/bundles/CampaignBundle/Executioner/Event/{Decision.php => DecisionExecutioner.php} (80%) create mode 100644 app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php rename app/bundles/CampaignBundle/Executioner/{DecisionExecutioner.php => RealTimeExecutioner.php} (96%) rename app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/{EventDispatcherTest.php => ActionDispatcherTest.php} (79%) create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ConditionDispatcherTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php rename app/bundles/CampaignBundle/Tests/Executioner/{DecisionExecutionerTest.php => RealTimeExecutionerTest.php} (91%) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 44321a84b71..5cdb2c309d2 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -220,20 +220,22 @@ ], ], 'mautic.campaign.model.event' => [ - 'class' => 'Mautic\CampaignBundle\Model\EventModel', + 'class' => \Mautic\CampaignBundle\Model\EventModel::class, 'arguments' => [ 'mautic.user.model.user', 'mautic.core.model.notification', 'mautic.campaign.model.campaign', 'mautic.lead.model.lead', 'mautic.helper.ip_lookup', - 'mautic.campaign.executioner.active_decision', + 'mautic.campaign.executioner.realtime', 'mautic.campaign.executioner.kickoff', 'mautic.campaign.executioner.scheduled', 'mautic.campaign.executioner.inactive', - 'mautic.campaign.executioner', - 'mautic.campaign.event_dispatcher', + 'mautic.campaign.event_executioner', 'mautic.campaign.event_collector', + 'mautic.campaign.dispatcher.action', + 'mautic.campaign.dispatcher.condition', + 'mautic.campaign.dispatcher.decision', ], ], 'mautic.campaign.model.event_log' => [ @@ -299,8 +301,8 @@ 'monolog.logger.mautic', ], ], - 'mautic.campaign.event_dispatcher' => [ - 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher::class, + 'mautic.campaign.dispatcher.action' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher::class, 'arguments' => [ 'event_dispatcher', 'monolog.logger.mautic', @@ -309,6 +311,19 @@ 'mautic.campaign.legacy_event_dispatcher', ], ], + 'mautic.campaign.dispatcher.condition' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\ConditionDispatcher::class, + 'arguments' => [ + 'event_dispatcher', + ], + ], + 'mautic.campaign.dispatcher.decision' => [ + 'class' => \Mautic\CampaignBundle\Executioner\Dispatcher\DecisionDispatcher::class, + 'arguments' => [ + 'event_dispatcher', + 'mautic.campaign.legacy_event_dispatcher', + ], + ], 'mautic.campaign.event_logger' => [ 'class' => \Mautic\CampaignBundle\Executioner\Logger\EventLogger::class, 'arguments' => [ @@ -349,25 +364,26 @@ ], ], 'mautic.campaign.executioner.action' => [ - 'class' => \Mautic\CampaignBundle\Executioner\Event\Action::class, + 'class' => \Mautic\CampaignBundle\Executioner\Event\ActionExecutioner::class, 'arguments' => [ - 'mautic.campaign.event_dispatcher', + 'mautic.campaign.dispatcher.action', + 'mautic.campaign.event_logger', ], ], 'mautic.campaign.executioner.condition' => [ - 'class' => \Mautic\CampaignBundle\Executioner\Event\Condition::class, + 'class' => \Mautic\CampaignBundle\Executioner\Event\ConditionExecutioner::class, 'arguments' => [ - 'mautic.campaign.event_dispatcher', + 'mautic.campaign.dispatcher.condition', ], ], 'mautic.campaign.executioner.decision' => [ - 'class' => \Mautic\CampaignBundle\Executioner\Event\Decision::class, + 'class' => \Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner::class, 'arguments' => [ 'mautic.campaign.event_logger', - 'mautic.campaign.event_dispatcher', + 'mautic.campaign.dispatcher.decision', ], ], - 'mautic.campaign.executioner' => [ + 'mautic.campaign.event_executioner' => [ 'class' => \Mautic\CampaignBundle\Executioner\EventExecutioner::class, 'arguments' => [ 'mautic.campaign.event_collector', @@ -386,7 +402,7 @@ 'monolog.logger.mautic', 'mautic.campaign.contact_finder.kickoff', 'translator', - 'mautic.campaign.executioner', + 'mautic.campaign.event_executioner', 'mautic.campaign.scheduler', ], ], @@ -396,18 +412,18 @@ 'mautic.campaign.repository.lead_event_log', 'monolog.logger.mautic', 'translator', - 'mautic.campaign.executioner', + 'mautic.campaign.event_executioner', 'mautic.campaign.scheduler', 'mautic.campaign.contact_finder.scheduled', ], ], - 'mautic.campaign.executioner.active_decision' => [ - 'class' => \Mautic\CampaignBundle\Executioner\DecisionExecutioner::class, + 'mautic.campaign.executioner.realtime' => [ + 'class' => \Mautic\CampaignBundle\Executioner\RealTimeExecutioner::class, 'arguments' => [ 'monolog.logger.mautic', 'mautic.lead.model.lead', 'mautic.campaign.repository.event', - 'mautic.campaign.executioner', + 'mautic.campaign.event_executioner', 'mautic.campaign.executioner.decision', 'mautic.campaign.event_collector', 'mautic.campaign.scheduler', @@ -422,7 +438,7 @@ 'translator', 'mautic.campaign.scheduler', 'mautic.campaign.helper.inactivity', - 'mautic.campaign.executioner', + 'mautic.campaign.event_executioner', ], ], 'mautic.campaign.helper.inactivity' => [ diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php similarity index 73% rename from app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php rename to app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php index dc86d4cc863..9d7ee7da75f 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/EventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php @@ -1,7 +1,7 @@ dispatcher->dispatch($config->getEventName(), $event); - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $event); - - $this->legacyDispatcher->dispatchDecisionEvent($event); - - return $event; - } - - /** - * @param DecisionAccessor $config - * @param ArrayCollection $logs - * @param EvaluatedContacts $evaluatedContacts - */ - public function dispatchDecisionResultsEvent(DecisionAccessor $config, ArrayCollection $logs, EvaluatedContacts $evaluatedContacts) - { - $this->dispatcher->dispatch( - CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, - new DecisionResultsEvent($config, $logs, $evaluatedContacts) - ); - } - - /** - * @param ConditionAccessor $config - * @param LeadEventLog $log - * - * @return ConditionEvent - */ - public function dispatchConditionEvent(ConditionAccessor $config, LeadEventLog $log) - { - $event = new ConditionEvent($config, $log); - $this->dispatcher->dispatch($config->getEventName(), $event); - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $event); - - return $event; - } - /** * @param AbstractEventAccessor $config * @param Event $event @@ -242,4 +187,4 @@ private function validateProcessedLogs(ArrayCollection $pending, ArrayCollection } } } -} +} \ No newline at end of file diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/ConditionDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/ConditionDispatcher.php new file mode 100644 index 00000000000..66fcc88448d --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/ConditionDispatcher.php @@ -0,0 +1,51 @@ +dispatcher = $dispatcher; + } + + /** + * @param ConditionAccessor $config + * @param LeadEventLog $log + * + * @return ConditionEvent + */ + public function dispatchEvent(ConditionAccessor $config, LeadEventLog $log) + { + $event = new ConditionEvent($config, $log); + $this->dispatcher->dispatch($config->getEventName(), $event); + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $event); + + return $event; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php new file mode 100644 index 00000000000..5b6dcff9834 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php @@ -0,0 +1,79 @@ +dispatcher = $dispatcher; + $this->legacyDispatcher = $legacyDispatcher; + } + + /** + * @param DecisionAccessor $config + * @param LeadEventLog $log + * @param $passthrough + * + * @return DecisionEvent + */ + public function dispatchEvent(DecisionAccessor $config, LeadEventLog $log, $passthrough) + { + $event = new DecisionEvent($config, $log, $passthrough); + $this->dispatcher->dispatch($config->getEventName(), $event); + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $event); + + $this->legacyDispatcher->dispatchDecisionEvent($event); + + return $event; + } + + /** + * @param DecisionAccessor $config + * @param ArrayCollection $logs + * @param EvaluatedContacts $evaluatedContacts + */ + public function dispatchDecisionResultsEvent(DecisionAccessor $config, ArrayCollection $logs, EvaluatedContacts $evaluatedContacts) + { + $this->dispatcher->dispatch( + CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, + new DecisionResultsEvent($config, $logs, $evaluatedContacts) + ); + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Event/Action.php b/app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php similarity index 51% rename from app/bundles/CampaignBundle/Executioner/Event/Action.php rename to app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php index 22477262ff9..1c2fa273e16 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Action.php +++ b/app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php @@ -15,45 +15,52 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; -use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher; use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; +use Mautic\CampaignBundle\Executioner\Logger\EventLogger; +use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; -class Action implements EventInterface +class ActionExecutioner implements EventInterface { const TYPE = 'action'; /** - * @var EventDispatcher + * @var ActionDispatcher */ private $dispatcher; /** - * Action constructor. + * @var EventLogger + */ + private $eventLogger; + + /** + * ActionExecutioner constructor. * - * @param EventDispatcher $dispatcher + * @param ActionDispatcher $dispatcher + * @param EventLogger $eventLogger */ - public function __construct(EventDispatcher $dispatcher) + public function __construct(ActionDispatcher $dispatcher, EventLogger $eventLogger) { - $this->dispatcher = $dispatcher; + $this->dispatcher = $dispatcher; + $this->eventLogger = $eventLogger; } /** - * @param ActionAccessor $config - * @param ArrayCollection $logs - * - * @return mixed|void + * @param AbstractEventAccessor $config + * @param ArrayCollection $logs * + * @return EvaluatedContacts * @throws CannotProcessEventException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException * @throws \ReflectionException */ - public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) + public function execute(AbstractEventAccessor $config, ArrayCollection $logs) { /** @var LeadEventLog $firstLog */ if (!$firstLog = $logs->first()) { - return; + return new EvaluatedContacts(); } $event = $firstLog->getEvent(); @@ -63,6 +70,12 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs } // Execute to process the batch of contacts - $this->dispatcher->dispatchActionEvent($config, $event, $logs); + $pendingEvent = $this->dispatcher->dispatchEvent($config, $event, $logs); + + /** @var ArrayCollection $contacts */ + $passed = $this->eventLogger->extractContactsFromLogs($pendingEvent->getSuccessful()); + $failed = $this->eventLogger->extractContactsFromLogs($pendingEvent->getFailures()); + + return new EvaluatedContacts($passed, $failed); } } diff --git a/app/bundles/CampaignBundle/Executioner/Event/Condition.php b/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php similarity index 76% rename from app/bundles/CampaignBundle/Executioner/Event/Condition.php rename to app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php index 75a7ec7eac6..b0517ecf317 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Condition.php +++ b/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php @@ -16,28 +16,28 @@ use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\ConditionDispatcher; use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; use Mautic\CampaignBundle\Executioner\Exception\ConditionFailedException; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; -class Condition implements EventInterface +class ConditionExecutioner implements EventInterface { const TYPE = 'condition'; /** - * @var EventDispatcher + * @var ConditionDispatcher */ private $dispatcher; /** - * Condition constructor. + * ConditionExecutioner constructor. * - * @param EventDispatcher $dispatcher + * @param ConditionDispatcher $dispatcher */ - public function __construct(EventDispatcher $dispatcher) + public function __construct(ConditionDispatcher $dispatcher) { - $this->dispatcher = $dispatcher; + $this->dispatcher = $dispatcher; } /** @@ -48,7 +48,7 @@ public function __construct(EventDispatcher $dispatcher) * * @throws CannotProcessEventException */ - public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) + public function execute(AbstractEventAccessor $config, ArrayCollection $logs) { $evaluatedContacts = new EvaluatedContacts(); @@ -56,7 +56,7 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs foreach ($logs as $log) { try { /* @var ConditionAccessor $config */ - $this->execute($config, $log); + $this->dispatchEvent($config, $log); $evaluatedContacts->pass($log->getLead()); } catch (ConditionFailedException $exception) { $evaluatedContacts->fail($log->getLead()); @@ -74,13 +74,13 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs * @throws CannotProcessEventException * @throws ConditionFailedException */ - private function execute(ConditionAccessor $config, LeadEventLog $log) + private function dispatchEvent(ConditionAccessor $config, LeadEventLog $log) { if (Event::TYPE_CONDITION !== $log->getEvent()->getEventType()) { throw new CannotProcessEventException('Cannot process event ID '.$log->getEvent()->getId().' as a condition.'); } - $conditionEvent = $this->dispatcher->dispatchConditionEvent($config, $log); + $conditionEvent = $this->dispatcher->dispatchEvent($config, $log); if (!$conditionEvent->wasConditionSatisfied()) { throw new ConditionFailedException('evaluation failed'); diff --git a/app/bundles/CampaignBundle/Executioner/Event/Decision.php b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php similarity index 80% rename from app/bundles/CampaignBundle/Executioner/Event/Decision.php rename to app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php index f11504cb66a..9fafd203ee2 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/Decision.php +++ b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php @@ -16,14 +16,14 @@ use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\DecisionDispatcher; use Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException; use Mautic\CampaignBundle\Executioner\Exception\DecisionNotApplicableException; use Mautic\CampaignBundle\Executioner\Logger\EventLogger; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\LeadBundle\Entity\Lead; -class Decision implements EventInterface +class DecisionExecutioner implements EventInterface { const TYPE = 'decision'; @@ -33,30 +33,56 @@ class Decision implements EventInterface private $eventLogger; /** - * @var EventDispatcher + * @var DecisionDispatcher */ private $dispatcher; /** - * Action constructor. + * DecisionExecutioner constructor. * - * @param EventLogger $eventLogger + * @param EventLogger $eventLogger + * @param DecisionDispatcher $dispatcher */ - public function __construct(EventLogger $eventLogger, EventDispatcher $dispatcher) + public function __construct(EventLogger $eventLogger, DecisionDispatcher $dispatcher) { $this->eventLogger = $eventLogger; $this->dispatcher = $dispatcher; } + /** + * @param DecisionAccessor $config + * @param Event $event + * @param Lead $contact + * @param null $passthrough + * @param null $channel + * @param null $channelId + * + * @throws CannotProcessEventException + * @throws DecisionNotApplicableException + */ + public function evaluateForContact(DecisionAccessor $config, Event $event, Lead $contact, $passthrough = null, $channel = null, $channelId = null) + { + if (Event::TYPE_DECISION !== $event->getEventType()) { + throw new CannotProcessEventException('Cannot process event ID '.$event->getId().' as a decision.'); + } + + $log = $this->eventLogger->buildLogEntry($event, $contact); + $log->setChannel($channel) + ->setChannelId($channelId); + + $this->dispatchEvent($config, $log, $passthrough); + $this->eventLogger->persistLog($log); + } + /** * @param AbstractEventAccessor $config * @param ArrayCollection $logs * - * @return EvaluatedContacts|mixed + * @return EvaluatedContacts * * @throws CannotProcessEventException */ - public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs) + public function execute(AbstractEventAccessor $config, ArrayCollection $logs) { $evaluatedContacts = new EvaluatedContacts(); @@ -68,7 +94,7 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs try { /* @var DecisionAccessor $config */ - $this->execute($config, $log); + $this->dispatchEvent($config, $log); $evaluatedContacts->pass($log->getLead()); } catch (DecisionNotApplicableException $exception) { $evaluatedContacts->fail($log->getLead()); @@ -80,31 +106,6 @@ public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs return $evaluatedContacts; } - /** - * @param DecisionAccessor $config - * @param Event $event - * @param Lead $contact - * @param null $passthrough - * @param null $channel - * @param null $channelId - * - * @throws CannotProcessEventException - * @throws DecisionNotApplicableException - */ - public function evaluateForContact(DecisionAccessor $config, Event $event, Lead $contact, $passthrough = null, $channel = null, $channelId = null) - { - if (Event::TYPE_DECISION !== $event->getEventType()) { - throw new CannotProcessEventException('Cannot process event ID '.$event->getId().' as a decision.'); - } - - $log = $this->eventLogger->buildLogEntry($event, $contact); - $log->setChannel($channel) - ->setChannelId($channelId); - - $this->execute($config, $log, $passthrough); - $this->eventLogger->persistLog($log); - } - /** * @param DecisionAccessor $config * @param LeadEventLog $log @@ -112,9 +113,9 @@ public function evaluateForContact(DecisionAccessor $config, Event $event, Lead * * @throws DecisionNotApplicableException */ - private function execute(DecisionAccessor $config, LeadEventLog $log, $passthrough = null) + private function dispatchEvent(DecisionAccessor $config, LeadEventLog $log, $passthrough = null) { - $decisionEvent = $this->dispatcher->dispatchDecisionEvent($config, $log, $passthrough); + $decisionEvent = $this->dispatcher->dispatchEvent($config, $log, $passthrough); if (!$decisionEvent->wasDecisionApplicable()) { throw new DecisionNotApplicableException('evaluation failed'); diff --git a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php index 4953f5dd2b2..c2703f0c264 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php +++ b/app/bundles/CampaignBundle/Executioner/Event/EventInterface.php @@ -13,6 +13,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; +use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; interface EventInterface { @@ -20,7 +21,7 @@ interface EventInterface * @param AbstractEventAccessor $config * @param ArrayCollection $logs * - * @return mixed + * @return EvaluatedContacts */ - public function executeLogs(AbstractEventAccessor $config, ArrayCollection $logs); + public function execute(AbstractEventAccessor $config, ArrayCollection $logs); } diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 9544fbc22d6..c219322e097 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -14,12 +14,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; -use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\TypeNotFoundException; use Mautic\CampaignBundle\EventCollector\EventCollector; -use Mautic\CampaignBundle\Executioner\Event\Action; -use Mautic\CampaignBundle\Executioner\Event\Condition; -use Mautic\CampaignBundle\Executioner\Event\Decision; +use Mautic\CampaignBundle\Executioner\Event\ActionExecutioner; +use Mautic\CampaignBundle\Executioner\Event\ConditionExecutioner; +use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner; use Mautic\CampaignBundle\Executioner\Logger\EventLogger; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; @@ -32,17 +31,17 @@ class EventExecutioner { /** - * @var Action + * @var ActionExecutioner */ private $actionExecutioner; /** - * @var Condition + * @var ConditionExecutioner */ private $conditionExecutioner; /** - * @var Decision + * @var DecisionExecutioner */ private $decisionExecutioner; @@ -66,11 +65,6 @@ class EventExecutioner */ private $scheduler; - /** - * @var \DateTime - */ - private $now; - /** * @var Responses */ @@ -81,23 +75,28 @@ class EventExecutioner */ private $removedContactTracker; + /** + * @var \DateTime + */ + private $executionDate; + /** * EventExecutioner constructor. * - * @param EventCollector $eventCollector - * @param EventLogger $eventLogger - * @param Action $actionExecutioner - * @param Condition $conditionExecutioner - * @param Decision $decisionExecutioner - * @param LoggerInterface $logger - * @param EventScheduler $scheduler + * @param EventCollector $eventCollector + * @param EventLogger $eventLogger + * @param ActionExecutioner $actionExecutioner + * @param ConditionExecutioner $conditionExecutioner + * @param DecisionExecutioner $decisionExecutioner + * @param LoggerInterface $logger + * @param EventScheduler $scheduler */ public function __construct( EventCollector $eventCollector, EventLogger $eventLogger, - Action $actionExecutioner, - Condition $conditionExecutioner, - Decision $decisionExecutioner, + ActionExecutioner $actionExecutioner, + ConditionExecutioner $conditionExecutioner, + DecisionExecutioner $decisionExecutioner, LoggerInterface $logger, EventScheduler $scheduler, RemovedContactTracker $removedContactTracker @@ -110,15 +109,9 @@ public function __construct( $this->logger = $logger; $this->scheduler = $scheduler; $this->removedContactTracker = $removedContactTracker; - $this->now = new \DateTime(); - } - /** - * @param \DateTime $now - */ - public function setNow(\DateTime $now) - { - $this->now = $now; + // Be sure that all events are compared using the exact same \DateTime + $this->executionDate = new \DateTime(); } /** @@ -191,103 +184,32 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte $this->checkForRemovedContacts($logs); if ($counter) { + // Must pass $counter around rather than setting it as a class property as this class is used + // circularly to process children of parent events thus counter must be kept track separately $counter->advanceExecuted($logs->count()); } switch ($event->getEventType()) { case Event::TYPE_ACTION: - $this->executeAction($config, $event, $logs, $counter); + $evaluatedContacts = $this->actionExecutioner->execute($config, $logs); + $this->persistLogs($logs); + $this->executeEventConditionsForContacts($event, $evaluatedContacts->getPassed(), $counter); break; case Event::TYPE_CONDITION: - $this->executeCondition($config, $event, $logs, $counter); + $evaluatedContacts = $this->conditionExecutioner->execute($config, $logs); + $this->persistLogs($logs); + $this->executeDecisionPathEventsForContacts($event, $evaluatedContacts, $counter); break; case Event::TYPE_DECISION: - $this->executeDecision($config, $event, $logs, $counter); + $evaluatedContacts = $this->decisionExecutioner->execute($config, $logs); + $this->persistLogs($logs); + $this->executeDecisionPathEventsForContacts($event, $evaluatedContacts, $counter); break; default: throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); } } - /** - * @param Event $event - * @param ArrayCollection $contacts - * @param bool $isInactiveEvent - */ - public function recordLogsAsExecutedForEvent(Event $event, ArrayCollection $contacts, $isInactiveEvent = false) - { - $config = $this->collector->getEventConfig($event); - $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); - - // Save updated log entries and clear from memory - $this->eventLogger->persistCollection($logs) - ->clear(); - } - - /** - * @param Event $event - * @param EvaluatedContacts $contacts - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - * @throws Scheduler\Exception\NotSchedulableException - */ - public function executeContactsForDecisionPathChildren(Event $event, EvaluatedContacts $contacts, Counter $counter = null) - { - $childrenCounter = new Counter(); - $positive = $contacts->getPassed(); - if ($positive->count()) { - $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $positive->getKeys()).' passed evaluation for event ID '.$event->getId()); - - $children = $event->getPositiveChildren(); - $childrenCounter->advanceEvaluated($children->count()); - $this->executeContactsForChildren($children, $positive, $childrenCounter); - } - - $negative = $contacts->getFailed(); - if ($negative->count()) { - $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $negative->getKeys()).' failed evaluation for event ID '.$event->getId()); - - $children = $event->getNegativeChildren(); - - $childrenCounter->advanceEvaluated($children->count()); - $this->executeContactsForChildren($children, $negative, $childrenCounter); - } - - if ($counter) { - $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); - $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); - } - } - - /** - * @param Event $event - * @param ArrayCollection $contacts - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - * @throws Scheduler\Exception\NotSchedulableException - */ - public function executeContactsForConditionChildren(Event $event, ArrayCollection $contacts, Counter $counter = null) - { - $childrenCounter = new Counter(); - $conditions = $event->getChildrenByEventType(Event::TYPE_CONDITION); - $childrenCounter->advanceEvaluated($conditions->count()); - - $this->logger->debug('CAMPAIGN: Evaluating '.$conditions->count().' conditions for action ID '.$event->getId()); - - $this->executeContactsForChildren($conditions, $contacts, $childrenCounter); - - if ($counter) { - $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); - $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); - } - } - /** * @param ArrayCollection $children * @param ArrayCollection $contacts @@ -298,143 +220,57 @@ public function executeContactsForConditionChildren(Event $event, ArrayCollectio * @throws Exception\CannotProcessEventException * @throws Scheduler\Exception\NotSchedulableException */ - public function executeContactsForChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter) + public function executeEventsForContacts(ArrayCollection $events, ArrayCollection $contacts, Counter $childrenCounter) { - foreach ($children as $child) { - // Ignore decisions - if (Event::TYPE_DECISION == $child->getEventType()) { - $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$child->getId().' as a decision'); - continue; - } - - $executionDate = $this->scheduler->getExecutionDateTime($child, $this->now); - - $this->logger->debug( - 'CAMPAIGN: Event ID# '.$child->getId(). - ' to be executed on '.$executionDate->format('Y-m-d H:i:s') - ); - - if ($this->scheduler->shouldSchedule($executionDate, $this->now)) { - $childrenCounter->advanceTotalScheduled($contacts->count()); - $this->scheduler->schedule($child, $executionDate, $contacts); - continue; - } - - $this->executeForContacts($child, $contacts, $childrenCounter); + if (!$contacts->count()) { + return; } - } - - /** - * @param ArrayCollection $children - * @param ArrayCollection $contacts - * @param Counter $childrenCounter - * @param bool $validatingInaction - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - * @throws Scheduler\Exception\NotSchedulableException - */ - public function executeContactsForInactiveChildren(ArrayCollection $children, ArrayCollection $contacts, Counter $childrenCounter, \DateTime $earliestLastActiveDateTime) - { - $eventExecutionDates = $this->scheduler->getSortedExecutionDates($children, $earliestLastActiveDateTime); - - /** @var \DateTime $earliestExecutionDate */ - $earliestExecutionDate = reset($eventExecutionDates); - foreach ($children as $child) { + foreach ($events as $event) { // Ignore decisions - if (Event::TYPE_DECISION == $child->getEventType()) { - $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$child->getId().' as a decision'); + if (Event::TYPE_DECISION == $event->getEventType()) { + $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$event->getId().' as a decision'); continue; } - $executionDate = $this->scheduler->getExecutionDateForInactivity( - $eventExecutionDates[$child->getId()], - $earliestExecutionDate, - $this->now - ); + $executionDate = $this->scheduler->getExecutionDateTime($event, $this->executionDate); $this->logger->debug( - 'CAMPAIGN: Event ID# '.$child->getId(). + 'CAMPAIGN: Event ID# '.$event->getId(). ' to be executed on '.$executionDate->format('Y-m-d H:i:s') ); - if ($this->scheduler->shouldSchedule($executionDate, $this->now)) { + if ($this->scheduler->shouldSchedule($executionDate, $this->executionDate)) { $childrenCounter->advanceTotalScheduled($contacts->count()); - $this->scheduler->schedule($child, $executionDate, $contacts, true); + $this->scheduler->schedule($event, $executionDate, $contacts); continue; } - $this->executeForContacts($child, $contacts, $childrenCounter, true); + $this->executeForContacts($event, $contacts, $childrenCounter); } } /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - * @throws Scheduler\Exception\NotSchedulableException - */ - private function executeAction(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) - { - $this->actionExecutioner->executeLogs($config, $logs); - - /** @var ArrayCollection $contacts */ - $contacts = $this->eventLogger->extractContactsFromLogs($logs); - - // Update and clear any pending logs - $this->persistLogs($logs); - - // Process conditions that are attached to this action - $this->executeContactsForConditionChildren($event, $contacts, $counter); - } - - /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - * @throws Scheduler\Exception\NotSchedulableException + * @param Event $event + * @param ArrayCollection $contacts + * @param bool $isInactiveEvent */ - private function executeCondition(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) + public function recordLogsAsExecutedForEvent(Event $event, ArrayCollection $contacts, $isInactiveEvent = false) { - $evaluatedContacts = $this->conditionExecutioner->executeLogs($config, $logs); - - // Update and clear any pending logs - $this->persistLogs($logs); + $config = $this->collector->getEventConfig($event); + $logs = $this->eventLogger->generateLogsFromContacts($event, $config, $contacts, $isInactiveEvent); - $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); + // Save updated log entries and clear from memory + $this->eventLogger->persistCollection($logs) + ->clear(); } /** - * @param AbstractEventAccessor $config - * @param Event $event - * @param ArrayCollection $logs - * @param Counter|null $counter - * - * @throws Dispatcher\Exception\LogNotProcessedException - * @throws Dispatcher\Exception\LogPassedAndFailedException - * @throws Exception\CannotProcessEventException - * @throws Scheduler\Exception\NotSchedulableException + * @return \DateTime */ - private function executeDecision(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, Counter $counter = null) + public function getExecutionDate() { - $evaluatedContacts = $this->decisionExecutioner->executeLogs($config, $logs); - - // Update and clear any pending logs - $this->persistLogs($logs); - - $this->executeContactsForDecisionPathChildren($event, $evaluatedContacts, $counter); + return $this->executionDate; } /** @@ -471,4 +307,63 @@ private function checkForRemovedContacts(ArrayCollection $logs) } } } + + /** + * @param Event $event + * @param ArrayCollection $contacts + * @param Counter|null $counter + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + private function executeEventConditionsForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null) + { + $childrenCounter = new Counter(); + $conditions = $event->getChildrenByEventType(Event::TYPE_CONDITION); + $childrenCounter->advanceEvaluated($conditions->count()); + + $this->logger->debug('CAMPAIGN: Evaluating '.$conditions->count().' conditions for action ID '.$event->getId()); + + $this->executeEventsForContacts($conditions, $contacts, $childrenCounter); + + if ($counter) { + $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); + $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); + } + } + + /** + * @param Event $event + * @param EvaluatedContacts $contacts + * @param Counter|null $counter + */ + private function executeDecisionPathEventsForContacts(Event $event, EvaluatedContacts $contacts, Counter $counter = null) + { + $childrenCounter = new Counter(); + $positive = $contacts->getPassed(); + if ($positive->count()) { + $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $positive->getKeys()).' passed evaluation for event ID '.$event->getId()); + + $children = $event->getPositiveChildren(); + $childrenCounter->advanceEvaluated($children->count()); + $this->executeEventsForContacts($children, $positive, $childrenCounter); + } + + $negative = $contacts->getFailed(); + if ($negative->count()) { + $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $negative->getKeys()).' failed evaluation for event ID '.$event->getId()); + + $children = $event->getNegativeChildren(); + + $childrenCounter->advanceEvaluated($children->count()); + $this->executeEventsForContacts($children, $negative, $childrenCounter); + } + + if ($counter) { + $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); + $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); + } + } } diff --git a/app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php new file mode 100644 index 00000000000..086925b88d8 --- /dev/null +++ b/app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php @@ -0,0 +1,56 @@ +eventCollector = $eventCollector; + $this->eventLogger = $eventLogger; + $this->eventExecutioner = $eventExecutioner; + $this->logger = $logger; + } +} diff --git a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php index 806cd7aa12b..75366e1518b 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/InactiveHelper.php @@ -57,6 +57,7 @@ class InactiveHelper * @param EventScheduler $scheduler * @param InactiveContactFinder $inactiveContactFinder * @param LeadEventLogRepository $eventLogRepository + * @param EventRepository $eventRepository * @param LoggerInterface $logger */ public function __construct( diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 6dacf22ffab..a828e0e62db 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -292,7 +292,7 @@ private function executeEvents() if ($contacts->count()) { // Execute or schedule the events attached to the inactive side of the decision - $this->executioner->executeContactsForInactiveChildren($inactiveEvents, $contacts, $this->counter, $earliestLastActiveDateTime); + $this->executeLogsForInactiveEvents($inactiveEvents, $contacts, $this->counter, $earliestLastActiveDateTime); // Record decision for these contacts $this->executioner->recordLogsAsExecutedForEvent($decisionEvent, $contacts, true); } @@ -334,4 +334,52 @@ private function getStartingContactIdForNextBatch(ArrayCollection $contacts) return $maxId; } + + /** + * @param ArrayCollection $children + * @param ArrayCollection $contacts + * @param Counter $childrenCounter + * @param \DateTime $earliestLastActiveDateTime + * + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException + * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException + * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException + * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException + */ + private function executeLogsForInactiveEvents(ArrayCollection $events, ArrayCollection $contacts, Counter $childrenCounter, \DateTime $earliestLastActiveDateTime) + { + $eventExecutionDates = $this->scheduler->getSortedExecutionDates($events, $earliestLastActiveDateTime); + + /** @var \DateTime $earliestExecutionDate */ + $earliestExecutionDate = reset($eventExecutionDates); + + $executionDate = $this->executioner->getExecutionDate(); + + foreach ($events as $event) { + // Ignore decisions + if (Event::TYPE_DECISION == $event->getEventType()) { + $this->logger->debug('CAMPAIGN: Ignoring child event ID '.$event->getId().' as a decision'); + continue; + } + + $eventExecutionDate = $this->scheduler->getExecutionDateForInactivity( + $eventExecutionDates[$event->getId()], + $earliestExecutionDate, + $executionDate + ); + + $this->logger->debug( + 'CAMPAIGN: Event ID# '.$event->getId(). + ' to be executed on '.$eventExecutionDate->format('Y-m-d H:i:s') + ); + + if ($this->scheduler->shouldSchedule($eventExecutionDate, $executionDate)) { + $childrenCounter->advanceTotalScheduled($contacts->count()); + $this->scheduler->schedule($event, $eventExecutionDate, $contacts, true); + continue; + } + + $this->executioner->executeForContacts($event, $contacts, $childrenCounter, true); + } + } } diff --git a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php similarity index 96% rename from app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php rename to app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php index 992fa4f538d..d693699cb38 100644 --- a/app/bundles/CampaignBundle/Executioner/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php @@ -16,7 +16,7 @@ use Mautic\CampaignBundle\Entity\EventRepository; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; -use Mautic\CampaignBundle\Executioner\Event\Decision; +use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner as Executioner; use Mautic\CampaignBundle\Executioner\Exception\CampaignNotExecutableException; use Mautic\CampaignBundle\Executioner\Exception\DecisionNotApplicableException; use Mautic\CampaignBundle\Executioner\Result\Responses; @@ -26,7 +26,7 @@ use Mautic\LeadBundle\Tracker\ContactTracker; use Psr\Log\LoggerInterface; -class DecisionExecutioner +class RealTimeExecutioner { /** * @var LoggerInterface @@ -59,7 +59,7 @@ class DecisionExecutioner private $executioner; /** - * @var Decision + * @var Executioner */ private $decisionExecutioner; @@ -84,13 +84,13 @@ class DecisionExecutioner private $responses; /** - * DecisionExecutioner constructor. + * RealTimeExecutioner constructor. * * @param LoggerInterface $logger * @param LeadModel $leadModel * @param EventRepository $eventRepository * @param EventExecutioner $executioner - * @param Decision $decisionExecutioner + * @param Executioner $decisionExecutioner * @param EventCollector $collector * @param EventScheduler $scheduler * @param ContactTracker $contactTracker @@ -100,7 +100,7 @@ public function __construct( LeadModel $leadModel, EventRepository $eventRepository, EventExecutioner $executioner, - Decision $decisionExecutioner, + Executioner $decisionExecutioner, EventCollector $collector, EventScheduler $scheduler, ContactTracker $contactTracker diff --git a/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php b/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php index 75d271b487d..4ad74e9c40c 100644 --- a/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php +++ b/app/bundles/CampaignBundle/Executioner/Result/EvaluatedContacts.php @@ -27,12 +27,15 @@ class EvaluatedContacts private $failed; /** - * ConditionContacts constructor. + * EvaluatedContacts constructor. + * + * @param ArrayCollection|null $passed + * @param ArrayCollection|null $failed */ - public function __construct() + public function __construct(ArrayCollection $passed = null, ArrayCollection $failed = null) { - $this->passed = new ArrayCollection(); - $this->failed = new ArrayCollection(); + $this->passed = (null === $passed) ? new ArrayCollection() : $passed; + $this->failed = (null === $failed) ? new ArrayCollection() : $failed; } /** diff --git a/app/bundles/CampaignBundle/Model/LegacyEventModel.php b/app/bundles/CampaignBundle/Model/LegacyEventModel.php index db25be5c238..46676ae1015 100644 --- a/app/bundles/CampaignBundle/Model/LegacyEventModel.php +++ b/app/bundles/CampaignBundle/Model/LegacyEventModel.php @@ -21,12 +21,13 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; -use Mautic\CampaignBundle\Executioner\DecisionExecutioner; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\ConditionDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\DecisionDispatcher; use Mautic\CampaignBundle\Executioner\EventExecutioner; -use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\InactiveExecutioner; use Mautic\CampaignBundle\Executioner\KickoffExecutioner; +use Mautic\CampaignBundle\Executioner\RealTimeExecutioner; use Mautic\CampaignBundle\Executioner\Result\Counter; use Mautic\CampaignBundle\Executioner\Result\Responses; use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; @@ -45,9 +46,9 @@ class LegacyEventModel extends CommonFormModel { /** - * @var DecisionExecutioner + * @var RealTimeExecutioner */ - private $decisionExecutioner; + private $realTimeExecutioner; /** * @var KickoffExecutioner @@ -70,19 +71,24 @@ class LegacyEventModel extends CommonFormModel private $eventExecutioner; /** - * @var EventDispatcher + * @var EventCollector */ - private $eventDispatcher; + private $eventCollector; /** - * @var EventCollector + * @var ActionDispatcher */ - private $eventCollector; + private $actionDispatcher; /** - * @var NotificationHelper + * @var ConditionDispatcher */ - private $notificationHelper; + private $conditionDispatcher; + + /** + * @var DecisionDispatcher + */ + private $decisionDispatcher; /** * @var @@ -115,19 +121,22 @@ class LegacyEventModel extends CommonFormModel protected $notificationModel; /** - * EventModel constructor. + * LegacyEventModel constructor. * - * @param IpLookupHelper $ipLookupHelper - * @param LeadModel $leadModel * @param UserModel $userModel * @param NotificationModel $notificationModel * @param CampaignModel $campaignModel - * @param DecisionExecutioner $decisionExecutioner + * @param LeadModel $leadModel + * @param IpLookupHelper $ipLookupHelper + * @param RealTimeExecutioner $realTimeExecutioner * @param KickoffExecutioner $kickoffExecutioner * @param ScheduledExecutioner $scheduledExecutioner * @param InactiveExecutioner $inactiveExecutioner * @param EventExecutioner $eventExecutioner - * @param EventDispatcher $eventDispatcher + * @param EventCollector $eventCollector + * @param ActionDispatcher $actionDispatcher + * @param ConditionDispatcher $conditionDispatcher + * @param DecisionDispatcher $decisionDispatcher */ public function __construct( UserModel $userModel, @@ -135,26 +144,30 @@ public function __construct( CampaignModel $campaignModel, LeadModel $leadModel, IpLookupHelper $ipLookupHelper, - DecisionExecutioner $decisionExecutioner, + RealTimeExecutioner $realTimeExecutioner, KickoffExecutioner $kickoffExecutioner, ScheduledExecutioner $scheduledExecutioner, InactiveExecutioner $inactiveExecutioner, EventExecutioner $eventExecutioner, - EventDispatcher $eventDispatcher, - EventCollector $eventCollector + EventCollector $eventCollector, + ActionDispatcher $actionDispatcher, + ConditionDispatcher $conditionDispatcher, + DecisionDispatcher $decisionDispatcher ) { $this->userModel = $userModel; $this->notificationModel = $notificationModel; $this->campaignModel = $campaignModel; $this->leadModel = $leadModel; $this->ipLookupHelper = $ipLookupHelper; - $this->decisionExecutioner = $decisionExecutioner; + $this->realTimeExecutioner = $realTimeExecutioner; $this->kickoffExecutioner = $kickoffExecutioner; $this->scheduledExecutioner = $scheduledExecutioner; $this->inactiveExecutioner = $inactiveExecutioner; $this->eventExecutioner = $eventExecutioner; - $this->eventDispatcher = $eventDispatcher; $this->eventCollector = $eventCollector; + $this->actionDispatcher = $actionDispatcher; + $this->conditionDispatcher = $conditionDispatcher; + $this->decisionDispatcher = $decisionDispatcher; } /** @@ -294,7 +307,7 @@ public function triggerNegativeEvents( */ public function triggerEvent($type, $eventDetails = null, $channel = null, $channelId = null) { - $response = $this->decisionExecutioner->execute($type, $eventDetails, $channel, $channelId); + $response = $this->realTimeExecutioner->execute($type, $eventDetails, $channel, $channelId); return $response->getResponseArray(); } @@ -389,17 +402,17 @@ public function invokeEventCallback($event, $settings, $lead = null, $eventDetai case Event::TYPE_ACTION: $logs = new ArrayCollection([$log]); /* @var ActionAccessor $config */ - $this->eventDispatcher->dispatchActionEvent($config, $event, $logs); + $this->actionDispatcher->dispatchEvent($config, $event, $logs); return !$log->getFailedLog(); case Event::TYPE_CONDITION: /** @var ConditionAccessor $config */ - $eventResult = $this->eventDispatcher->dispatchConditionEvent($config, $log); + $eventResult = $this->conditionDispatcher->dispatchEvent($config, $log); return $eventResult->getResult(); case Event::TYPE_DECISION: /** @var DecisionAccessor $config */ - $eventResult = $this->eventDispatcher->dispatchDecisionEvent($config, $log, $eventDetails); + $eventResult = $this->decisionDispatcher->dispatchEvent($config, $log, $eventDetails); return $eventResult->getResult(); } diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php similarity index 79% rename from app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php rename to app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php index 64ac37111d4..9bcf18302b1 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/EventDispatcherTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php @@ -25,6 +25,7 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; +use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher; use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher; @@ -35,7 +36,7 @@ use Psr\Log\NullLogger; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -class EventDispatcherTest extends \PHPUnit_Framework_TestCase +class ActionDispatcherTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPUnit_Framework_MockObject_MockBuilder|EventDispatcherInterface @@ -159,7 +160,7 @@ function ($eventName, PendingEvent $pendingEvent) use ($logs) { $this->legacyDispatcher->expects($this->once()) ->method('dispatchExecutionEvents'); - $this->getEventDispatcher()->dispatchActionEvent($config, $event, $logs); + $this->getEventDispatcher()->dispatchEvent($config, $event, $logs); } public function testActionLogNotProcessedExceptionIsThrownIfLogNotProcessedWithSuccess() @@ -225,7 +226,7 @@ function ($eventName, PendingEvent $pendingEvent) use ($logs) { } ); - $this->getEventDispatcher()->dispatchActionEvent($config, $event, $logs); + $this->getEventDispatcher()->dispatchEvent($config, $event, $logs); } public function testActionLogNotProcessedExceptionIsThrownIfLogNotProcessedWithFailed() @@ -291,7 +292,7 @@ function ($eventName, PendingEvent $pendingEvent) use ($logs) { } ); - $this->getEventDispatcher()->dispatchActionEvent($config, $event, $logs); + $this->getEventDispatcher()->dispatchEvent($config, $event, $logs); } public function testActionBatchEventIsIgnoredWithLegacy() @@ -312,65 +313,7 @@ public function testActionBatchEventIsIgnoredWithLegacy() $this->legacyDispatcher->expects($this->once()) ->method('dispatchCustomEvent'); - $this->getEventDispatcher()->dispatchActionEvent($config, $event, new ArrayCollection()); - } - - public function testDecisionEventIsDispatched() - { - $config = $this->getMockBuilder(DecisionAccessor::class) - ->disableOriginalConstructor() - ->getMock(); - - $config->expects($this->once()) - ->method('getEventName') - ->willReturn('something'); - - $this->legacyDispatcher->expects($this->once()) - ->method('dispatchDecisionEvent'); - - $this->dispatcher->expects($this->at(0)) - ->method('dispatch') - ->with('something', $this->isInstanceOf(DecisionEvent::class)); - - $this->dispatcher->expects($this->at(1)) - ->method('dispatch') - ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $this->isInstanceOf(DecisionEvent::class)); - - $this->getEventDispatcher()->dispatchDecisionEvent($config, new LeadEventLog(), null); - } - - public function testDecisionResultsEventIsDispatched() - { - $config = $this->getMockBuilder(DecisionAccessor::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->dispatcher->expects($this->at(0)) - ->method('dispatch') - ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, $this->isInstanceOf(DecisionResultsEvent::class)); - - $this->getEventDispatcher()->dispatchDecisionResultsEvent($config, new ArrayCollection(), new EvaluatedContacts()); - } - - public function testConditionEventIsDispatched() - { - $config = $this->getMockBuilder(ConditionAccessor::class) - ->disableOriginalConstructor() - ->getMock(); - - $config->expects($this->once()) - ->method('getEventName') - ->willReturn('something'); - - $this->dispatcher->expects($this->at(0)) - ->method('dispatch') - ->with('something', $this->isInstanceOf(ConditionEvent::class)); - - $this->dispatcher->expects($this->at(1)) - ->method('dispatch') - ->with(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $this->isInstanceOf(ConditionEvent::class)); - - $this->getEventDispatcher()->dispatchConditionEvent($config, new LeadEventLog()); + $this->getEventDispatcher()->dispatchEvent($config, $event, new ArrayCollection()); } /** @@ -378,7 +321,7 @@ public function testConditionEventIsDispatched() */ private function getEventDispatcher() { - return new EventDispatcher( + return new ActionDispatcher( $this->dispatcher, new NullLogger(), $this->scheduler, diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ConditionDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ConditionDispatcherTest.php new file mode 100644 index 00000000000..58f5ce0a9ea --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ConditionDispatcherTest.php @@ -0,0 +1,63 @@ +dispatcher = $this->getMockBuilder(EventDispatcherInterface::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testConditionEventIsDispatched() + { + $config = $this->getMockBuilder(ConditionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getEventName') + ->willReturn('something'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(ConditionEvent::class)); + + $this->dispatcher->expects($this->at(1)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_CONDITION_EVALUATION, $this->isInstanceOf(ConditionEvent::class)); + + $this->getEventDispatcher()->dispatchEvent($config, new LeadEventLog()); + } + + /** + * @return ConditionDispatcher + */ + private function getEventDispatcher() + { + return new ConditionDispatcher($this->dispatcher); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php new file mode 100644 index 00000000000..06707bd752f --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php @@ -0,0 +1,92 @@ +dispatcher = $this->getMockBuilder(EventDispatcherInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->legacyDispatcher = $this->getMockBuilder(LegacyEventDispatcher::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testDecisionEventIsDispatched() + { + $config = $this->getMockBuilder(DecisionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->once()) + ->method('getEventName') + ->willReturn('something'); + + $this->legacyDispatcher->expects($this->once()) + ->method('dispatchDecisionEvent'); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with('something', $this->isInstanceOf(DecisionEvent::class)); + + $this->dispatcher->expects($this->at(1)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $this->isInstanceOf(DecisionEvent::class)); + + $this->getEventDispatcher()->dispatchEvent($config, new LeadEventLog(), null); + } + + public function testDecisionResultsEventIsDispatched() + { + $config = $this->getMockBuilder(DecisionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->dispatcher->expects($this->at(0)) + ->method('dispatch') + ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION_RESULTS, $this->isInstanceOf(DecisionResultsEvent::class)); + + $this->getEventDispatcher()->dispatchDecisionResultsEvent($config, new ArrayCollection(), new EvaluatedContacts()); + } + + /** + * @return DecisionDispatcher + */ + public function getEventDispatcher() + { + return new DecisionDispatcher($this->dispatcher, $this->legacyDispatcher); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php similarity index 91% rename from app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php rename to app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php index 232f43680b0..fb093599972 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/DecisionExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php @@ -16,7 +16,8 @@ use Mautic\CampaignBundle\Entity\EventRepository; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; -use Mautic\CampaignBundle\Executioner\DecisionExecutioner; +use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner; +use Mautic\CampaignBundle\Executioner\RealTimeExecutioner; use Mautic\CampaignBundle\Executioner\Event\Decision; use Mautic\CampaignBundle\Executioner\EventExecutioner; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; @@ -25,7 +26,7 @@ use Mautic\LeadBundle\Tracker\ContactTracker; use Psr\Log\NullLogger; -class DecisionExecutionerTest extends \PHPUnit_Framework_TestCase +class RealTimeExecutionerTest extends \PHPUnit_Framework_TestCase { /** * @var \PHPUnit_Framework_MockObject_MockObject|LeadModel @@ -43,7 +44,7 @@ class DecisionExecutionerTest extends \PHPUnit_Framework_TestCase private $executioner; /** - * @var \PHPUnit_Framework_MockObject_MockObject|Decision + * @var \PHPUnit_Framework_MockObject_MockObject|DecisionExecutioner */ private $decisionExecutioner; @@ -76,7 +77,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->decisionExecutioner = $this->getMockBuilder(Decision::class) + $this->decisionExecutioner = $this->getMockBuilder(DecisionExecutioner::class) ->disableOriginalConstructor() ->getMock(); @@ -102,7 +103,7 @@ public function testContactNotFoundResultsInEmptyResponses() $this->eventRepository->expects($this->never()) ->method('getContactPendingEvents'); - $responses = $this->getDecisionExecutioner()->execute('something'); + $responses = $this->getExecutioner()->execute('something'); $this->assertEquals(0, $responses->containsResponses()); } @@ -126,7 +127,7 @@ public function testNoRelatedEventsResultInEmptyResponses() $this->eventCollector->expects($this->never()) ->method('getEventConfig'); - $responses = $this->getDecisionExecutioner()->execute('something'); + $responses = $this->getExecutioner()->execute('something'); $this->assertEquals(0, $responses->containsResponses()); } @@ -156,7 +157,7 @@ public function testChannelMisMatchResultsInEmptyResponses() $this->eventCollector->expects($this->never()) ->method('getEventConfig'); - $responses = $this->getDecisionExecutioner()->execute('something', null, 'page'); + $responses = $this->getExecutioner()->execute('something', null, 'page'); $this->assertEquals(0, $responses->containsResponses()); } @@ -189,7 +190,7 @@ public function testChannelIdMisMatchResultsInEmptyResponses() $this->eventCollector->expects($this->never()) ->method('getEventConfig'); - $responses = $this->getDecisionExecutioner()->execute('something', null, 'email', 1); + $responses = $this->getExecutioner()->execute('something', null, 'email', 1); $this->assertEquals(0, $responses->containsResponses()); } @@ -229,7 +230,7 @@ public function testEmptyPositiveactionsResultsInEmptyResponses() $this->decisionExecutioner->expects($this->once()) ->method('evaluateForContact'); - $responses = $this->getDecisionExecutioner()->execute('something', null, 'email', 3); + $responses = $this->getExecutioner()->execute('something', null, 'email', 3); $this->assertEquals(0, $responses->containsResponses()); } @@ -298,17 +299,17 @@ public function testAssociatedEventsAreExecuted() $this->executioner->expects($this->once()) ->method('executeForContact'); - $responses = $this->getDecisionExecutioner()->execute('something', null, 'email', 3); + $responses = $this->getExecutioner()->execute('something', null, 'email', 3); $this->assertEquals(0, $responses->containsResponses()); } /** - * @return DecisionExecutioner + * @return RealTimeExecutioner */ - private function getDecisionExecutioner() + private function getExecutioner() { - return new DecisionExecutioner( + return new RealTimeExecutioner( new NullLogger(), $this->leadModel, $this->eventRepository, diff --git a/app/bundles/ChannelBundle/Config/config.php b/app/bundles/ChannelBundle/Config/config.php index 9ba4fc44fc7..fd0fcebd95b 100644 --- a/app/bundles/ChannelBundle/Config/config.php +++ b/app/bundles/ChannelBundle/Config/config.php @@ -64,7 +64,7 @@ 'class' => Mautic\ChannelBundle\EventListener\CampaignSubscriber::class, 'arguments' => [ 'mautic.channel.model.message', - 'mautic.campaign.event_dispatcher', + 'mautic.campaign.dispatcher.action', 'mautic.campaign.event_collector', 'monolog.logger.mautic', 'translator', diff --git a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php index 59d003bcdb4..f5ab9caeac7 100644 --- a/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/ChannelBundle/EventListener/CampaignSubscriber.php @@ -19,7 +19,7 @@ use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; use Mautic\ChannelBundle\ChannelEvents; use Mautic\ChannelBundle\Model\MessageModel; @@ -39,9 +39,9 @@ class CampaignSubscriber implements EventSubscriberInterface protected $messageModel; /** - * @var EventDispatcher + * @var ActionDispatcher */ - private $eventDispatcher; + private $actionDispatcher; /** * @var EventCollector @@ -82,23 +82,23 @@ class CampaignSubscriber implements EventSubscriberInterface * CampaignSubscriber constructor. * * @param MessageModel $messageModel - * @param EventDispatcher $eventDispatcher + * @param ActionDispatcher $actionDispatcher * @param EventCollector $collector * @param LoggerInterface $logger * @param TranslatorInterface $translator */ public function __construct( MessageModel $messageModel, - EventDispatcher $eventDispatcher, + ActionDispatcher $actionDispatcher, EventCollector $collector, LoggerInterface $logger, TranslatorInterface $translator ) { - $this->messageModel = $messageModel; - $this->eventDispatcher = $eventDispatcher; - $this->eventCollector = $collector; - $this->logger = $logger; - $this->translator = $translator; + $this->messageModel = $messageModel; + $this->actionDispatcher = $actionDispatcher; + $this->eventCollector = $collector; + $this->logger = $logger; + $this->translator = $translator; } /** @@ -232,7 +232,7 @@ protected function sendChannelMessage(ArrayCollection $logs, $channel, array $me $pendingEvent = new PendingEvent($config, $this->pseudoEvent, $logs); $pendingEvent->setChannel('campaign.event', $messageChannel['channel_id']); - $this->eventDispatcher->dispatchActionEvent( + $this->actionDispatcher->dispatchEvent( $config, $this->pseudoEvent, $logs, From 3d88cc59858f95c2f33f1e08f0cbbd4396b4735c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 08:50:07 -0500 Subject: [PATCH 476/778] Fixed tests and typehints --- .../Executioner/Dispatcher/ActionDispatcherTest.php | 9 +-------- .../Tests/EventListener/CampaignSubscriberTest.php | 6 +++--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php index 9bcf18302b1..52b47a3285d 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/ActionDispatcherTest.php @@ -15,22 +15,15 @@ use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; -use Mautic\CampaignBundle\Event\ConditionEvent; -use Mautic\CampaignBundle\Event\DecisionEvent; -use Mautic\CampaignBundle\Event\DecisionResultsEvent; use Mautic\CampaignBundle\Event\ExecutedBatchEvent; use Mautic\CampaignBundle\Event\ExecutedEvent; use Mautic\CampaignBundle\Event\FailedEvent; use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; -use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; -use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher; use Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException; use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher; use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; -use Mautic\CampaignBundle\Executioner\Result\EvaluatedContacts; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; use Psr\Log\NullLogger; @@ -317,7 +310,7 @@ public function testActionBatchEventIsIgnoredWithLegacy() } /** - * @return EventDispatcher + * @return ActionDispatcher */ private function getEventDispatcher() { diff --git a/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php b/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php index d0f10fceedb..7c964bd56cd 100644 --- a/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php +++ b/app/bundles/ChannelBundle/Tests/EventListener/CampaignSubscriberTest.php @@ -19,7 +19,7 @@ use Mautic\CampaignBundle\Event\PendingEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; -use Mautic\CampaignBundle\Executioner\Dispatcher\EventDispatcher as CampaignEventDispatcher; +use Mautic\CampaignBundle\Executioner\Dispatcher\ActionDispatcher; use Mautic\CampaignBundle\Executioner\Dispatcher\LegacyEventDispatcher; use Mautic\CampaignBundle\Executioner\Helper\NotificationHelper; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; @@ -49,7 +49,7 @@ class CampaignSubscriberTest extends \PHPUnit_Framework_TestCase private $messageModel; /** - * @var CampaignEventDispatcher + * @var ActionDispatcher */ private $eventDispatcher; @@ -150,7 +150,7 @@ protected function setUp() $factory ); - $this->eventDispatcher = new CampaignEventDispatcher( + $this->eventDispatcher = new ActionDispatcher( $this->dispatcher, new NullLogger(), $this->scheduler, From d7310f072d25697d73dad5522418d8d670ad3bc9 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 9 May 2018 15:19:10 -0500 Subject: [PATCH 477/778] CS fixes --- .../Executioner/Dispatcher/ActionDispatcher.php | 2 +- .../CampaignBundle/Executioner/Event/ActionExecutioner.php | 5 +++-- .../CampaignBundle/Executioner/RealTimeExecutioner.php | 2 +- .../Tests/Executioner/RealTimeExecutionerTest.php | 3 +-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php index 9d7ee7da75f..a3e0d03af1d 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/ActionDispatcher.php @@ -187,4 +187,4 @@ private function validateProcessedLogs(ArrayCollection $pending, ArrayCollection } } } -} \ No newline at end of file +} diff --git a/app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php index 1c2fa273e16..ebd02079712 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/ActionExecutioner.php @@ -37,8 +37,8 @@ class ActionExecutioner implements EventInterface /** * ActionExecutioner constructor. * - * @param ActionDispatcher $dispatcher - * @param EventLogger $eventLogger + * @param ActionDispatcher $dispatcher + * @param EventLogger $eventLogger */ public function __construct(ActionDispatcher $dispatcher, EventLogger $eventLogger) { @@ -51,6 +51,7 @@ public function __construct(ActionDispatcher $dispatcher, EventLogger $eventLogg * @param ArrayCollection $logs * * @return EvaluatedContacts + * * @throws CannotProcessEventException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException * @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException diff --git a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php index d693699cb38..1d67b2ce4e9 100644 --- a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php @@ -90,7 +90,7 @@ class RealTimeExecutioner * @param LeadModel $leadModel * @param EventRepository $eventRepository * @param EventExecutioner $executioner - * @param Executioner $decisionExecutioner + * @param Executioner $decisionExecutioner * @param EventCollector $collector * @param EventScheduler $scheduler * @param ContactTracker $contactTracker diff --git a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php index fb093599972..eeb5d040582 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php @@ -17,9 +17,8 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\Event\DecisionExecutioner; -use Mautic\CampaignBundle\Executioner\RealTimeExecutioner; -use Mautic\CampaignBundle\Executioner\Event\Decision; use Mautic\CampaignBundle\Executioner\EventExecutioner; +use Mautic\CampaignBundle\Executioner\RealTimeExecutioner; use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\LeadModel; From a79b1cae72599b6bae8b6e330efa24212c1affe9 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 09:49:46 -0500 Subject: [PATCH 478/778] Made CSV to array reusable by leveraging FormatterHelper --- .../Command/ContactIdsInputTrait.php | 37 ------------------- .../Command/ExecuteEventCommand.php | 23 ++++++------ .../Command/TriggerCampaignCommand.php | 14 +++++-- .../Command/ValidateEventCommand.php | 13 +++++-- app/bundles/CampaignBundle/Config/config.php | 3 ++ .../Templating/Helper/FormatterHelper.php | 23 ++++++++++++ 6 files changed, 59 insertions(+), 54 deletions(-) delete mode 100644 app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php diff --git a/app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php b/app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php deleted file mode 100644 index 91866b2fcde..00000000000 --- a/app/bundles/CampaignBundle/Command/ContactIdsInputTrait.php +++ /dev/null @@ -1,37 +0,0 @@ -getOption('contact-ids'); - if ($string) { - return array_map( - function ($id) { - return (int) trim($id); - }, - explode(',', $string) - ); - } - - return []; - } -} diff --git a/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php b/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php index 8a7aa051513..0f3afd3d76a 100644 --- a/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php +++ b/app/bundles/CampaignBundle/Command/ExecuteEventCommand.php @@ -1,7 +1,7 @@ scheduledExecutioner = $scheduledExecutioner; $this->translator = $translator; + $this->formatterHelper = $formatterHelper; } /** @@ -78,15 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output) { defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); - $scheduledLogIds = $input->getOption('scheduled-log-ids'); - - $ids = array_map( - function ($id) { - return (int) trim($id); - }, - explode(',', $scheduledLogIds) - ); - + $ids = $this->formatterHelper->simpleCsvToArray($input->getOption('scheduled-log-ids'), 'int'); $counter = $this->scheduledExecutioner->executeByIds($ids, $output); $this->writeCounts($output, $this->translator, $counter); diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 3a83995a472..c5d3d0ac7cd 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -21,6 +21,7 @@ use Mautic\CampaignBundle\Executioner\ScheduledExecutioner; use Mautic\CampaignBundle\Model\CampaignModel; use Mautic\CoreBundle\Command\ModeratedCommand; +use Mautic\CoreBundle\Templating\Helper\FormatterHelper; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -34,7 +35,6 @@ */ class TriggerCampaignCommand extends ModeratedCommand { - use ContactIdsInputTrait; use WriteCountTrait; /** @@ -77,6 +77,11 @@ class TriggerCampaignCommand extends ModeratedCommand */ private $logger; + /** + * @var FormatterHelper + */ + private $formatterHelper; + /** * @var OutputInterface */ @@ -118,6 +123,7 @@ class TriggerCampaignCommand extends ModeratedCommand * @param InactiveExecutioner $inactiveExecutioner * @param EntityManagerInterface $em * @param LoggerInterface $logger + * @param FormatterHelper $formatterHelper */ public function __construct( CampaignModel $campaignModel, @@ -127,7 +133,8 @@ public function __construct( ScheduledExecutioner $scheduledExecutioner, InactiveExecutioner $inactiveExecutioner, EntityManagerInterface $em, - LoggerInterface $logger + LoggerInterface $logger, + FormatterHelper $formatterHelper ) { parent::__construct(); @@ -139,6 +146,7 @@ public function __construct( $this->inactiveExecutioner = $inactiveExecutioner; $this->em = $em; $this->logger = $logger; + $this->formatterHelper = $formatterHelper; } /** @@ -238,7 +246,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $contactMinId = $input->getOption('min-contact-id'); $contactMaxId = $input->getOption('max-contact-id'); $contactId = $input->getOption('contact-id'); - $contactIds = $this->getContactIds($input); + $contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int'); $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds); diff --git a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php index d4e10e95189..e7ad3c21662 100644 --- a/app/bundles/CampaignBundle/Command/ValidateEventCommand.php +++ b/app/bundles/CampaignBundle/Command/ValidateEventCommand.php @@ -13,6 +13,7 @@ use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\InactiveExecutioner; +use Mautic\CoreBundle\Templating\Helper\FormatterHelper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -24,7 +25,6 @@ */ class ValidateEventCommand extends Command { - use ContactIdsInputTrait; use WriteCountTrait; /** @@ -37,18 +37,25 @@ class ValidateEventCommand extends Command */ private $translator; + /** + * @var FormatterHelper + */ + private $formatterHelper; + /** * ValidateEventCommand constructor. * * @param InactiveExecutioner $inactiveExecutioner * @param TranslatorInterface $translator + * @param FormatterHelper $formatterHelper */ - public function __construct(InactiveExecutioner $inactiveExecutioner, TranslatorInterface $translator) + public function __construct(InactiveExecutioner $inactiveExecutioner, TranslatorInterface $translator, FormatterHelper $formatterHelper) { parent::__construct(); $this->inactiveExecution = $inactiveExecutioner; $this->translator = $translator; + $this->formatterHelper = $formatterHelper; } /** @@ -95,7 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $decisionId = $input->getOption('decision-id'); $contactId = $input->getOption('contact-id'); - $contactIds = $this->getContactIds($input); + $contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int'); if (!$contactIds && !$contactId) { $output->writeln( diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 5cdb2c309d2..ff7af837b1d 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -488,6 +488,7 @@ 'mautic.campaign.executioner.inactive', 'doctrine.orm.entity_manager', 'monolog.logger.mautic', + 'mautic.helper.template.formatter', ], 'tag' => 'console.command', ], @@ -496,6 +497,7 @@ 'arguments' => [ 'mautic.campaign.executioner.scheduled', 'translator', + 'mautic.helper.template.formatter', ], 'tag' => 'console.command', ], @@ -504,6 +506,7 @@ 'arguments' => [ 'mautic.campaign.executioner.inactive', 'translator', + 'mautic.helper.template.formatter', ], 'tag' => 'console.command', ], diff --git a/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php b/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php index a50711e525a..6b594abf1e9 100644 --- a/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php +++ b/app/bundles/CoreBundle/Templating/Helper/FormatterHelper.php @@ -167,6 +167,29 @@ public function simpleArrayToHtml(array $array, $delimeter = '
') return implode($delimeter, $pairs); } + /** + * Takes a simple csv list like 1,2,3,4 and returns as an array. + * + * @param $csv + * + * @return array + */ + public function simpleCsvToArray($csv, $type = null) + { + if (!$csv) { + return []; + } + + return array_map( + function ($value) use ($type) { + $value = trim($value); + + return $this->_($value, $type); + }, + explode(',', $csv) + ); + } + /** * @return string */ From c8310b005097b683a2a4c2f4dc8a7760d63ba36c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 11:39:34 -0500 Subject: [PATCH 479/778] Use new class for getting tracked contact --- app/bundles/CampaignBundle/Config/config.php | 2 +- .../Executioner/Logger/EventLogger.php | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index ff7af837b1d..a1ce0710ad5 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -328,7 +328,7 @@ 'class' => \Mautic\CampaignBundle\Executioner\Logger\EventLogger::class, 'arguments' => [ 'mautic.helper.ip_lookup', - 'mautic.lead.model.lead', + 'mautic.tracker.contact', 'mautic.campaign.repository.lead_event_log', ], ], diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index 3d5e0b12737..1bb9d46da47 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -18,7 +18,8 @@ use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\Helper\ChannelExtractor; use Mautic\CoreBundle\Helper\IpLookupHelper; -use Mautic\LeadBundle\Model\LeadModel; +use Mautic\LeadBundle\Entity\Lead; +use Mautic\LeadBundle\Tracker\ContactTracker; class EventLogger { @@ -28,9 +29,9 @@ class EventLogger private $ipLookupHelper; /** - * @var LeadModel + * @var ContactTracker */ - private $leadModel; + private $contactTracker; /** * @var LeadEventLogRepository @@ -51,14 +52,14 @@ class EventLogger * LogHelper constructor. * * @param IpLookupHelper $ipLookupHelper - * @param LeadModel $leadModel + * @param ContactTracker $contactTracker * @param LeadEventLogRepository $repo */ - public function __construct(IpLookupHelper $ipLookupHelper, LeadModel $leadModel, LeadEventLogRepository $repo) + public function __construct(IpLookupHelper $ipLookupHelper, ContactTracker $contactTracker, LeadEventLogRepository $repo) { - $this->ipLookupHelper = $ipLookupHelper; - $this->leadModel = $leadModel; - $this->repo = $repo; + $this->ipLookupHelper = $ipLookupHelper; + $this->contactTracker = $contactTracker; + $this->repo = $repo; $this->queued = new ArrayCollection(); $this->processed = new ArrayCollection(); @@ -85,13 +86,13 @@ public function persistLog(LeadEventLog $log) } /** - * @param Event $event - * @param null $lead - * @param bool $isInactiveEvent + * @param Event $event + * @param Lead|null $lead + * @param bool $isInactiveEvent * * @return LeadEventLog */ - public function buildLogEntry(Event $event, $lead = null, $isInactiveEvent = false) + public function buildLogEntry(Event $event, Lead $lead = null, $isInactiveEvent = false) { $log = new LeadEventLog(); @@ -100,8 +101,8 @@ public function buildLogEntry(Event $event, $lead = null, $isInactiveEvent = fal $log->setEvent($event); $log->setCampaign($event->getCampaign()); - if ($lead == null) { - $lead = $this->leadModel->getCurrentLead(); + if (null === $lead) { + $lead = $this->contactTracker->getContact(); } $log->setLead($lead); From 8f71820bfc4672d35afa6d2995ea5b66b179642f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 11:40:39 -0500 Subject: [PATCH 480/778] Fixed check for sent email to contact --- .../EmailBundle/Entity/StatRepository.php | 52 +++++++++++-------- .../EventListener/CampaignSubscriber.php | 4 +- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index b9327b1dd08..c81314091d3 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -541,18 +541,13 @@ public function findContactEmailStats($leadId, $emailId) } /** - * @param $contacts - * @param $emailId - * @param bool $organizeByContact + * @param $contacts + * @param $emailId * - * @return array|mixed|string + * @return mixed */ - public function checkContactsSentEmail($contacts, $emailId, $organizeByContact = false) + public function checkContactsSentEmail($contacts, $emailId) { - if (is_array($contacts)) { - $contacts = implode(',', $contacts); - } - $query = $this->getEntityManager()->getConnection()->createQueryBuilder(); $query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's'); $query->select('id, lead_id') @@ -560,24 +555,39 @@ public function checkContactsSentEmail($contacts, $emailId, $organizeByContact = ->andWhere('s.lead_id in (:contacts)') ->andWhere('is_failed = 0') ->setParameter(':email', $emailId) - ->setParameter(':contacts', $contacts, Connection::PARAM_INT_ARRAY) - ->groupBy('lead_id'); + ->setParameter(':contacts', $contacts); $results = $query->execute()->fetch(); - if ($organizeByContact) { - $contacts = []; - foreach ($results as $result) { - if (!isset($contacts[$result['lead_id']])) { - $contacts[$result['lead_id']] = []; - } + return $results; + } - $contacts[$result['lead_id']][] = $result['id']; - } + /** + * @param array $contacts + * @param $emailId + * @param bool $organizeByContact + * + * @return array Formatted as [contactId => sentCount] + */ + public function getSentCountForContacts(array $contacts, $emailId) + { + $query = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's'); + $query->select('count(s.id) as sent_count, s.lead_id') + ->where('s.email_id = :email') + ->andWhere('s.lead_id in (:contacts)') + ->andWhere('s.is_failed = 0') + ->setParameter(':email', $emailId) + ->setParameter(':contacts', $contacts, Connection::PARAM_INT_ARRAY) + ->groupBy('s.lead_id'); - return $contacts; + $results = $query->execute()->fetchAll(); + + $contacts = []; + foreach ($results as $result) { + $contacts[$result['lead_id']] = $result['sent_count']; } - return $results; + return $contacts; } } diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 4eb7f8c107f..80b33e2828d 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -328,9 +328,9 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) if ('marketing' == $type) { // Determine if this lead has received the email before and if so, don't send it again - $stats = $this->emailModel->getStatRepository()->checkContactsSentEmail($contactIds, $emailId, true); + $stats = $this->emailModel->getStatRepository()->getSentCountForContacts($contactIds, $emailId); - foreach ($stats as $contactId => $sent) { + foreach ($stats as $contactId => $sentCount) { /** @var LeadEventLog $log */ $log = $event->findLogByContactId($contactId); $event->fail( From af603b58f5966e5624e3234d85b2c8ef26b1c91f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 11:41:00 -0500 Subject: [PATCH 481/778] Docblock/typehint fixes --- .../Entity/CampaignRepository.php | 7 ++- .../Entity/ChannelInterface.php | 4 +- .../CampaignBundle/Entity/LeadEventLog.php | 2 +- .../Entity/LeadEventLogRepository.php | 9 +-- .../CampaignBundle/Entity/LeadRepository.php | 12 ++-- .../Event/AbstractLogCollectionEvent.php | 4 +- .../CampaignBundle/Event/ConditionEvent.php | 5 +- .../CampaignBundle/Event/ContextTrait.php | 2 +- .../CampaignBundle/Event/DecisionEvent.php | 10 ++-- .../CampaignBundle/Event/ExecutedEvent.php | 3 +- .../CampaignBundle/Event/FailedEvent.php | 5 +- .../CampaignBundle/Event/PendingEvent.php | 12 ++-- .../CampaignBundle/Event/ScheduledEvent.php | 1 + .../Accessor/Event/AbstractEventAccessor.php | 2 +- .../Accessor/Event/ConditionAccessor.php | 7 ++- .../Accessor/Event/DecisionAccessor.php | 7 ++- .../EventCollector/Accessor/EventAccessor.php | 17 ++++-- .../Builder/ConnectionBuilder.php | 20 +++---- .../EventCollector/EventCollector.php | 6 +- .../ContactFinder/InactiveContactFinder.php | 18 +++--- .../ContactFinder/KickoffContactFinder.php | 10 ++-- .../ContactFinder/Limiter/ContactLimiter.php | 10 ++-- .../Event/ConditionExecutioner.php | 2 +- .../Executioner/Event/DecisionExecutioner.php | 8 +-- .../Executioner/Helper/DecisionTreeHelper.php | 56 ------------------- .../Executioner/Helper/NotificationHelper.php | 9 +-- .../Executioner/InactiveExecutioner.php | 4 +- .../Executioner/RealTimeExecutioner.php | 8 +-- .../Executioner/Result/Responses.php | 12 ++-- .../Executioner/ScheduledExecutioner.php | 2 +- .../Executioner/Scheduler/EventScheduler.php | 4 +- .../Executioner/Scheduler/Mode/DateTime.php | 2 +- .../Executioner/Scheduler/Mode/Interval.php | 2 +- .../Scheduler/Mode/ScheduleModeInterface.php | 2 +- .../Helper/RemovedContactTracker.php | 6 +- .../Tests/CampaignTestAbstract.php | 8 +-- .../Executioner/ScheduledExecutionerTest.php | 2 +- .../PreferenceBuilder/ChannelPreferences.php | 16 ++++-- .../PreferenceBuilder/PreferenceBuilder.php | 53 ++++++++++-------- .../CoreBundle/Helper/DateTimeHelper.php | 4 +- 40 files changed, 178 insertions(+), 195 deletions(-) delete mode 100644 app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php diff --git a/app/bundles/CampaignBundle/Entity/CampaignRepository.php b/app/bundles/CampaignBundle/Entity/CampaignRepository.php index cbf5985e3c6..3048f7ebe70 100644 --- a/app/bundles/CampaignBundle/Entity/CampaignRepository.php +++ b/app/bundles/CampaignBundle/Entity/CampaignRepository.php @@ -11,6 +11,7 @@ namespace Mautic\CampaignBundle\Entity; +use Doctrine\DBAL\Types\Type; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CoreBundle\Entity\CommonRepository; @@ -660,11 +661,11 @@ public function getPendingContactIds($campaignId, ContactLimiter $limiter) /** * Get a count of leads that belong to the campaign. * - * @param $campaignId + * @param int $campaignId * @param int $leadId Optional lead ID to check if lead is part of campaign * @param array $pendingEvents List of specific events to rule out * - * @return mixed + * @return int */ public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents = []) { @@ -678,7 +679,7 @@ public function getCampaignLeadCount($campaignId, $leadId = null, $pendingEvents $q->expr()->eq('cl.manually_removed', ':false') ) ) - ->setParameter('false', false, 'boolean'); + ->setParameter('false', false, Type::BOOLEAN); if ($leadId) { $q->andWhere( diff --git a/app/bundles/CampaignBundle/Entity/ChannelInterface.php b/app/bundles/CampaignBundle/Entity/ChannelInterface.php index 4d537afeb11..1327c11df4d 100644 --- a/app/bundles/CampaignBundle/Entity/ChannelInterface.php +++ b/app/bundles/CampaignBundle/Entity/ChannelInterface.php @@ -21,7 +21,7 @@ public function getChannel(); /** * @param $channel * - * @return mixed + * @return ChannelInterface */ public function setChannel($channel); @@ -33,7 +33,7 @@ public function getChannelId(); /** * @param $id * - * @return mixed + * @return ChannelInterface */ public function setChannelId($id); } diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLog.php b/app/bundles/CampaignBundle/Entity/LeadEventLog.php index d15a8e5d5b2..9a10cf6a2c8 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLog.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLog.php @@ -438,7 +438,7 @@ public function getMetadata() public function appendToMetadata($metadata) { if (!is_array($metadata)) { - // Assumed output for timeline + // Assumed output for timeline BC for <2.14 $metadata = ['timeline' => $metadata]; } diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index 43a361640b2..a560bf845ab 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -12,6 +12,7 @@ namespace Mautic\CampaignBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\Types\Type; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CoreBundle\Entity\CommonRepository; use Mautic\CoreBundle\Helper\Chart\ChartQuery; @@ -386,7 +387,7 @@ public function getChartQuery($options) } /** - * @param $eventId + * @param int $eventId * @param \DateTime $now * @param ContactLimiter $limiter * @@ -412,7 +413,7 @@ public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter) ) ->setParameter('eventId', (int) $eventId) ->setParameter('now', $now) - ->setParameter('true', true, 'boolean'); + ->setParameter('true', true, Type::BOOLEAN); if ($contactId = $limiter->getContactId()) { $q->andWhere( @@ -445,7 +446,7 @@ public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter) * * @throws \Doctrine\ORM\Query\QueryException */ - public function getScheduledById(array $ids) + public function getScheduledByIds(array $ids) { $q = $this->createQueryBuilder('o'); @@ -465,7 +466,7 @@ public function getScheduledById(array $ids) } /** - * @param $campaignId + * @param int $campaignId * @param \DateTime $date * @param ContactLimiter $limiter * diff --git a/app/bundles/CampaignBundle/Entity/LeadRepository.php b/app/bundles/CampaignBundle/Entity/LeadRepository.php index 0abeabf0c35..66d5f135b97 100644 --- a/app/bundles/CampaignBundle/Entity/LeadRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadRepository.php @@ -192,10 +192,10 @@ public function checkLeadInCampaigns($lead, $options = []) } /** - * @param $campaignId - * @param $decisionId - * @param $parentDecisionId - * @param $startAtContactId + * @param int $campaignId + * @param int $decisionId + * @param int $parentDecisionId + * @param int $startAtContactId * @param ContactLimiter $limiter * * @return array @@ -289,8 +289,8 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, /** * This is approximate because the query that fetches contacts per decision is based on if the grandparent has been executed or not. * - * @param $decisionId - * @param $parentDecisionId + * @param int $decisionId + * @param int $parentDecisionId * @param null $specificContactId * * @return int diff --git a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php index 4e85125c9f4..531f7cf7be3 100644 --- a/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php +++ b/app/bundles/CampaignBundle/Event/AbstractLogCollectionEvent.php @@ -101,9 +101,9 @@ public function getContactIds() } /** - * @param $id + * @param int $id * - * @return mixed|null + * @return LeadEventLog * * @throws NoContactsFoundException */ diff --git a/app/bundles/CampaignBundle/Event/ConditionEvent.php b/app/bundles/CampaignBundle/Event/ConditionEvent.php index 43a4e6e936d..040d4aabbd6 100644 --- a/app/bundles/CampaignBundle/Event/ConditionEvent.php +++ b/app/bundles/CampaignBundle/Event/ConditionEvent.php @@ -38,7 +38,6 @@ class ConditionEvent extends CampaignExecutionEvent * * @param AbstractEventAccessor $config * @param LeadEventLog $log - * @param $passthrough */ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) { @@ -100,8 +99,8 @@ public function wasConditionSatisfied() } /** - * @param $channel - * @param null $channelId + * @param string $channel + * @param null|int $channelId */ public function setChannel($channel, $channelId = null) { diff --git a/app/bundles/CampaignBundle/Event/ContextTrait.php b/app/bundles/CampaignBundle/Event/ContextTrait.php index dc2c724fa65..e036c60f6e1 100644 --- a/app/bundles/CampaignBundle/Event/ContextTrait.php +++ b/app/bundles/CampaignBundle/Event/ContextTrait.php @@ -26,6 +26,6 @@ public function checkContext($eventType) $type = ($this->event instanceof \Mautic\CampaignBundle\Entity\Event) ? $this->event->getType() : $this->event['type']; - return strtolower($eventType) == strtolower($type); + return strtolower($eventType) === strtolower($type); } } diff --git a/app/bundles/CampaignBundle/Event/DecisionEvent.php b/app/bundles/CampaignBundle/Event/DecisionEvent.php index e45095bf631..95c812dc09f 100644 --- a/app/bundles/CampaignBundle/Event/DecisionEvent.php +++ b/app/bundles/CampaignBundle/Event/DecisionEvent.php @@ -30,6 +30,8 @@ class DecisionEvent extends CampaignExecutionEvent private $eventLog; /** + * Anything that the dispatching listener wants to pass through to other listeners. + * * @var mixed */ private $passthrough; @@ -108,8 +110,8 @@ public function wasDecisionApplicable() } /** - * @param $channel - * @param null $channelId + * @param string $channel + * @param null|int $channelId */ public function setChannel($channel, $channelId = null) { @@ -130,13 +132,13 @@ public function getResult() /** * @deprecated 2.13.0 to be removed in 3.0; BC support * - * @param $result + * @param mixed $result * * @return $this */ public function setResult($result) { - $this->applicable = $result; + $this->applicable = (bool) $result; return $this; } diff --git a/app/bundles/CampaignBundle/Event/ExecutedEvent.php b/app/bundles/CampaignBundle/Event/ExecutedEvent.php index 5b817b0bf22..353c13f2497 100644 --- a/app/bundles/CampaignBundle/Event/ExecutedEvent.php +++ b/app/bundles/CampaignBundle/Event/ExecutedEvent.php @@ -11,7 +11,6 @@ namespace Mautic\CampaignBundle\Event; -use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; @@ -31,7 +30,7 @@ class ExecutedEvent extends \Symfony\Component\EventDispatcher\Event * ExecutedEvent constructor. * * @param AbstractEventAccessor $config - * @param Event $event + * @param LeadEventLog $log */ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) { diff --git a/app/bundles/CampaignBundle/Event/FailedEvent.php b/app/bundles/CampaignBundle/Event/FailedEvent.php index e01de92031a..5188c6c4dd7 100644 --- a/app/bundles/CampaignBundle/Event/FailedEvent.php +++ b/app/bundles/CampaignBundle/Event/FailedEvent.php @@ -11,7 +11,6 @@ namespace Mautic\CampaignBundle\Event; -use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; @@ -28,10 +27,10 @@ class FailedEvent extends \Symfony\Component\EventDispatcher\Event private $log; /** - * ExecutedEvent constructor. + * FailedEvent constructor. * * @param AbstractEventAccessor $config - * @param Event $event + * @param LeadEventLog $log */ public function __construct(AbstractEventAccessor $config, LeadEventLog $log) { diff --git a/app/bundles/CampaignBundle/Event/PendingEvent.php b/app/bundles/CampaignBundle/Event/PendingEvent.php index 1b6ed074050..199c4877108 100644 --- a/app/bundles/CampaignBundle/Event/PendingEvent.php +++ b/app/bundles/CampaignBundle/Event/PendingEvent.php @@ -101,7 +101,7 @@ public function fail(LeadEventLog $log, $reason) } /** - * @param $reason + * @param string $reason */ public function failAll($reason) { @@ -112,6 +112,8 @@ public function failAll($reason) /** * Fail all that have not passed yet. + * + * @param string $reason */ public function failRemaining($reason) { @@ -124,7 +126,7 @@ public function failRemaining($reason) /** * @param ArrayCollection $logs - * @param $reason + * @param string $reason */ public function failLogs(ArrayCollection $logs, $reason) { @@ -150,7 +152,7 @@ public function pass(LeadEventLog $log) /** * @param LeadEventLog $log - * @param $error + * @param string $error */ public function passWithError(LeadEventLog $log, $error) { @@ -214,8 +216,8 @@ public function getSuccessful() } /** - * @param $channel - * @param null $channelId + * @param string $channel + * @param null|int $channelId */ public function setChannel($channel, $channelId = null) { diff --git a/app/bundles/CampaignBundle/Event/ScheduledEvent.php b/app/bundles/CampaignBundle/Event/ScheduledEvent.php index ebb5eb25e5a..104ef6a1d3e 100644 --- a/app/bundles/CampaignBundle/Event/ScheduledEvent.php +++ b/app/bundles/CampaignBundle/Event/ScheduledEvent.php @@ -38,6 +38,7 @@ class ScheduledEvent extends CampaignScheduledEvent * * @param AbstractEventAccessor $config * @param LeadEventLog $log + * @param bool $isReschedule */ public function __construct(AbstractEventAccessor $config, LeadEventLog $log, $isReschedule = false) { diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php index 7f2dd58df31..82b0f6c3497 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/AbstractEventAccessor.php @@ -39,7 +39,7 @@ abstract class AbstractEventAccessor private $extraProperties = []; /** - * ActionAccessor constructor. + * AbstractEventAccessor constructor. * * @param array $config */ diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php index 2476bd2dc45..4fa318452f7 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/ConditionAccessor.php @@ -16,6 +16,11 @@ */ class ConditionAccessor extends AbstractEventAccessor { + /** + * ConditionAccessor constructor. + * + * @param array $config + */ public function __construct(array $config) { $this->systemProperties[] = 'eventName'; @@ -24,7 +29,7 @@ public function __construct(array $config) } /** - * @return mixed + * @return string */ public function getEventName() { diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php index 1e3a3e254fb..9b140d77960 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/Event/DecisionAccessor.php @@ -13,6 +13,11 @@ class DecisionAccessor extends AbstractEventAccessor { + /** + * DecisionAccessor constructor. + * + * @param array $config + */ public function __construct(array $config) { $this->systemProperties[] = 'eventName'; @@ -21,7 +26,7 @@ public function __construct(array $config) } /** - * @return mixed + * @return string */ public function getEventName() { diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php index 84bbcbd0ab1..edf882e8833 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php @@ -14,7 +14,6 @@ use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\ActionAccessor; -use Mautic\CampaignBundle\EventCollector\Accessor\Event\ConditionAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Event\DecisionAccessor; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\EventNotFoundException; use Mautic\CampaignBundle\EventCollector\Accessor\Exception\TypeNotFoundException; @@ -48,8 +47,8 @@ public function __construct(array $events) } /** - * @param $type - * @param $key + * @param string $type + * @param string $key * * @return AbstractEventAccessor * @@ -71,9 +70,11 @@ public function getEvent($type, $key) } /** - * @param $key + * @param string $key * * @return ActionAccessor + * + * @throws EventNotFoundException */ public function getAction($key) { @@ -95,7 +96,9 @@ public function getActions() /** * @param $key * - * @return ConditionAccessor + * @return mixed + * + * @throws EventNotFoundException */ public function getCondition($key) { @@ -115,9 +118,11 @@ public function getConditions() } /** - * @param $key + * @param string $key * * @return DecisionAccessor + * + * @throws EventNotFoundException */ public function getDecision($key) { diff --git a/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php b/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php index dbc8e8cdab2..54eb558b3b4 100644 --- a/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php +++ b/app/bundles/CampaignBundle/EventCollector/Builder/ConnectionBuilder.php @@ -18,7 +18,7 @@ class ConnectionBuilder /** * @var array */ - private static $eventTypes =[]; + private static $eventTypes = []; /** * @var array @@ -49,9 +49,9 @@ public static function buildRestrictionsArray(array $events) } /** - * @param $eventType - * @param $key - * @param array $event + * @param string $eventType + * @param string $key + * @param array $event */ private static function addTypeConnection($eventType, $key, array $event) { @@ -76,9 +76,9 @@ private static function addTypeConnection($eventType, $key, array $event) } /** - * @param $key - * @param $restrictionType - * @param array $restrictions + * @param string $key + * @param string $restrictionType + * @param array $restrictions */ private static function addRestriction($key, $restrictionType, array $restrictions) { @@ -102,9 +102,9 @@ private static function addRestriction($key, $restrictionType, array $restrictio /** * @deprecated 2.6.0 to be removed in 3.0; BC support * - * @param $eventType - * @param $event - * @param $key + * @param string $eventType + * @param string $key + * @param array $event */ private static function addDeprecatedAnchorRestrictions($eventType, $key, array $event) { diff --git a/app/bundles/CampaignBundle/EventCollector/EventCollector.php b/app/bundles/CampaignBundle/EventCollector/EventCollector.php index 9be1aadd517..274264b54f4 100644 --- a/app/bundles/CampaignBundle/EventCollector/EventCollector.php +++ b/app/bundles/CampaignBundle/EventCollector/EventCollector.php @@ -71,8 +71,8 @@ public function getEvents() } /** - * @param $type - * @param $key + * @param string $type + * @param string $key * * @return AbstractEventAccessor */ @@ -86,7 +86,7 @@ public function getEventConfig(Event $event) * * @deprecated 2.13.0 to be removed in 3.0 * - * @param null $type + * @param null|string $type * * @return array|mixed */ diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php index 0cfc4405149..1ecfd7f3b92 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php @@ -55,8 +55,12 @@ class InactiveContactFinder * @param CampaignLeadRepository $campaignLeadRepository * @param LoggerInterface $logger */ - public function __construct(LeadRepository $leadRepository, CampaignRepository $campaignRepository, CampaignLeadRepository $campaignLeadRepository, LoggerInterface $logger) - { + public function __construct( + LeadRepository $leadRepository, + CampaignRepository $campaignRepository, + CampaignLeadRepository $campaignLeadRepository, + LoggerInterface $logger + ) { $this->leadRepository = $leadRepository; $this->campaignRepository = $campaignRepository; $this->campaignLeadRepository = $campaignLeadRepository; @@ -64,9 +68,9 @@ public function __construct(LeadRepository $leadRepository, CampaignRepository $ } /** - * @param $campaignId + * @param int $campaignId * @param Event $decisionEvent - * @param $startAtContactId + * @param int $startAtContactId * @param ContactLimiter $limiter * * @return ArrayCollection @@ -115,9 +119,9 @@ public function getDatesAdded() } /** - * @param $campaignId - * @param array $decisionEvents - * @param null $specificContactId + * @param int $campaignId + * @param array $decisionEvents + * @param ContactLimiter $limiter * * @return int */ diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php index 9a31f14df68..d96aece2d93 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php @@ -51,7 +51,7 @@ public function __construct(LeadRepository $leadRepository, CampaignRepository $ } /** - * @param $campaignId + * @param int $campaignId * @param ContactLimiter $limiter * * @return ArrayCollection @@ -85,11 +85,11 @@ public function getContacts($campaignId, ContactLimiter $limiter) } /** - * @param $campaignId - * @param array $eventIds - * @param null $specificContactId + * @param int $campaignId + * @param array $eventIds + * @param ContactLimiter $limiter * - * @return mixed + * @return int */ public function getContactCount($campaignId, array $eventIds, ContactLimiter $limiter) { diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php index cf91d95e05d..b8436cb87c6 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -44,9 +44,11 @@ class ContactLimiter /** * ContactLimiter constructor. * - * @param $contactId - * @param $minContactId - * @param $maxContactId + * @param $batchLimit + * @param $contactId + * @param $minContactId + * @param $maxContactId + * @param array $contactIdList */ public function __construct($batchLimit, $contactId, $minContactId, $maxContactId, array $contactIdList = []) { @@ -58,7 +60,7 @@ public function __construct($batchLimit, $contactId, $minContactId, $maxContactI } /** - * @return int|null + * @return int */ public function getBatchLimit() { diff --git a/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php index b0517ecf317..c94e4fcda73 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php @@ -44,7 +44,7 @@ public function __construct(ConditionDispatcher $dispatcher) * @param AbstractEventAccessor $config * @param ArrayCollection $logs * - * @return EvaluatedContacts|mixed + * @return EvaluatedContacts * * @throws CannotProcessEventException */ diff --git a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php index 9fafd203ee2..cfda14e0a36 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php @@ -53,9 +53,9 @@ public function __construct(EventLogger $eventLogger, DecisionDispatcher $dispat * @param DecisionAccessor $config * @param Event $event * @param Lead $contact - * @param null $passthrough - * @param null $channel - * @param null $channelId + * @param mixed $passthrough + * @param string|null $channel + * @param int|null $channelId * * @throws CannotProcessEventException * @throws DecisionNotApplicableException @@ -109,7 +109,7 @@ public function execute(AbstractEventAccessor $config, ArrayCollection $logs) /** * @param DecisionAccessor $config * @param LeadEventLog $log - * @param null $passthrough + * @param mixed $passthrough * * @throws DecisionNotApplicableException */ diff --git a/app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php deleted file mode 100644 index 086925b88d8..00000000000 --- a/app/bundles/CampaignBundle/Executioner/Helper/DecisionTreeHelper.php +++ /dev/null @@ -1,56 +0,0 @@ -eventCollector = $eventCollector; - $this->eventLogger = $eventLogger; - $this->eventExecutioner = $eventExecutioner; - $this->logger = $logger; - } -} diff --git a/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php b/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php index 220493185a6..91afb35ad7d 100644 --- a/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php +++ b/app/bundles/CampaignBundle/Executioner/Helper/NotificationHelper.php @@ -44,8 +44,10 @@ class NotificationHelper /** * NotificationHelper constructor. * - * @param UserModel $userModel - * @param NotificationModel $notificationModel + * @param UserModel $userModel + * @param NotificationModel $notificationModel + * @param TranslatorInterface $translator + * @param Router $router */ public function __construct(UserModel $userModel, NotificationModel $notificationModel, TranslatorInterface $translator, Router $router) { @@ -58,7 +60,6 @@ public function __construct(UserModel $userModel, NotificationModel $notificatio /** * @param Lead $contact * @param Event $event - * @param $header */ public function notifyOfFailure(Lead $contact, Event $event) { @@ -90,7 +91,7 @@ public function notifyOfFailure(Lead $contact, Event $event) * @param Lead $contact * @param Event $event * - * @return User|null|object + * @return User */ private function getUser(Lead $contact, Event $event) { diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index a828e0e62db..118b5bd96c1 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -126,7 +126,7 @@ public function __construct( * @param ContactLimiter $limiter * @param OutputInterface|null $output * - * @return Counter|mixed + * @return Counter * * @throws Dispatcher\Exception\LogNotProcessedException * @throws Dispatcher\Exception\LogPassedAndFailedException @@ -316,7 +316,7 @@ private function executeEvents() /** * @param ArrayCollection $contacts * - * @return mixed + * @return int * * @throws NoContactsFoundException */ diff --git a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php index 1d67b2ce4e9..077f8c5b0fa 100644 --- a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php @@ -116,10 +116,10 @@ public function __construct( } /** - * @param $type - * @param null $passthrough - * @param null $channel - * @param null $channelId + * @param string $type + * @param mixed $passthrough + * @param string|null $channel + * @param int|null $channelId * * @return Responses * diff --git a/app/bundles/CampaignBundle/Executioner/Result/Responses.php b/app/bundles/CampaignBundle/Executioner/Result/Responses.php index ed02128f691..0dc14083462 100644 --- a/app/bundles/CampaignBundle/Executioner/Result/Responses.php +++ b/app/bundles/CampaignBundle/Executioner/Result/Responses.php @@ -30,7 +30,7 @@ class Responses /** * DecisionResponses constructor. * - * @param ArrayCollection|null $logs + * @param ArrayCollection $logs */ public function setFromLogs(ArrayCollection $logs) { @@ -42,7 +42,7 @@ public function setFromLogs(ArrayCollection $logs) /** * @param Event $event - * @param $response + * @param mixed $response */ public function setResponse(Event $event, $response) { @@ -63,9 +63,9 @@ public function setResponse(Event $event, $response) } /** - * @param null $type + * @param string|null $type * - * @return array|mixed + * @return array */ public function getActionResponses($type = null) { @@ -77,9 +77,9 @@ public function getActionResponses($type = null) } /** - * @param null $type + * @param string|null $type * - * @return array|mixed + * @return array */ public function getConditionResponses($type = null) { diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index 7819b13ab4c..23a916fd5a8 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -178,7 +178,7 @@ public function executeByIds(array $logIds, OutputInterface $output = null) return $this->counter; } - $logs = $this->repo->getScheduledById($logIds); + $logs = $this->repo->getScheduledByIds($logIds); $totalLogsFound = $logs->count(); $this->counter->advanceEvaluated($totalLogsFound); diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index 1da213afba3..15d66678c7b 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -181,8 +181,8 @@ public function rescheduleFailure(LeadEventLog $log) * @param Event $event * @param \DateTime|null $compareFromDateTime * @param \DateTime|null $comparedToDateTime - ] * - * @return \DateTime|mixed + * + * @return \DateTime * * @throws NotSchedulableException */ diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php index 82802d65aeb..5b788c5343c 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/DateTime.php @@ -36,7 +36,7 @@ public function __construct(LoggerInterface $logger) * @param \DateTime $compareFromDateTime * @param \DateTime $comparedToDateTime * - * @return \DateTime|mixed + * @return \DateTime */ public function getExecutionDateTime(Event $event, \DateTime $compareFromDateTime, \DateTime $comparedToDateTime) { diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php index f57e68e85fd..ddc1640cf56 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/Interval.php @@ -38,7 +38,7 @@ public function __construct(LoggerInterface $logger) * @param \DateTime $compareFromDateTime * @param \DateTime $comparedToDateTime * - * @return \DateTime|mixed + * @return \DateTime * * @throws NotSchedulableException */ diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php index bc1f6012f48..76238dc3c0d 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/Mode/ScheduleModeInterface.php @@ -20,7 +20,7 @@ interface ScheduleModeInterface * @param \DateTime $now * @param \DateTime $comparedToDateTime * - * @return mixed + * @return \DateTime */ public function getExecutionDateTime(Event $event, \DateTime $now, \DateTime $comparedToDateTime); } diff --git a/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php b/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php index 03471456760..53d9158f4f7 100644 --- a/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php +++ b/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php @@ -19,8 +19,8 @@ class RemovedContactTracker private $removedContacts = []; /** - * @param $campaignId - * @param $contactId + * @param int $campaignId + * @param int $contactId */ public function addRemovedContact($campaignId, $contactId) { @@ -32,7 +32,7 @@ public function addRemovedContact($campaignId, $contactId) } /** - * @param $campaignId + * @param int $campaignId */ public function wasContactRemoved($campaignId, $contactId) { diff --git a/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php b/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php index 23dbbeec359..9a76e8922cf 100644 --- a/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php +++ b/app/bundles/CampaignBundle/Tests/CampaignTestAbstract.php @@ -82,13 +82,9 @@ protected function initCampaignModel() ->method('getRepository') ->will($this->returnValue($formRepository)); - $eventCollector = $this->getMockBuilder(EventCollector::class) - ->disableOriginalConstructor() - ->getMock(); + $eventCollector = $this->createMock(EventCollector::class); - $removedContactTracker = $this->getMockBuilder(RemovedContactTracker::class) - ->disableOriginalConstructor() - ->getMock(); + $removedContactTracker = $this->createMock(RemovedContactTracker::class); $campaignModel = new CampaignModel($coreParametersHelper, $leadModel, $leadListModel, $formModel, $eventCollector, $removedContactTracker); diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php index 72f751908b2..a68ce179700 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php @@ -178,7 +178,7 @@ public function testSpecificEventsAreExecuted() $logs = new ArrayCollection([$log1, $log2]); $this->repository->expects($this->once()) - ->method('getScheduledById') + ->method('getScheduledByIds') ->with([1, 2]) ->willReturn($logs); diff --git a/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php b/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php index 685930316b1..740197d00fe 100644 --- a/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php +++ b/app/bundles/ChannelBundle/PreferenceBuilder/ChannelPreferences.php @@ -41,7 +41,9 @@ class ChannelPreferences /** * ChannelPreferences constructor. * - * @param $channel + * @param string $channel + * @param Event $event + * @param LoggerInterface $logger */ public function __construct($channel, Event $event, LoggerInterface $logger) { @@ -51,12 +53,14 @@ public function __construct($channel, Event $event, LoggerInterface $logger) } /** - * @param $priority + * @param int $priority * * @return $this */ public function addPriority($priority) { + $priority = (int) $priority; + if (!isset($this->organizedByPriority[$priority])) { $this->organizedByPriority[$priority] = new ArrayCollection(); } @@ -66,12 +70,14 @@ public function addPriority($priority) /** * @param LeadEventLog $log - * @param $priority + * @param int $priority * * @return $this */ public function addLog(LeadEventLog $log, $priority) { + $priority = (int) $priority; + $this->addPriority($priority); // We have to clone the log to not affect the original assocaited with the MM event itself @@ -106,12 +112,14 @@ public function removeLog(LeadEventLog $log) } /** - * @param $priority + * @param int $priority * * @return ArrayCollection|LeadEventLog[] */ public function getLogsByPriority($priority) { + $priority = (int) $priority; + return isset($this->organizedByPriority[$priority]) ? $this->organizedByPriority[$priority] : new ArrayCollection(); } } diff --git a/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php b/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php index 539ef82511e..5e7e02e83b9 100644 --- a/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php +++ b/app/bundles/ChannelBundle/PreferenceBuilder/PreferenceBuilder.php @@ -47,25 +47,7 @@ public function __construct(ArrayCollection $logs, Event $event, array $channels $this->logger = $logger; $this->event = $event; - /** @var LeadEventLog $log */ - foreach ($logs as $log) { - $channelRules = $log->getLead()->getChannelRules(); - $allChannels = $channels; - $priority = 1; - - // Build priority based on channel rules - foreach ($channelRules as $channel => $rule) { - $this->addChannelRule($channel, $rule, $log, $priority); - ++$priority; - unset($allChannels[$channel]); - } - - // Add the rest of the channels as least priority - foreach ($allChannels as $channel => $messageSettings) { - $this->addChannelRule($channel, ['dnc' => DoNotContact::IS_CONTACTABLE], $log, $priority); - ++$priority; - } - } + $this->buildRules($logs, $channels); } /** @@ -87,10 +69,10 @@ public function removeLogFromAllChannels(LeadEventLog $log) } /** - * @param $channel + * @param string $channel * @param array $rule * @param LeadEventLog $log - * @param $priority + * @param int $priority */ private function addChannelRule($channel, array $rule, LeadEventLog $log, $priority) { @@ -115,7 +97,7 @@ private function addChannelRule($channel, array $rule, LeadEventLog $log, $prior } /** - * @param $channel + * @param string $channel * * @return ChannelPreferences */ @@ -129,4 +111,31 @@ private function getChannelPreferenceObject($channel, $priority) return $this->channels[$channel]; } + + /** + * @param ArrayCollection $logs + * @param array $channels + */ + private function buildRules(ArrayCollection $logs, array $channels) + { + /** @var LeadEventLog $log */ + foreach ($logs as $log) { + $channelRules = $log->getLead()->getChannelRules(); + $allChannels = $channels; + $priority = 1; + + // Build priority based on channel rules + foreach ($channelRules as $channel => $rule) { + $this->addChannelRule($channel, $rule, $log, $priority); + ++$priority; + unset($allChannels[$channel]); + } + + // Add the rest of the channels as least priority + foreach ($allChannels as $channel => $messageSettings) { + $this->addChannelRule($channel, ['dnc' => DoNotContact::IS_CONTACTABLE], $log, $priority); + ++$priority; + } + } + } } diff --git a/app/bundles/CoreBundle/Helper/DateTimeHelper.php b/app/bundles/CoreBundle/Helper/DateTimeHelper.php index 8ecba85cc69..4f80f938293 100644 --- a/app/bundles/CoreBundle/Helper/DateTimeHelper.php +++ b/app/bundles/CoreBundle/Helper/DateTimeHelper.php @@ -285,8 +285,8 @@ public function sub($intervalString, $clone = false) /** * Returns interval based on $interval number and $unit. * - * @param $interval - * @param $unit + * @param int $interval + * @param string $unit * * @return \DateInterval * From ee5b9fe5258098ff2604bfb4bf8c94bf8cb2791e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 11:43:39 -0500 Subject: [PATCH 482/778] Docblock/typehint fixes --- .../EventCollector/Accessor/EventAccessor.php | 2 +- .../CampaignBundle/Executioner/InactiveExecutioner.php | 2 +- .../CampaignBundle/Executioner/RealTimeExecutioner.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php b/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php index edf882e8833..4f9b0a6e89e 100644 --- a/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php +++ b/app/bundles/CampaignBundle/EventCollector/Accessor/EventAccessor.php @@ -94,7 +94,7 @@ public function getActions() } /** - * @param $key + * @param string $key * * @return mixed * diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 118b5bd96c1..92efa5c6280 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -160,7 +160,7 @@ public function execute(Campaign $campaign, ContactLimiter $limiter, OutputInter } /** - * @param $decisionId + * @param int $decisionId * @param ContactLimiter $limiter * @param OutputInterface|null $output * diff --git a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php index 077f8c5b0fa..c18578a4823 100644 --- a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php @@ -211,10 +211,10 @@ private function executeAssociatedEvents(ArrayCollection $children, \DateTime $n } /** - * @param Event $event - * @param null $passthrough - * @param null $channel - * @param null $channelId + * @param Event $event + * @param mixed $passthrough + * @param string|null $channel + * @param int|null $channelId * * @throws DecisionNotApplicableException * @throws Exception\CannotProcessEventException From 68d233277ba2d3b8b0d0f6394e4a17a40a834775 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 12:26:42 -0500 Subject: [PATCH 483/778] Fixed issue where company query overwrote the where clause causing the same set of contacts to be returned every time --- app/bundles/LeadBundle/Entity/CompanyLeadRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/CompanyLeadRepository.php b/app/bundles/LeadBundle/Entity/CompanyLeadRepository.php index 4f7bcb98a56..2856ead62d6 100644 --- a/app/bundles/LeadBundle/Entity/CompanyLeadRepository.php +++ b/app/bundles/LeadBundle/Entity/CompanyLeadRepository.php @@ -68,7 +68,7 @@ public function getCompaniesByLeadId($leadId, $companyId = null) )->setParameter('false', false, 'boolean'); if ($companyId) { - $q->where( + $q->andWhere( $q->expr()->eq('cl.company_id', ':companyId') )->setParameter('companyId', $companyId); } From 1dabbba36f58c1c073f51c9bdde816155886177a Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 23:28:45 -0500 Subject: [PATCH 484/778] Fixed issues with decisions at the root level getting executed and then causing an indefinite loop --- .../Entity/CampaignRepository.php | 22 +----- .../Entity/ContactLimiterTrait.php | 61 ++++++++++++++ .../CampaignBundle/Entity/LeadRepository.php | 38 ++------- .../CampaignBundle/Event/DecisionEvent.php | 4 +- .../ContactFinder/InactiveContactFinder.php | 4 +- .../ContactFinder/Limiter/ContactLimiter.php | 34 +++++++- .../Dispatcher/DecisionDispatcher.php | 19 ++++- .../Executioner/Event/DecisionExecutioner.php | 12 ++- .../Executioner/EventExecutioner.php | 79 ++++++++++++++----- .../Executioner/InactiveExecutioner.php | 35 ++------ .../Executioner/KickoffExecutioner.php | 9 +++ .../Executioner/Logger/EventLogger.php | 53 +++++++------ .../Executioner/Scheduler/EventScheduler.php | 4 +- .../Command/TriggerCampaignCommandTest.php | 7 +- .../InactiveContactFinderTest.php | 6 +- .../Limiter/ContactLimiterTest.php | 74 +++++++++++++++++ .../Dispatcher/DecisionDispatcherTest.php | 23 +++++- .../Executioner/InactiveExecutionerTest.php | 20 ++--- 18 files changed, 347 insertions(+), 157 deletions(-) create mode 100644 app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php diff --git a/app/bundles/CampaignBundle/Entity/CampaignRepository.php b/app/bundles/CampaignBundle/Entity/CampaignRepository.php index 3048f7ebe70..3157cf4555d 100644 --- a/app/bundles/CampaignBundle/Entity/CampaignRepository.php +++ b/app/bundles/CampaignBundle/Entity/CampaignRepository.php @@ -20,6 +20,8 @@ */ class CampaignRepository extends CommonRepository { + use ContactLimiterTrait; + /** * {@inheritdoc} */ @@ -609,21 +611,7 @@ public function getPendingContactIds($campaignId, ContactLimiter $limiter) ->setParameter('false', false, 'boolean') ->orderBy('cl.lead_id', 'ASC'); - if ($contactId = $limiter->getContactId()) { - $q->andWhere( - $q->expr()->eq('cl.lead_id', $contactId) - ); - } elseif ($minContactId = $limiter->getMinContactId()) { - $q->andWhere( - 'cl.lead_id BETWEEN :minContactId AND :maxContactId' - ) - ->setParameter('minContactId', $minContactId) - ->setParameter('maxContactId', $limiter->getMaxContactId()); - } elseif ($contactIds = $limiter->getContactIdList()) { - $q->andWhere( - $q->expr()->in('cl.lead_id', $contactIds) - ); - } + $this->updateQueryFromContactLimiter('cl', $q, $limiter); // Only leads that have not started the campaign $sq = $this->getEntityManager()->getConnection()->createQueryBuilder(); @@ -642,10 +630,6 @@ public function getPendingContactIds($campaignId, ContactLimiter $limiter) ) ->setParameter('campaignId', (int) $campaignId); - if ($limit = $limiter->getBatchLimit()) { - $q->setMaxResults($limit); - } - $results = $q->execute()->fetchAll(); $leads = []; diff --git a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php new file mode 100644 index 00000000000..86f91df90c9 --- /dev/null +++ b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php @@ -0,0 +1,61 @@ +getMinContactId(); + $maxContactId = $contactLimiter->getMaxContactId(); + if ($contactId = $contactLimiter->getContactId()) { + $qb->andWhere( + $qb->expr()->eq("$alias.lead_id", ':contactId') + ) + ->setParameter('contactId', $contactId); + } elseif ($contactIds = $contactLimiter->getContactIdList()) { + $qb->andWhere( + $qb->expr()->in("$alias.lead_id", ':contactIds') + ) + ->setParameter('contactIds', $contactIds, Connection::PARAM_INT_ARRAY); + } elseif ($minContactId && $maxContactId) { + $qb->andWhere( + "$alias.lead_id BETWEEN :minContactId AND :maxContactId" + ) + ->setParameter('minContactId', $minContactId) + ->setParameter('maxContactId', $maxContactId); + } elseif ($minContactId) { + $qb->andWhere( + $qb->expr()->gte("$alias.lead_id", ':minContactId') + ) + ->setParameter('minContactId', $minContactId); + } elseif ($maxContactId) { + $qb->andWhere( + $qb->expr()->lte("$alias.lead_id", ':maxContactId') + ) + ->setParameter('maxContactId', $maxContactId); + } + + if ($limit = $contactLimiter->getBatchLimit()) { + $qb->setMaxResults($limit); + } + } +} diff --git a/app/bundles/CampaignBundle/Entity/LeadRepository.php b/app/bundles/CampaignBundle/Entity/LeadRepository.php index 66d5f135b97..00461498915 100644 --- a/app/bundles/CampaignBundle/Entity/LeadRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadRepository.php @@ -19,6 +19,8 @@ */ class LeadRepository extends CommonRepository { + use ContactLimiterTrait; + /** * Get the details of leads added to a campaign. * @@ -195,12 +197,11 @@ public function checkLeadInCampaigns($lead, $options = []) * @param int $campaignId * @param int $decisionId * @param int $parentDecisionId - * @param int $startAtContactId * @param ContactLimiter $limiter * * @return array */ - public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, $startAtContactId, ContactLimiter $limiter) + public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, ContactLimiter $limiter) { // Main query $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); @@ -213,33 +214,10 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, ->setParameter('decisionId', (int) $decisionId); // Contact IDs - $expr = $q->expr()->andX(); - if ($specificContactId = $limiter->getContactId()) { - // Still query for this ID in case the ID fed to the command no longer exists - $expr->add( - $q->expr()->eq('l.lead_id', ':contactId') - ); - $q->setParameter('contactId', (int) $specificContactId); - } elseif ($contactIds = $limiter->getContactIdList()) { - $expr->add( - $q->expr()->in('l.lead_id', $contactIds) - ); - } else { - $expr->add( - $q->expr()->gt('l.lead_id', ':minContactId') - ); - $q->setParameter('minContactId', (int) $startAtContactId); - - if ($maxContactId = $limiter->getMaxContactId()) { - $expr->add( - $q->expr()->lte('l.lead_id', ':maxContactId') - ); - $q->setParameter('maxContactId', $maxContactId); - } - } + $this->updateQueryFromContactLimiter('l', $q, $limiter); // Limit to specific campaign - $expr->add( + $q->andWhere( $q->expr()->eq('l.campaign_id', ':campaignId') ); @@ -254,7 +232,7 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, $eventQb->expr()->eq('log.rotation', 'l.rotation') ) ); - $expr->add( + $q->andWhere( sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) ); @@ -270,13 +248,11 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, ); $q->setParameter('grandparentId', (int) $parentDecisionId); - $expr->add( + $q->andWhere( sprintf('EXISTS (%s)', $grandparentQb->getSQL()) ); } - $q->where($expr); - $results = $q->execute()->fetchAll(); $contacts = []; foreach ($results as $result) { diff --git a/app/bundles/CampaignBundle/Event/DecisionEvent.php b/app/bundles/CampaignBundle/Event/DecisionEvent.php index 95c812dc09f..4d5473e2060 100644 --- a/app/bundles/CampaignBundle/Event/DecisionEvent.php +++ b/app/bundles/CampaignBundle/Event/DecisionEvent.php @@ -46,9 +46,9 @@ class DecisionEvent extends CampaignExecutionEvent * * @param AbstractEventAccessor $config * @param LeadEventLog $log - * @param $passthrough + * @param mixed $passthrough */ - public function __construct(AbstractEventAccessor $config, LeadEventLog $log, $passthrough) + public function __construct(AbstractEventAccessor $config, LeadEventLog $log, $passthrough = null) { $this->eventConfig = $config; $this->eventLog = $log; diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php index 1ecfd7f3b92..4ae5e46c2c8 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/InactiveContactFinder.php @@ -70,14 +70,13 @@ public function __construct( /** * @param int $campaignId * @param Event $decisionEvent - * @param int $startAtContactId * @param ContactLimiter $limiter * * @return ArrayCollection * * @throws NoContactsFoundException */ - public function getContacts($campaignId, Event $decisionEvent, $startAtContactId, ContactLimiter $limiter) + public function getContacts($campaignId, Event $decisionEvent, ContactLimiter $limiter) { // Get list of all campaign leads $decisionParentEvent = $decisionEvent->getParent(); @@ -85,7 +84,6 @@ public function getContacts($campaignId, Event $decisionEvent, $startAtContactId $campaignId, $decisionEvent->getId(), ($decisionParentEvent) ? $decisionParentEvent->getId() : null, - $startAtContactId, $limiter ); diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php index b8436cb87c6..e8df03fead0 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -11,6 +11,8 @@ namespace Mautic\CampaignBundle\Executioner\ContactFinder\Limiter; +use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; + /** * Class ContactLimiter. */ @@ -31,6 +33,11 @@ class ContactLimiter */ private $minContactId; + /** + * @var int|null + */ + private $batchMinContactId; + /** * @var int|null */ @@ -80,7 +87,7 @@ public function getContactId() */ public function getMinContactId() { - return $this->minContactId; + return ($this->batchMinContactId) ? $this->batchMinContactId : $this->minContactId; } /** @@ -98,4 +105,29 @@ public function getContactIdList() { return $this->contactIdList; } + + /** + * @param int $id + * + * @throws NoContactsFoundException + */ + public function setBatchMinContactId($id) + { + // Prevent a never ending loop if the contact ID never changes due to being the last batch of contacts + if ($this->minContactId && $this->minContactId > (int) $id) { + throw new NoContactsFoundException(); + } + + // We've surpasssed the max so bai + if ($this->maxContactId && $this->maxContactId < (int) $id) { + throw new NoContactsFoundException(); + } + + // The same batch of contacts were somehow processed so let's stop to prevent the loop + if ($this->batchMinContactId && $this->batchMinContactId >= $id) { + throw new NoContactsFoundException(); + } + + $this->batchMinContactId = (int) $id; + } } diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php index 5b6dcff9834..3c2af0faec7 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/DecisionDispatcher.php @@ -49,16 +49,29 @@ public function __construct( /** * @param DecisionAccessor $config * @param LeadEventLog $log - * @param $passthrough + * @param mixed $passthrough * * @return DecisionEvent */ - public function dispatchEvent(DecisionAccessor $config, LeadEventLog $log, $passthrough) + public function dispatchRealTimeEvent(DecisionAccessor $config, LeadEventLog $log, $passthrough) { $event = new DecisionEvent($config, $log, $passthrough); $this->dispatcher->dispatch($config->getEventName(), $event); - $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $event); + return $event; + } + + /** + * @param DecisionAccessor $config + * @param LeadEventLog $log + * + * @return DecisionEvent + */ + public function dispatchEvaluationEvent(DecisionAccessor $config, LeadEventLog $log) + { + $event = new DecisionEvent($config, $log); + + $this->dispatcher->dispatch(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $event); $this->legacyDispatcher->dispatchDecisionEvent($event); return $event; diff --git a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php index cfda14e0a36..7322615252e 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php @@ -70,7 +70,12 @@ public function evaluateForContact(DecisionAccessor $config, Event $event, Lead $log->setChannel($channel) ->setChannelId($channelId); - $this->dispatchEvent($config, $log, $passthrough); + $decisionEvent = $this->dispatcher->dispatchRealTimeEvent($config, $log, $passthrough); + + if (!$decisionEvent->wasDecisionApplicable()) { + throw new DecisionNotApplicableException('evaluation failed'); + } + $this->eventLogger->persistLog($log); } @@ -97,6 +102,9 @@ public function execute(AbstractEventAccessor $config, ArrayCollection $logs) $this->dispatchEvent($config, $log); $evaluatedContacts->pass($log->getLead()); } catch (DecisionNotApplicableException $exception) { + // Fail the contact but remove the log from being processed upstream + // active/positive/green path while letting the InactiveExecutioner handle the inactive/negative/red paths + $logs->removeElement($log); $evaluatedContacts->fail($log->getLead()); } } @@ -115,7 +123,7 @@ public function execute(AbstractEventAccessor $config, ArrayCollection $logs) */ private function dispatchEvent(DecisionAccessor $config, LeadEventLog $log, $passthrough = null) { - $decisionEvent = $this->dispatcher->dispatchEvent($config, $log, $passthrough); + $decisionEvent = $this->dispatcher->dispatchEvaluationEvent($config, $log, $passthrough); if (!$decisionEvent->wasDecisionApplicable()) { throw new DecisionNotApplicableException('evaluation failed'); diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index c219322e097..44b47d55fe7 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -193,17 +193,17 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte case Event::TYPE_ACTION: $evaluatedContacts = $this->actionExecutioner->execute($config, $logs); $this->persistLogs($logs); - $this->executeEventConditionsForContacts($event, $evaluatedContacts->getPassed(), $counter); + $this->executeConditionEventsForContacts($event, $evaluatedContacts->getPassed(), $counter); break; case Event::TYPE_CONDITION: $evaluatedContacts = $this->conditionExecutioner->execute($config, $logs); $this->persistLogs($logs); - $this->executeDecisionPathEventsForContacts($event, $evaluatedContacts, $counter); + $this->executeBranchedEventsForContacts($event, $evaluatedContacts, $counter); break; case Event::TYPE_DECISION: $evaluatedContacts = $this->decisionExecutioner->execute($config, $logs); $this->persistLogs($logs); - $this->executeDecisionPathEventsForContacts($event, $evaluatedContacts, $counter); + $this->executePositivePathEventsForContacts($event, $evaluatedContacts->getPassed(), $counter); break; default: throw new TypeNotFoundException("{$event->getEventType()} is not a valid event type"); @@ -318,7 +318,7 @@ private function checkForRemovedContacts(ArrayCollection $logs) * @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException * @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException */ - private function executeEventConditionsForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null) + private function executeConditionEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter = null) { $childrenCounter = new Counter(); $conditions = $event->getChildrenByEventType(Event::TYPE_CONDITION); @@ -338,32 +338,69 @@ private function executeEventConditionsForContacts(Event $event, ArrayCollection * @param Event $event * @param EvaluatedContacts $contacts * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException */ - private function executeDecisionPathEventsForContacts(Event $event, EvaluatedContacts $contacts, Counter $counter = null) + private function executeBranchedEventsForContacts(Event $event, EvaluatedContacts $contacts, Counter $counter = null) { $childrenCounter = new Counter(); - $positive = $contacts->getPassed(); - if ($positive->count()) { - $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $positive->getKeys()).' passed evaluation for event ID '.$event->getId()); + $this->executePositivePathEventsForContacts($event, $contacts->getPassed(), $childrenCounter); + $this->executeNegativePathEventsForContacts($event, $contacts->getFailed(), $childrenCounter); - $children = $event->getPositiveChildren(); - $childrenCounter->advanceEvaluated($children->count()); - $this->executeEventsForContacts($children, $positive, $childrenCounter); + if ($counter) { + $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); + $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); } + } - $negative = $contacts->getFailed(); - if ($negative->count()) { - $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $negative->getKeys()).' failed evaluation for event ID '.$event->getId()); + /** + * @param Event $event + * @param ArrayCollection $contacts + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + private function executePositivePathEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter) + { + if (!$contacts->count()) { + return; + } - $children = $event->getNegativeChildren(); + $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $contacts->getKeys()).' passed evaluation for event ID '.$event->getId()); - $childrenCounter->advanceEvaluated($children->count()); - $this->executeEventsForContacts($children, $negative, $childrenCounter); - } + $children = $event->getPositiveChildren(); + $counter->advanceEvaluated($children->count()); - if ($counter) { - $counter->advanceTotalEvaluated($childrenCounter->getTotalEvaluated()); - $counter->advanceTotalExecuted($childrenCounter->getTotalExecuted()); + $this->executeEventsForContacts($children, $contacts, $counter); + } + + /** + * @param Event $event + * @param ArrayCollection $contacts + * @param Counter|null $counter + * + * @throws Dispatcher\Exception\LogNotProcessedException + * @throws Dispatcher\Exception\LogPassedAndFailedException + * @throws Exception\CannotProcessEventException + * @throws Scheduler\Exception\NotSchedulableException + */ + private function executeNegativePathEventsForContacts(Event $event, ArrayCollection $contacts, Counter $counter) + { + if (!$contacts->count()) { + return; } + + $this->logger->debug('CAMPAIGN: Contact IDs '.implode(',', $contacts->getKeys()).' failed evaluation for event ID '.$event->getId()); + + $children = $event->getNegativeChildren(); + $counter->advanceEvaluated($children->count()); + + $this->executeEventsForContacts($children, $contacts, $counter); } } diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 92efa5c6280..9676953f12b 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -266,17 +266,13 @@ private function executeEvents() $parentEvent = $decisionEvent->getParent(); $parentEventId = ($parentEvent) ? $parentEvent->getId() : null; - // Because timing may not be appropriate, the starting row of the query may or may not change. - // So use the max contact ID to filter/sort results. - $this->startAtContactId = $this->limiter->getMinContactId() ?: 0; - // Ge the first batch of contacts - $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->startAtContactId, $this->limiter); + $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->limiter); // Loop over all contacts till we've processed all those applicable for this decision while ($contacts->count()) { // Get the max contact ID before any are removed - $startAtContactId = $this->getStartingContactIdForNextBatch($contacts); + $batchMinContactId = max($contacts->getKeys()) + 1; $this->progressBar->advance($contacts->count()); $this->counter->advanceEvaluated($contacts->count()); @@ -305,36 +301,15 @@ private function executeEvents() break; } - $this->logger->debug('CAMPAIGN: Fetching the next batch of inactive contacts after contact ID '.$startAtContactId); + $this->logger->debug('CAMPAIGN: Fetching the next batch of inactive contacts starting with contact ID '.$batchMinContactId); + $this->limiter->setBatchMinContactId($batchMinContactId); // Get the next batch, starting with the max contact ID - $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $startAtContactId, $this->limiter); + $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->limiter); } } } - /** - * @param ArrayCollection $contacts - * - * @return int - * - * @throws NoContactsFoundException - */ - private function getStartingContactIdForNextBatch(ArrayCollection $contacts) - { - $maxId = max($contacts->getKeys()); - - // Prevent a never ending loop if the contact ID never changes due to the last batch of contacts - // getting removed because previously executed events are scheduled - if ($this->startAtContactId === $maxId) { - throw new NoContactsFoundException(); - } - - $this->startAtContactId = $maxId; - - return $maxId; - } - /** * @param ArrayCollection $children * @param ArrayCollection $contacts diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index a56b759df5d..c5757f15c1b 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -197,9 +197,15 @@ private function executeOrScheduleEvent() $now = new \DateTime(); $this->counter->advanceEventCount($this->rootEvents->count()); + // We have to keep track of this in case the campaign begins with a decision in order + // to prevent a never ending loop + $originalMinContactId = $this->limiter->getMinContactId(); + // Loop over contacts until the entire campaign is executed $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); while ($contacts->count()) { + $batchMinContactId = max($contacts->getKeys()) + 1; + /** @var Event $event */ foreach ($this->rootEvents as $event) { $this->progressBar->advance($contacts->count()); @@ -230,6 +236,9 @@ private function executeOrScheduleEvent() break; } + $this->logger->debug('CAMPAIGN: Fetching the next batch of kickoff contacts starting with contact ID '.$batchMinContactId); + $this->limiter->setBatchMinContactId($batchMinContactId); + // Get the next batch $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); } diff --git a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php index 1bb9d46da47..cafb29e9d86 100644 --- a/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php +++ b/app/bundles/CampaignBundle/Executioner/Logger/EventLogger.php @@ -41,12 +41,12 @@ class EventLogger /** * @var ArrayCollection */ - private $queued; + private $persistQueue; /** * @var ArrayCollection */ - private $processed; + private $logs; /** * LogHelper constructor. @@ -61,19 +61,19 @@ public function __construct(IpLookupHelper $ipLookupHelper, ContactTracker $cont $this->contactTracker = $contactTracker; $this->repo = $repo; - $this->queued = new ArrayCollection(); - $this->processed = new ArrayCollection(); + $this->persistQueue = new ArrayCollection(); + $this->logs = new ArrayCollection(); } /** * @param LeadEventLog $log */ - public function addToQueue(LeadEventLog $log) + public function queueToPersist(LeadEventLog $log) { - $this->queued->add($log); + $this->persistQueue->add($log); - if ($this->queued->count() >= 20) { - $this->persistQueued(); + if ($this->persistQueue->count() >= 20) { + $this->persistQueuedLogs(); } } @@ -119,19 +119,19 @@ public function buildLogEntry(Event $event, Lead $lead = null, $isInactiveEvent /** * Persist the queue, clear the entities from memory, and reset the queue. */ - public function persistQueued() + public function persistQueuedLogs() { - if ($this->queued->count()) { - $this->repo->saveEntities($this->queued->getValues()); + if ($this->persistQueue->count()) { + $this->repo->saveEntities($this->persistQueue->getValues()); } - // Push them into the processed ArrayCollection to be used later. + // Push them into the logs ArrayCollection to be used later. /** @var LeadEventLog $log */ - foreach ($this->queued as $log) { - $this->processed->set($log->getId(), $log); + foreach ($this->persistQueue as $log) { + $this->logs->set($log->getId(), $log); } - $this->queued->clear(); + $this->persistQueue->clear(); } /** @@ -139,7 +139,7 @@ public function persistQueued() */ public function getLogs() { - return $this->processed; + return $this->logs; } /** @@ -171,17 +171,17 @@ public function clearCollection(ArrayCollection $collection) } /** - * Persist processed entities after they've been updated. + * Persist logs entities after they've been updated. * * @return $this */ public function persist() { - if (!$this->processed->count()) { + if (!$this->logs->count()) { return $this; } - $this->repo->saveEntities($this->processed->getValues()); + $this->repo->saveEntities($this->logs->getValues()); return $this; } @@ -191,7 +191,7 @@ public function persist() */ public function clear() { - $this->processed->clear(); + $this->logs->clear(); $this->repo->clear(); return $this; @@ -225,6 +225,8 @@ public function extractContactsFromLogs(ArrayCollection $logs) */ public function generateLogsFromContacts(Event $event, AbstractEventAccessor $config, ArrayCollection $contacts, $isInactiveEntry = false) { + $isDecision = Event::TYPE_DECISION === $event->getEventType(); + // Ensure each contact has a log entry to prevent them from being picked up again prematurely foreach ($contacts as $contact) { $log = $this->buildLogEntry($event, $contact, $isInactiveEntry); @@ -233,11 +235,16 @@ public function generateLogsFromContacts(Event $event, AbstractEventAccessor $co ChannelExtractor::setChannel($log, $event, $config); - $this->addToQueue($log); + if ($isDecision) { + // Do not pre-persist decision logs as they must be evaluated first + $this->logs->add($log); + } else { + $this->queueToPersist($log); + } } - $this->persistQueued(); + $this->persistQueuedLogs(); - return $this->getLogs(); + return $this->logs; } } diff --git a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php index 15d66678c7b..a2190ff02e0 100644 --- a/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php +++ b/app/bundles/CampaignBundle/Executioner/Scheduler/EventScheduler.php @@ -123,7 +123,7 @@ public function schedule(Event $event, \DateTime $executionDate, ArrayCollection $log->setTriggerDate($executionDate); // Add it to the queue to persist to the DB - $this->eventLogger->addToQueue($log); + $this->eventLogger->queueToPersist($log); //lead actively triggered this event, a decision wasn't involved, or it was system triggered and a "no" path so schedule the event to be fired at the defined time $this->logger->debug( @@ -135,7 +135,7 @@ public function schedule(Event $event, \DateTime $executionDate, ArrayCollection } // Persist any pending in the queue - $this->eventLogger->persistQueued(); + $this->eventLogger->persistQueuedLogs(); // Send out a batch event $this->dispatchBatchScheduledEvent($config, $event, $this->eventLogger->getLogs()); diff --git a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php index 596e811ecd7..824d3b0783b 100644 --- a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php @@ -18,7 +18,8 @@ class TriggerCampaignCommandTest extends AbstractCampaignCommand */ public function testCampaignExecutionForAll() { - $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); + // Process in batches of 10 to ensure batching is working as expected + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Let's analyze $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13]); @@ -61,7 +62,7 @@ public function testCampaignExecutionForAll() // Wait 15 seconds then execute the campaign again to send scheduled events sleep(15); - $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Send email 1 should no longer be scheduled $byEvent = $this->getCampaignEventLogs([2, 4]); @@ -106,7 +107,7 @@ public function testCampaignExecutionForAll() sleep(15); // Execute the command again to trigger inaction related events - $this->runCommand('mautic:campaigns:trigger', ['-i' => 1]); + $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Now we should have 50 email open decisions $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactFinderTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactFinderTest.php index e226661f74e..fd6c9add939 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactFinderTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/InactiveContactFinderTest.php @@ -63,7 +63,7 @@ public function testNoContactsFoundExceptionIsThrown() $this->expectException(NoContactsFoundException::class); $limiter = new ContactLimiter(0, 0, 0, 0); - $this->getContactFinder()->getContacts(1, new Event(), 0, $limiter); + $this->getContactFinder()->getContacts(1, new Event(), $limiter); } public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound() @@ -83,7 +83,7 @@ public function testNoContactsFoundExceptionIsThrownIfEntitiesAreNotFound() $this->expectException(NoContactsFoundException::class); $limiter = new ContactLimiter(0, 0, 0, 0); - $this->getContactFinder()->getContacts(1, new Event(), 0, $limiter); + $this->getContactFinder()->getContacts(1, new Event(), $limiter); } public function testContactsAreFoundAndStoredInCampaignMemberDatesAdded() @@ -103,7 +103,7 @@ public function testContactsAreFoundAndStoredInCampaignMemberDatesAdded() $contactFinder = $this->getContactFinder(); $limiter = new ContactLimiter(0, 0, 0, 0); - $contacts = $contactFinder->getContacts(1, new Event(), 0, $limiter); + $contacts = $contactFinder->getContacts(1, new Event(), $limiter); $this->assertCount(1, $contacts); $this->assertEquals($contactMemberDates, $contactFinder->getDatesAdded()); diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php new file mode 100644 index 00000000000..05ec7833a2b --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php @@ -0,0 +1,74 @@ +assertEquals(1, $limiter->getBatchLimit()); + $this->assertEquals(2, $limiter->getContactId()); + $this->assertEquals(3, $limiter->getMinContactId()); + $this->assertEquals(4, $limiter->getMaxContactId()); + $this->assertEquals([1, 2, 3], $limiter->getContactIdList()); + } + + public function testBatchMinContactIsReturned() + { + $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); + + $limiter->setBatchMinContactId(5); + $this->assertEquals(5, $limiter->getMinContactId()); + } + + public function testNoContactsFoundExceptionThrownIfIdIsLessThanMin() + { + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); + $limiter->setBatchMinContactId(1); + } + + public function testNoContactsFoundExceptionThrownIfIdIsMoreThanMax() + { + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); + $limiter->setBatchMinContactId(11); + } + + public function testNoContactsFoundExceptionThrownIfIdIsTheSameAsLastBatch() + { + $this->expectException(NoContactsFoundException::class); + + $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); + $limiter->setBatchMinContactId(5); + $limiter->setBatchMinContactId(5); + } + + public function testExceptionNotThrownIfIdEqualsMinSoThatItsIsIncluded() + { + $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); + $limiter->setBatchMinContactId(3); + } + + public function testExceptionNotThrownIfIdEqualsMaxSoThatItsIsIncluded() + { + $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); + $limiter->setBatchMinContactId(10); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php index 06707bd752f..86984700a02 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/Dispatcher/DecisionDispatcherTest.php @@ -55,18 +55,33 @@ public function testDecisionEventIsDispatched() ->method('getEventName') ->willReturn('something'); - $this->legacyDispatcher->expects($this->once()) + $this->legacyDispatcher->expects($this->never()) ->method('dispatchDecisionEvent'); - $this->dispatcher->expects($this->at(0)) + $this->dispatcher->expects($this->once()) ->method('dispatch') ->with('something', $this->isInstanceOf(DecisionEvent::class)); - $this->dispatcher->expects($this->at(1)) + $this->getEventDispatcher()->dispatchRealTimeEvent($config, new LeadEventLog(), null); + } + + public function testDecisionEvaluationEventIsDispatched() + { + $config = $this->getMockBuilder(DecisionAccessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $config->expects($this->never()) + ->method('getEventName'); + + $this->legacyDispatcher->expects($this->once()) + ->method('dispatchDecisionEvent'); + + $this->dispatcher->expects($this->once()) ->method('dispatch') ->with(CampaignEvents::ON_EVENT_DECISION_EVALUATION, $this->isInstanceOf(DecisionEvent::class)); - $this->getEventDispatcher()->dispatchEvent($config, new LeadEventLog(), null); + $this->getEventDispatcher()->dispatchEvaluationEvent($config, new LeadEventLog()); } public function testDecisionResultsEventIsDispatched() diff --git a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php index 30a9a0f8b3f..dc0f2022a0d 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/InactiveExecutionerTest.php @@ -126,11 +126,7 @@ public function testNextBatchOfContactsAreExecuted() $this->inactiveContactFinder->expects($this->exactly(3)) ->method('getContacts') - ->withConsecutive( - [null, $decision, 0, $limiter], - [null, $decision, 3, $limiter], - [null, $decision, 10, $limiter] - ) + ->with(null, $decision, $limiter) ->willReturnOnConsecutiveCalls( new ArrayCollection([3 => new Lead()]), new ArrayCollection([10 => new Lead()]), @@ -141,6 +137,10 @@ public function testNextBatchOfContactsAreExecuted() ->method('getEarliestInactiveDateTime') ->willReturn(new \DateTime()); + $this->eventScheduler->expects($this->exactly(2)) + ->method('getSortedExecutionDates') + ->willReturn([]); + $this->getExecutioner()->execute($campaign, $limiter); } @@ -193,11 +193,7 @@ public function testValidationEvaluatesFoundEvents() $this->inactiveContactFinder->expects($this->exactly(3)) ->method('getContacts') - ->withConsecutive( - [null, $decision, 0, $limiter], - [null, $decision, 3, $limiter], - [null, $decision, 10, $limiter] - ) + ->with(null, $decision, $limiter) ->willReturnOnConsecutiveCalls( new ArrayCollection([3 => new Lead()]), new ArrayCollection([10 => new Lead()]), @@ -208,6 +204,10 @@ public function testValidationEvaluatesFoundEvents() ->method('getEarliestInactiveDateTime') ->willReturn(new \DateTime()); + $this->eventScheduler->expects($this->exactly(2)) + ->method('getSortedExecutionDates') + ->willReturn([]); + $this->getExecutioner()->validate(1, $limiter); } From 4f6489785a8a46527929842e07c4cd1bdaa2516c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 10 May 2018 23:46:54 -0500 Subject: [PATCH 485/778] Remove logs after event dispatched --- .../Executioner/Event/DecisionExecutioner.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php index 7322615252e..959ab486737 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php @@ -90,6 +90,7 @@ public function evaluateForContact(DecisionAccessor $config, Event $event, Lead public function execute(AbstractEventAccessor $config, ArrayCollection $logs) { $evaluatedContacts = new EvaluatedContacts(); + $failedLogs = []; /** @var LeadEventLog $log */ foreach ($logs as $log) { @@ -104,13 +105,18 @@ public function execute(AbstractEventAccessor $config, ArrayCollection $logs) } catch (DecisionNotApplicableException $exception) { // Fail the contact but remove the log from being processed upstream // active/positive/green path while letting the InactiveExecutioner handle the inactive/negative/red paths - $logs->removeElement($log); + $failedLogs[] = $log; $evaluatedContacts->fail($log->getLead()); } } $this->dispatcher->dispatchDecisionResultsEvent($config, $logs, $evaluatedContacts); + // Remove the logs + foreach ($failedLogs as $log) { + $logs->removeElement($log); + } + return $evaluatedContacts; } From 76e4e08cd55fcd239e99ad05bca53199225739ad Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 11 May 2018 14:39:51 -0500 Subject: [PATCH 486/778] Fixed issue with DWC channel not matching and not bubbling up content --- .../Dispatcher/LegacyEventDispatcher.php | 4 +- .../Executioner/EventExecutioner.php | 4 +- .../Executioner/RealTimeExecutioner.php | 4 +- .../Executioner/Result/Responses.php | 18 ++- .../Executioner/RealTimeExecutionerTest.php | 4 +- .../Tests/Executioner/Result/CounterTest.php | 41 +++++++ .../Result/EvalutatedContactsTest.php | 37 +++++++ .../Executioner/Result/ResponsesTest.php | 104 ++++++++++++++++++ .../Helper/DynamicContentHelper.php | 2 +- 9 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Result/CounterTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Result/EvalutatedContactsTest.php create mode 100644 app/bundles/CampaignBundle/Tests/Executioner/Result/ResponsesTest.php diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 3234c718c2f..67e6e3b93d6 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -138,9 +138,7 @@ public function dispatchCustomEvent( if (!$wasBatchProcessed) { $this->dispatchExecutionEvent($config, $log, $result); - if (is_array($result)) { - $log->appendToMetadata($result); - } + $log->appendToMetadata($result); // Dispatch new events for legacy processed logs if ($this->isFailed($result)) { diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 44b47d55fe7..25dfc400443 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -127,7 +127,9 @@ public function __construct( */ public function executeForContact(Event $event, Lead $contact, Responses $responses = null, Counter $counter = null) { - $this->responses = $responses; + if ($responses) { + $this->responses = $responses; + } $contacts = new ArrayCollection([$contact->getId() => $contact]); diff --git a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php index c18578a4823..3ce65362c08 100644 --- a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php @@ -225,11 +225,11 @@ private function evaluateDecisionForContact(Event $event, $passthrough = null, $ // If channels do not match up, there's no need to go further if ($channel && $event->getChannel() && $channel !== $event->getChannel()) { - throw new DecisionNotApplicableException('channels do not match'); + throw new DecisionNotApplicableException("Channels, $channel and {$event->getChannel()}, do not match."); } if ($channel && $channelId && $event->getChannelId() && $channelId !== $event->getChannelId()) { - throw new DecisionNotApplicableException('channel IDs do not match for channel '.$channel); + throw new DecisionNotApplicableException("Channel IDs, $channelId and {$event->getChannelId()}, do not match for $channel."); } /** @var DecisionAccessor $config */ diff --git a/app/bundles/CampaignBundle/Executioner/Result/Responses.php b/app/bundles/CampaignBundle/Executioner/Result/Responses.php index 0dc14083462..f9d56314248 100644 --- a/app/bundles/CampaignBundle/Executioner/Result/Responses.php +++ b/app/bundles/CampaignBundle/Executioner/Result/Responses.php @@ -36,7 +36,18 @@ public function setFromLogs(ArrayCollection $logs) { /** @var LeadEventLog $log */ foreach ($logs as $log) { - $this->setResponse($log->getEvent(), $log->getMetadata()); + $metadata = $log->getMetadata(); + $response = $metadata; + + if (isset($metadata['timeline']) && count($metadata) === 1) { + // Legacy listeners set a string in CampaignExecutionEvent::setResult that Lead::appendToMetadata put into + // under a timeline key for BC support. To keep BC for decisions, we have to extract that back out for the bubble + // up responses + + $response = $metadata['timeline']; + } + + $this->setResponse($log->getEvent(), $response); } } @@ -105,6 +116,9 @@ public function containsResponses() */ public function getResponseArray() { - return array_merge($this->actionResponses, $this->conditionResponses); + return [ + Event::TYPE_ACTION => $this->actionResponses, + Event::TYPE_CONDITION => $this->conditionResponses, + ]; } } diff --git a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php index eeb5d040582..e78c12bbec8 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php @@ -145,7 +145,7 @@ public function testChannelMisMatchResultsInEmptyResponses() $event = $this->getMockBuilder(Event::class) ->getMock(); - $event->expects($this->exactly(2)) + $event->expects($this->exactly(3)) ->method('getChannel') ->willReturn('email'); @@ -178,7 +178,7 @@ public function testChannelIdMisMatchResultsInEmptyResponses() $event->expects($this->exactly(2)) ->method('getChannel') ->willReturn('email'); - $event->expects($this->exactly(2)) + $event->expects($this->exactly(3)) ->method('getChannelId') ->willReturn(3); diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Result/CounterTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Result/CounterTest.php new file mode 100644 index 00000000000..72c16eba71d --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Result/CounterTest.php @@ -0,0 +1,41 @@ +advanceEvaluated(2); + $this->assertEquals(3, $counter->getEvaluated()); + $this->assertEquals(3, $counter->getTotalEvaluated()); + + $counter->advanceTotalEvaluated(1); + $this->assertEquals(3, $counter->getEvaluated()); + $this->assertEquals(4, $counter->getTotalEvaluated()); + + $counter->advanceExecuted(2); + $this->assertEquals(3, $counter->getExecuted()); + $this->assertEquals(3, $counter->getTotalExecuted()); + + $counter->advanceTotalExecuted(1); + $this->assertEquals(3, $counter->getExecuted()); + $this->assertEquals(4, $counter->getTotalExecuted()); + + $counter->advanceTotalScheduled(2); + $this->assertEquals(3, $counter->getTotalScheduled()); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Result/EvalutatedContactsTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Result/EvalutatedContactsTest.php new file mode 100644 index 00000000000..c1ca2de6a18 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Result/EvalutatedContactsTest.php @@ -0,0 +1,37 @@ +pass($passLead); + + $failedLead = new Lead(); + $evaluatedContacts->fail($failedLead); + + $passed = $evaluatedContacts->getPassed(); + $failed = $evaluatedContacts->getFailed(); + + $this->assertCount(1, $passed); + $this->assertCount(1, $failed); + + $this->assertTrue($passLead === $passed->first()); + $this->assertTrue($failedLead === $failed->first()); + } +} diff --git a/app/bundles/CampaignBundle/Tests/Executioner/Result/ResponsesTest.php b/app/bundles/CampaignBundle/Tests/Executioner/Result/ResponsesTest.php new file mode 100644 index 00000000000..adc62880df6 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Executioner/Result/ResponsesTest.php @@ -0,0 +1,104 @@ +createMock(Event::class); + $actionEvent->method('getEventType') + ->willReturn(Event::TYPE_ACTION); + $actionEvent->method('getType') + ->willReturn('actionEvent'); + $actionEvent->method('getId') + ->willReturn(1); + + // BC should set response as just test + $actionLog = $this->createMock(LeadEventLog::class); + $actionLog->method('getEvent') + ->willReturn($actionEvent); + $actionLog->method('getMetadata') + ->willReturn(['timeline' => 'test']); + + $action2Event = $this->createMock(Event::class); + $action2Event->method('getEventType') + ->willReturn(Event::TYPE_ACTION); + $action2Event->method('getType') + ->willReturn('action2Event'); + $action2Event->method('getId') + ->willReturn(2); + + // Response should be full array + $action2Log = $this->createMock(LeadEventLog::class); + $action2Log->method('getEvent') + ->willReturn($action2Event); + $action2Log->method('getMetadata') + ->willReturn(['timeline' => 'test', 'something' => 'else']); + + // Response should be full array + $conditionEvent = $this->createMock(Event::class); + $conditionEvent->method('getEventType') + ->willReturn(Event::TYPE_CONDITION); + $conditionEvent->method('getType') + ->willReturn('conditionEvent'); + $conditionEvent->method('getId') + ->willReturn(3); + + $conditionLog = $this->createMock(LeadEventLog::class); + $conditionLog->method('getEvent') + ->willReturn($conditionEvent); + $conditionLog->method('getMetadata') + ->willReturn(['something' => 'else']); + + $logs = new ArrayCollection([$actionLog, $action2Log, $conditionLog]); + + $responses = new Responses(); + $responses->setFromLogs($logs); + + $actions = [ + 'actionEvent' => [ + 1 => 'test', + ], + 'action2Event' => [ + 2 => [ + 'timeline' => 'test', + 'something' => 'else', + ], + ], + ]; + + $conditions = [ + 'conditionEvent' => [ + 3 => [ + 'something' => 'else', + ], + ], + ]; + + $this->assertEquals( + [ + 'action' => $actions, + 'condition' => $conditions, + ], + $responses->getResponseArray() + ); + + $this->assertEquals($actions, $responses->getActionResponses()); + $this->assertEquals($conditions, $responses->getConditionResponses()); + } +} diff --git a/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php b/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php index 4cdf12ad047..a7cb108f49e 100644 --- a/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php +++ b/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php @@ -63,7 +63,7 @@ public function __construct(DynamicContentModel $dynamicContentModel, EventModel */ public function getDynamicContentForLead($slot, $lead) { - $response = $this->campaignEventModel->triggerEvent('dwc.decision', $slot, 'dwc.decision.'.$slot); + $response = $this->campaignEventModel->triggerEvent('dwc.decision', $slot, 'dynamicContent'); $content = ''; if (is_array($response) && !empty($response['action']['dwc.push_content'])) { From ee15bfc4189f216b87099dbed5b4458c31dd03ed Mon Sep 17 00:00:00 2001 From: heathdutton Date: Mon, 14 May 2018 15:31:25 -0400 Subject: [PATCH 487/778] Multi-thread support by modulating over lead ID. Needs tests. --- .../Command/TriggerCampaignCommand.php | 16 +++++++- .../Entity/ContactLimiterTrait.php | 8 ++++ .../ContactFinder/Limiter/ContactLimiter.php | 41 ++++++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index c5d3d0ac7cd..892291f1c7d 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -191,6 +191,18 @@ protected function configure() 'Trigger events starting up to a specific contact ID.', null ) + ->addOption( + '--thread-id', + null, + InputOption::VALUE_OPTIONAL, + 'The number of this current process if running multiple in parallel.' + ) + ->addOption( + '--max-thread-id', + null, + InputOption::VALUE_OPTIONAL, + 'The maximum number of processes you intend to run in parallel.' + ) ->addOption( '--kickoff-only', null, @@ -247,8 +259,10 @@ protected function execute(InputInterface $input, OutputInterface $output) $contactMaxId = $input->getOption('max-contact-id'); $contactId = $input->getOption('contact-id'); $contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int'); + $threadId = $input->getOption('thread-id'); + $threadMaxId = $input->getOption('max-thread-id'); - $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds); + $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $threadMaxId); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); diff --git a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php index 86f91df90c9..f9aa0485176 100644 --- a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php +++ b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php @@ -54,6 +54,14 @@ private function updateQueryFromContactLimiter($alias, QueryBuilder $qb, Contact ->setParameter('maxContactId', $maxContactId); } + if ($threadId = $contactLimiter->getThreadId() && $threadMaxId = $contactLimiter->getThreadMaxId()) { + if ($threadId < $threadMaxId) { + $qb->andWhere("MOD(($alias.lead_id + :threadShift), :threadMax) = 0"); + $qb->setParameter('threadShift', $threadId - 1); + $qb->setParameter('threadMax', $threadMaxId); + } + } + if ($limit = $contactLimiter->getBatchLimit()) { $qb->setMaxResults($limit); } diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php index e8df03fead0..87e78cfce8a 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -48,6 +48,16 @@ class ContactLimiter */ private $contactIdList; + /** + * @var int|null + */ + private $threadId; + + /** + * @var int|null + */ + private $maxThreadId; + /** * ContactLimiter constructor. * @@ -56,14 +66,25 @@ class ContactLimiter * @param $minContactId * @param $maxContactId * @param array $contactIdList + * @param $threadId + * @param $maxThreadId */ - public function __construct($batchLimit, $contactId, $minContactId, $maxContactId, array $contactIdList = []) - { + public function __construct( + $batchLimit, + $contactId, + $minContactId, + $maxContactId, + array $contactIdList = [], + $threadId, + $maxThreadId + ) { $this->batchLimit = ($batchLimit) ? (int) $batchLimit : 100; $this->contactId = ($contactId) ? (int) $contactId : null; $this->minContactId = ($minContactId) ? (int) $minContactId : null; $this->maxContactId = ($maxContactId) ? (int) $maxContactId : null; $this->contactIdList = $contactIdList; + $this->threadId = ($threadId) ? (int) $threadId : null; + $this->maxThreadId = ($maxThreadId && $this->threadId) ? (int) $maxThreadId : null; } /** @@ -130,4 +151,20 @@ public function setBatchMinContactId($id) $this->batchMinContactId = (int) $id; } + + /** + * @return int|null + */ + public function getThreadMaxId() + { + return $this->maxThreadId; + } + + /** + * @return int|null + */ + public function getThreadId() + { + return $this->threadId; + } } From c60fdf6d37f4d332a302126fa80f26bd1dd6c2bf Mon Sep 17 00:00:00 2001 From: heathdutton Date: Mon, 14 May 2018 15:37:07 -0400 Subject: [PATCH 488/778] Consistent variable names. --- .../CampaignBundle/Entity/ContactLimiterTrait.php | 10 +++++----- .../ContactFinder/Limiter/ContactLimiter.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php index f9aa0485176..a7aa46d4358 100644 --- a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php +++ b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php @@ -54,11 +54,11 @@ private function updateQueryFromContactLimiter($alias, QueryBuilder $qb, Contact ->setParameter('maxContactId', $maxContactId); } - if ($threadId = $contactLimiter->getThreadId() && $threadMaxId = $contactLimiter->getThreadMaxId()) { - if ($threadId < $threadMaxId) { - $qb->andWhere("MOD(($alias.lead_id + :threadShift), :threadMax) = 0"); - $qb->setParameter('threadShift', $threadId - 1); - $qb->setParameter('threadMax', $threadMaxId); + if ($threadId = $contactLimiter->getThreadId() && $maxThreadId = $contactLimiter->getMaxThreadId()) { + if ($threadId <= $maxThreadId) { + $qb->andWhere("MOD(($alias.lead_id + :threadShift), :maxThreadId) = 0") + ->setParameter('threadShift', $threadId - 1) + ->setParameter('maxThreadId', $maxThreadId); } } diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php index 87e78cfce8a..c6885d15501 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -155,7 +155,7 @@ public function setBatchMinContactId($id) /** * @return int|null */ - public function getThreadMaxId() + public function getMaxThreadId() { return $this->maxThreadId; } From c6ab3d25add8b4250123a4bf51278908c11ec383 Mon Sep 17 00:00:00 2001 From: heathdutton Date: Mon, 14 May 2018 15:39:37 -0400 Subject: [PATCH 489/778] Consistent variable names. --- app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 892291f1c7d..204f9f34a54 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -260,9 +260,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $contactId = $input->getOption('contact-id'); $contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int'); $threadId = $input->getOption('thread-id'); - $threadMaxId = $input->getOption('max-thread-id'); + $maxThreadId = $input->getOption('max-thread-id'); - $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $threadMaxId); + $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $maxThreadId); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); From d5dd7f722625273da12a9efa4307285c9484d440 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 15 May 2018 12:44:52 -0600 Subject: [PATCH 490/778] Don't save booleans to BC timeline --- .../Executioner/Dispatcher/LegacyEventDispatcher.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php index 67e6e3b93d6..b3bf3525fa4 100644 --- a/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php +++ b/app/bundles/CampaignBundle/Executioner/Dispatcher/LegacyEventDispatcher.php @@ -138,7 +138,9 @@ public function dispatchCustomEvent( if (!$wasBatchProcessed) { $this->dispatchExecutionEvent($config, $log, $result); - $log->appendToMetadata($result); + if (!is_bool($result)) { + $log->appendToMetadata($result); + } // Dispatch new events for legacy processed logs if ($this->isFailed($result)) { From 3c30c604cd0a6a8caf0e7fc9d9ac850cfeb269f1 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 15 May 2018 13:30:24 -0600 Subject: [PATCH 491/778] Fixed issue where subsequent decisions were not analyzed for inactive contacts --- .../Executioner/InactiveExecutioner.php | 93 ++++++++++--------- .../Executioner/KickoffExecutioner.php | 4 - 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 9676953f12b..36a226d73c0 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -90,11 +90,6 @@ class InactiveExecutioner implements ExecutionerInterface */ private $helper; - /** - * @var int - */ - private $startAtContactId = 0; - /** * InactiveExecutioner constructor. * @@ -262,50 +257,58 @@ private function executeEvents() /** @var Event $decisionEvent */ foreach ($this->decisions as $decisionEvent) { - // We need the parent ID of the decision in order to fetch the time the contact executed this event - $parentEvent = $decisionEvent->getParent(); - $parentEventId = ($parentEvent) ? $parentEvent->getId() : null; - - // Ge the first batch of contacts - $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->limiter); - - // Loop over all contacts till we've processed all those applicable for this decision - while ($contacts->count()) { - // Get the max contact ID before any are removed - $batchMinContactId = max($contacts->getKeys()) + 1; - - $this->progressBar->advance($contacts->count()); - $this->counter->advanceEvaluated($contacts->count()); - - $inactiveEvents = $decisionEvent->getNegativeChildren(); - $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); - $earliestLastActiveDateTime = $this->helper->getEarliestInactiveDateTime(); - - $this->logger->debug( - 'CAMPAIGN: ('.$decisionEvent->getId().') Earliest date for inactivity for this batch of contacts is '. - $earliestLastActiveDateTime->format('Y-m-d H:i:s T') - ); - - if ($contacts->count()) { - // Execute or schedule the events attached to the inactive side of the decision - $this->executeLogsForInactiveEvents($inactiveEvents, $contacts, $this->counter, $earliestLastActiveDateTime); - // Record decision for these contacts - $this->executioner->recordLogsAsExecutedForEvent($decisionEvent, $contacts, true); - } + try { + // Ensure the batch min is reset from the last decision event + $this->limiter->setBatchMinContactId(null); - // Clear contacts from memory - $this->inactiveContactFinder->clear(); + // We need the parent ID of the decision in order to fetch the time the contact executed this event + $parentEvent = $decisionEvent->getParent(); + $parentEventId = ($parentEvent) ? $parentEvent->getId() : null; - if ($this->limiter->getContactId()) { - // No use making another call - break; - } + // Ge the first batch of contacts + $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->limiter); - $this->logger->debug('CAMPAIGN: Fetching the next batch of inactive contacts starting with contact ID '.$batchMinContactId); - $this->limiter->setBatchMinContactId($batchMinContactId); + // Loop over all contacts till we've processed all those applicable for this decision + while ($contacts->count()) { + // Get the max contact ID before any are removed + $batchMinContactId = max($contacts->getKeys()) + 1; - // Get the next batch, starting with the max contact ID - $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->limiter); + $this->progressBar->advance($contacts->count()); + $this->counter->advanceEvaluated($contacts->count()); + + $inactiveEvents = $decisionEvent->getNegativeChildren(); + $this->helper->removeContactsThatAreNotApplicable($now, $contacts, $parentEventId, $inactiveEvents); + $earliestLastActiveDateTime = $this->helper->getEarliestInactiveDateTime(); + + $this->logger->debug( + 'CAMPAIGN: ('.$decisionEvent->getId().') Earliest date for inactivity for this batch of contacts is '. + $earliestLastActiveDateTime->format('Y-m-d H:i:s T') + ); + + if ($contacts->count()) { + // Execute or schedule the events attached to the inactive side of the decision + $this->executeLogsForInactiveEvents($inactiveEvents, $contacts, $this->counter, $earliestLastActiveDateTime); + // Record decision for these contacts + $this->executioner->recordLogsAsExecutedForEvent($decisionEvent, $contacts, true); + } + + // Clear contacts from memory + $this->inactiveContactFinder->clear(); + + if ($this->limiter->getContactId()) { + // No use making another call + break; + } + + $this->logger->debug('CAMPAIGN: Fetching the next batch of inactive contacts starting with contact ID '.$batchMinContactId); + $this->limiter->setBatchMinContactId($batchMinContactId); + + // Get the next batch, starting with the max contact ID + $contacts = $this->inactiveContactFinder->getContacts($this->campaign->getId(), $decisionEvent, $this->limiter); + } + } catch (NoContactsFoundException $exception) { + // On to the next decision + $this->logger->debug('CAMPAIGN: No more contacts to process for decision ID #'.$decisionEvent->getId()); } } } diff --git a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php index c5757f15c1b..d2fe2b941c5 100644 --- a/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/KickoffExecutioner.php @@ -197,10 +197,6 @@ private function executeOrScheduleEvent() $now = new \DateTime(); $this->counter->advanceEventCount($this->rootEvents->count()); - // We have to keep track of this in case the campaign begins with a decision in order - // to prevent a never ending loop - $originalMinContactId = $this->limiter->getMinContactId(); - // Loop over contacts until the entire campaign is executed $contacts = $this->kickoffContactFinder->getContacts($this->campaign->getId(), $this->limiter); while ($contacts->count()) { From 58d6118348ecd18a31f79ace5cf85143228ddcb3 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 15 May 2018 17:57:32 -0600 Subject: [PATCH 492/778] Make campaign based dynamic content work in landing pages --- .../DynamicContentApiController.php | 9 ++--- .../DynamicContentSubscriber.php | 36 +++++++++---------- .../Helper/DynamicContentHelper.php | 29 ++++++++------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/app/bundles/DynamicContentBundle/Controller/DynamicContentApiController.php b/app/bundles/DynamicContentBundle/Controller/DynamicContentApiController.php index 53143f4deb8..e03d3944cba 100644 --- a/app/bundles/DynamicContentBundle/Controller/DynamicContentApiController.php +++ b/app/bundles/DynamicContentBundle/Controller/DynamicContentApiController.php @@ -56,13 +56,8 @@ public function getAction($objectAlias) $pageModel = $this->getModel('page'); /** @var Lead $lead */ - $lead = $model->getContactFromRequest($pageModel->getHitQuery($this->request)); - $content = $helper->getDynamicContentForLead($objectAlias, $lead); - - if (empty($content)) { - $content = $helper->getDynamicContentSlotForLead($objectAlias, $lead); - } - + $lead = $model->getContactFromRequest($pageModel->getHitQuery($this->request)); + $content = $helper->getDynamicContentForLead($objectAlias, $lead); $trackedDevice = $deviceTrackingService->getTrackedDevice(); $deviceId = ($trackedDevice === null ? null : $trackedDevice->getTrackingId()); diff --git a/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php b/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php index 9db47bd1d6f..0db1da0a824 100644 --- a/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php +++ b/app/bundles/DynamicContentBundle/EventListener/DynamicContentSubscriber.php @@ -208,8 +208,12 @@ public function onTokenReplacement(MauticEvents\TokenReplacementEvent $event) */ public function decodeTokens(PageDisplayEvent $event) { + $lead = $this->security->isAnonymous() ? $this->leadModel->getCurrentLead() : null; + if (!$lead) { + return; + } + $content = $event->getContent(); - $lead = $this->security->isAnonymous() ? $this->leadModel->getCurrentLead() : null; $tokens = $this->dynamicContentHelper->findDwcTokens($content, $lead); $leadArray = []; if ($lead instanceof Lead) { @@ -218,7 +222,7 @@ public function decodeTokens(PageDisplayEvent $event) $result = []; foreach ($tokens as $token => $dwc) { $result[$token] = ''; - if ($lead && $this->matchFilterForLead($dwc['filters'], $leadArray)) { + if ($this->matchFilterForLead($dwc['filters'], $leadArray)) { $result[$token] = $dwc['content']; } } @@ -231,24 +235,20 @@ public function decodeTokens(PageDisplayEvent $event) $divContent = $xpath->query('//*[@data-slot="dwc"]'); for ($i = 0; $i < $divContent->length; ++$i) { - $slot = $divContent->item($i); - $slotName = $slot->getAttribute('data-param-slot-name'); - $dwcs = $this->dynamicContentHelper->getDwcsBySlotName($slotName); - /** @var DynamicContent $dwc */ - foreach ($dwcs as $dwc) { - if ($dwc->getIsCampaignBased()) { - continue; - } - if ($lead && $this->matchFilterForLead($dwc->getFilters(), $leadArray)) { - $slotContent = $lead ? $this->dynamicContentHelper->getRealDynamicContent($dwc->getSlotName(), $lead, $dwc) : ''; - $newnode = $dom->createDocumentFragment(); - $newnode->appendXML(mb_convert_encoding($slotContent, 'HTML-ENTITIES', 'UTF-8')); - // in case we want to just change the slot contents: - // $slot->appendChild($newnode); - $slot->parentNode->replaceChild($newnode, $slot); - } + $slot = $divContent->item($i); + if (!$slotName = $slot->getAttribute('data-param-slot-name')) { + continue; + } + + if (!$slotContent = $this->dynamicContentHelper->getDynamicContentForLead($slotName, $lead)) { + continue; } + + $newnode = $dom->createDocumentFragment(); + $newnode->appendXML(mb_convert_encoding($slotContent, 'HTML-ENTITIES', 'UTF-8')); + $slot->parentNode->replaceChild($newnode, $slot); } + $content = $dom->saveHTML(); $event->setContent($content); diff --git a/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php b/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php index a7cb108f49e..8a4741bf702 100644 --- a/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php +++ b/app/bundles/DynamicContentBundle/Helper/DynamicContentHelper.php @@ -63,24 +63,26 @@ public function __construct(DynamicContentModel $dynamicContentModel, EventModel */ public function getDynamicContentForLead($slot, $lead) { + // Attempt campaign slots first $response = $this->campaignEventModel->triggerEvent('dwc.decision', $slot, 'dynamicContent'); - $content = ''; - if (is_array($response) && !empty($response['action']['dwc.push_content'])) { - $content = array_shift($response['action']['dwc.push_content']); - } else { - $data = $this->dynamicContentModel->getSlotContentForLead($slot, $lead); - - if (!empty($data)) { - $content = $data['content']; - $dwc = $this->dynamicContentModel->getEntity($data['id']); - if ($dwc instanceof DynamicContent) { - $content = $this->getRealDynamicContent($slot, $lead, $dwc); - } + return array_shift($response['action']['dwc.push_content']); + } + + // Attempt stored content second + $data = $this->dynamicContentModel->getSlotContentForLead($slot, $lead); + if (!empty($data)) { + $content = $data['content']; + $dwc = $this->dynamicContentModel->getEntity($data['id']); + if ($dwc instanceof DynamicContent) { + $content = $this->getRealDynamicContent($slot, $lead, $dwc); } + + return $content; } - return $content; + // Finally attempt standalone DWC + return $this->getDynamicContentSlotForLead($slot, $lead); } /** @@ -95,6 +97,7 @@ public function getDynamicContentSlotForLead($slotName, $lead) if ($lead instanceof Lead) { $leadArray = $this->convertLeadToArray($lead); } + $dwcs = $this->getDwcsBySlotName($slotName, true); /** @var DynamicContent $dwc */ foreach ($dwcs as $dwc) { From 86584a902646c000cb403844e11ef8c1bf7bd81d Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 13:40:54 -0600 Subject: [PATCH 493/778] Fixed tests from adding the thread command --- .../Command/TriggerCampaignCommand.php | 6 + .../ContactFinder/Limiter/ContactLimiter.php | 22 ++-- .../Tests/Entity/ContactLimiterTraitTest.php | 115 ++++++++++++++++++ 3 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 204f9f34a54..353c2b9d5f2 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -262,6 +262,12 @@ protected function execute(InputInterface $input, OutputInterface $output) $threadId = $input->getOption('thread-id'); $maxThreadId = $input->getOption('max-thread-id'); + if ($threadId && $maxThreadId && (int) $threadId > (int) $maxThreadId) { + $this->output->writeln('--thread-id cannot be larger than --max-thread-id'); + + return 1; + } + $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $maxThreadId); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php index c6885d15501..e7ed2d5c62e 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -61,22 +61,22 @@ class ContactLimiter /** * ContactLimiter constructor. * - * @param $batchLimit - * @param $contactId - * @param $minContactId - * @param $maxContactId - * @param array $contactIdList - * @param $threadId - * @param $maxThreadId + * @param int $batchLimit + * @param int $contactId + * @param int|null $minContactId + * @param int|null $maxContactId + * @param array $contactIdList + * @param int|null $threadId + * @param int|null $maxThreadId */ public function __construct( $batchLimit, $contactId, - $minContactId, - $maxContactId, + $minContactId = null, + $maxContactId = null, array $contactIdList = [], - $threadId, - $maxThreadId + $threadId = null, + $maxThreadId = null ) { $this->batchLimit = ($batchLimit) ? (int) $batchLimit : 100; $this->contactId = ($contactId) ? (int) $contactId : null; diff --git a/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php b/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php new file mode 100644 index 00000000000..05cee4dc2d3 --- /dev/null +++ b/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php @@ -0,0 +1,115 @@ +connection = $this->createMock(Connection::class); + + $expr = new ExpressionBuilder($this->connection); + $this->connection->method('getExpressionBuilder') + ->willReturn($expr); + + $platform = $this->createMock(AbstractPlatform::class); + $this->connection->method('getDatabasePlatform') + ->willReturn($platform); + } + + public function testSpecificContactId() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, 1); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE l.lead_id = :contactId LIMIT 50', $qb->getSQL()); + $this->assertEquals(['contactId' => 1], $qb->getParameters()); + } + + public function testListOfContacts() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, null, null, null, [1, 2, 3]); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE l.lead_id IN (:contactIds) LIMIT 50', $qb->getSQL()); + $this->assertEquals(['contactIds' => [1, 2, 3]], $qb->getParameters()); + } + + public function testMinContactId() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, null, 4, null); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE l.lead_id >= :minContactId LIMIT 50', $qb->getSQL()); + $this->assertEquals(['minContactId' => 4], $qb->getParameters()); + } + + public function testBatchMinContactId() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, null, 4, null); + $contactLimiter->setBatchMinContactId(10); + + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE l.lead_id >= :minContactId LIMIT 50', $qb->getSQL()); + $this->assertEquals(['minContactId' => 10], $qb->getParameters()); + } + + public function testMaxContactId() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, null, null, 10); + + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE l.lead_id <= :maxContactId LIMIT 50', $qb->getSQL()); + $this->assertEquals(['maxContactId' => 10], $qb->getParameters()); + } + + public function testMinAndMaxContactId() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, null, 1, 10); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE l.lead_id BETWEEN :minContactId AND :maxContactId LIMIT 50', $qb->getSQL()); + $this->assertEquals(['minContactId' => 1, 'maxContactId' => 10], $qb->getParameters()); + } + + public function testThreads() + { + $qb = new QueryBuilder($this->connection); + $contactLimiter = new ContactLimiter(50, null, null, null, [], 1, 5); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + + $this->assertEquals('SELECT WHERE MOD((l.lead_id + :threadShift), :maxThreadId) = 0 LIMIT 50', $qb->getSQL()); + $this->assertEquals(['threadShift' => 0, 'maxThreadId' => 5], $qb->getParameters()); + } +} From 65005fe5ccca35c4b78b932574d714a62e142f6e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 13:43:56 -0600 Subject: [PATCH 494/778] Increased sleep time as some automated servers don't execute process quick enough to have a passing test --- .../Tests/Command/TriggerCampaignCommandTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php index 824d3b0783b..cf6593b74c0 100644 --- a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php @@ -61,7 +61,7 @@ public function testCampaignExecutionForAll() $this->assertCount(0, $stats); // Wait 15 seconds then execute the campaign again to send scheduled events - sleep(15); + sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Send email 1 should no longer be scheduled @@ -104,7 +104,7 @@ public function testCampaignExecutionForAll() $this->assertCount(25, $byEvent[10]); // Wait 15 seconds to go beyond the inaction timeframe - sleep(15); + sleep(20); // Execute the command again to trigger inaction related events $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); @@ -222,7 +222,7 @@ public function testCampaignExecutionForOne() $this->assertCount(0, $stats); // Wait 15 seconds then execute the campaign again to send scheduled events - sleep(15); + sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); // Send email 1 should no longer be scheduled @@ -265,7 +265,7 @@ public function testCampaignExecutionForOne() $this->assertCount(1, $byEvent[10]); // Wait 15 seconds to go beyond the inaction timeframe - sleep(15); + sleep(20); // Execute the command again to trigger inaction related events $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); @@ -377,7 +377,7 @@ public function testCampaignExecutionForSome() $this->assertCount(0, $stats); // Wait 15 seconds then execute the campaign again to send scheduled events - sleep(15); + sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); // Send email 1 should no longer be scheduled @@ -420,7 +420,7 @@ public function testCampaignExecutionForSome() $this->assertCount(2, $byEvent[10]); // Wait 15 seconds to go beyond the inaction timeframe - sleep(15); + sleep(20); // Execute the command again to trigger inaction related events $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); From 9ba506dfb9357e23805c0ac2df707a2fedfb246f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 13:46:19 -0600 Subject: [PATCH 495/778] Fixed error message for when a contact has no email --- app/bundles/EmailBundle/EventListener/CampaignSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php index 80b33e2828d..2162363f0b0 100644 --- a/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/CampaignSubscriber.php @@ -315,7 +315,7 @@ public function onCampaignTriggerActionSendEmailToContact(PendingEvent $event) $event->passWithError( $pending->get($logId), $this->translator->trans( - 'mautic.email.contact_already_received_marketing_email', + 'mautic.email.contact_has_no_email', ['%contact%' => $contact->getPrimaryIdentifier()] ) ); From fa0b87609ef2b1da5f5a528830c78a370855986e Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 14:14:40 -0600 Subject: [PATCH 496/778] Ensure that Lead associations in logs found by executeByIds have custom field data hydrated --- .../CampaignBundle/Executioner/ScheduledExecutioner.php | 3 +++ .../Tests/Executioner/ScheduledExecutionerTest.php | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php index 23a916fd5a8..1475fbfb26c 100644 --- a/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/ScheduledExecutioner.php @@ -206,6 +206,9 @@ public function executeByIds(array $logIds, OutputInterface $output = null) $scheduledLogCount = $totalLogsFound - $logs->count(); $this->progressBar->advance($scheduledLogCount); + // Hydrate contacts with custom field data + $this->scheduledContactFinder->hydrateContacts($logs); + // Organize the logs by event ID $organized = $this->organizeByEvent($logs); foreach ($organized as $organizedLogs) { diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php index a68ce179700..f102f5f9860 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ScheduledExecutionerTest.php @@ -189,6 +189,10 @@ public function testSpecificEventsAreExecuted() $this->executioner->expects($this->exactly(1)) ->method('executeLogs'); + $this->contactFinder->expects($this->once()) + ->method('hydrateContacts') + ->with($logs); + $counter = $this->getExecutioner()->executeByIds([1, 2]); // Two events were evaluated From 19812f7214ebb82c678a16eb39118afaf287c85b Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 14:49:28 -0600 Subject: [PATCH 497/778] Add event ID to preview in dev env to help debugging --- app/bundles/CampaignBundle/Views/Event/preview.html.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/bundles/CampaignBundle/Views/Event/preview.html.php b/app/bundles/CampaignBundle/Views/Event/preview.html.php index 318c8a1be08..8b02c2db062 100644 --- a/app/bundles/CampaignBundle/Views/Event/preview.html.php +++ b/app/bundles/CampaignBundle/Views/Event/preview.html.php @@ -14,13 +14,14 @@ $eventType = $event['eventType']; $eventLogic = ''; - +// Show ID in dev mode to help with debugging +$eventName = ('dev' === MAUTIC_ENV) ? "{$event['name']} {$event['id']}" : $event['name']; ?>
-
+
trans('mautic.campaign.'.$event['type']); ?>
From d08fb7e0fa712595228f3ab83935ec92ec90042d Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 16:09:24 -0600 Subject: [PATCH 498/778] Reset batchMinContactId between each executioner to prevent catching inactive events --- .../Command/TriggerCampaignCommand.php | 22 +++++++++--- .../ContactFinder/Limiter/ContactLimiter.php | 35 +++++++++++++++---- .../Executioner/InactiveExecutioner.php | 7 ++-- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php index 353c2b9d5f2..b93fb1ddd8d 100644 --- a/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php +++ b/app/bundles/CampaignBundle/Command/TriggerCampaignCommand.php @@ -198,7 +198,7 @@ protected function configure() 'The number of this current process if running multiple in parallel.' ) ->addOption( - '--max-thread-id', + '--max-threads', null, InputOption::VALUE_OPTIONAL, 'The maximum number of processes you intend to run in parallel.' @@ -260,15 +260,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $contactId = $input->getOption('contact-id'); $contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int'); $threadId = $input->getOption('thread-id'); - $maxThreadId = $input->getOption('max-thread-id'); + $maxThreads = $input->getOption('max-threads'); - if ($threadId && $maxThreadId && (int) $threadId > (int) $maxThreadId) { - $this->output->writeln('--thread-id cannot be larger than --max-thread-id'); + if ($threadId && $maxThreads && (int) $threadId > (int) $maxThreads) { + $this->output->writeln('--thread-id cannot be larger than --max-thread'); return 1; } - $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $maxThreadId); + $this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $maxThreads); defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1); @@ -345,14 +345,26 @@ private function triggerCampaign(Campaign $campaign) try { $this->output->writeln(''.$this->translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $campaign->getId()]).''); + // Reset batch limiter + $this->limiter->resetBatchMinContactId(); + + // Execute starting events if (!$this->inactiveOnly && !$this->scheduleOnly) { $this->executeKickoff(); } + // Reset batch limiter + $this->limiter->resetBatchMinContactId(); + + // Execute scheduled events if (!$this->inactiveOnly && !$this->kickoffOnly) { $this->executeScheduled(); } + // Reset batch limiter + $this->limiter->resetBatchMinContactId(); + + // Execute inactive events if (!$this->scheduleOnly && !$this->kickoffOnly) { $this->executeInactive(); } diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php index e7ed2d5c62e..894b3d81fa5 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/Limiter/ContactLimiter.php @@ -56,7 +56,7 @@ class ContactLimiter /** * @var int|null */ - private $maxThreadId; + private $maxThreads; /** * ContactLimiter constructor. @@ -67,7 +67,7 @@ class ContactLimiter * @param int|null $maxContactId * @param array $contactIdList * @param int|null $threadId - * @param int|null $maxThreadId + * @param int|null $maxThreads */ public function __construct( $batchLimit, @@ -76,15 +76,22 @@ public function __construct( $maxContactId = null, array $contactIdList = [], $threadId = null, - $maxThreadId = null + $maxThreads = null ) { $this->batchLimit = ($batchLimit) ? (int) $batchLimit : 100; $this->contactId = ($contactId) ? (int) $contactId : null; $this->minContactId = ($minContactId) ? (int) $minContactId : null; $this->maxContactId = ($maxContactId) ? (int) $maxContactId : null; $this->contactIdList = $contactIdList; - $this->threadId = ($threadId) ? (int) $threadId : null; - $this->maxThreadId = ($maxThreadId && $this->threadId) ? (int) $maxThreadId : null; + + if ($threadId && $maxThreads) { + $this->threadId = (int) $threadId; + $this->maxThreads = (int) $maxThreads; + + if ($threadId > $maxThreads) { + throw new \InvalidArgumentException('$threadId cannot be larger than $maxThreads'); + } + } } /** @@ -130,6 +137,8 @@ public function getContactIdList() /** * @param int $id * + * @return $this + * * @throws NoContactsFoundException */ public function setBatchMinContactId($id) @@ -150,14 +159,26 @@ public function setBatchMinContactId($id) } $this->batchMinContactId = (int) $id; + + return $this; + } + + /** + * @return $this + */ + public function resetBatchMinContactId() + { + $this->batchMinContactId = null; + + return $this; } /** * @return int|null */ - public function getMaxThreadId() + public function getMaxThreads() { - return $this->maxThreadId; + return $this->maxThreads; } /** diff --git a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php index 36a226d73c0..b865412cbfc 100644 --- a/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/InactiveExecutioner.php @@ -223,6 +223,7 @@ private function prepareForExecution() } $totalContacts = $this->inactiveContactFinder->getContactCount($this->campaign->getId(), $this->decisions->getKeys(), $this->limiter); + $this->output->writeln( $this->translator->trans( 'mautic.campaign.trigger.decision_count_analyzed', @@ -258,9 +259,6 @@ private function executeEvents() /** @var Event $decisionEvent */ foreach ($this->decisions as $decisionEvent) { try { - // Ensure the batch min is reset from the last decision event - $this->limiter->setBatchMinContactId(null); - // We need the parent ID of the decision in order to fetch the time the contact executed this event $parentEvent = $decisionEvent->getParent(); $parentEventId = ($parentEvent) ? $parentEvent->getId() : null; @@ -310,6 +308,9 @@ private function executeEvents() // On to the next decision $this->logger->debug('CAMPAIGN: No more contacts to process for decision ID #'.$decisionEvent->getId()); } + + // Ensure the batch min is reset from the last decision event + $this->limiter->resetBatchMinContactId(); } } From f37fe14dbf379e8565a9a08448408596d7080913 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 17 May 2018 16:31:53 -0600 Subject: [PATCH 499/778] Use ContactLimiterTrait in other places that consume a ContactLimiter object --- .../Entity/CampaignRepository.php | 16 +-- .../Entity/ContactLimiterTrait.php | 71 ++++++++++-- .../Entity/LeadEventLogRepository.php | 39 +------ .../CampaignBundle/Entity/LeadRepository.php | 36 +----- .../Tests/Entity/ContactLimiterTraitTest.php | 104 +++++++++++++++--- .../Limiter/ContactLimiterTest.php | 7 ++ 6 files changed, 163 insertions(+), 110 deletions(-) diff --git a/app/bundles/CampaignBundle/Entity/CampaignRepository.php b/app/bundles/CampaignBundle/Entity/CampaignRepository.php index 3157cf4555d..c463a0bad1c 100644 --- a/app/bundles/CampaignBundle/Entity/CampaignRepository.php +++ b/app/bundles/CampaignBundle/Entity/CampaignRepository.php @@ -551,21 +551,7 @@ public function getPendingEventContactCount($campaignId, array $pendingEvents, C ) ->setParameter('false', false, 'boolean'); - if ($leadId = $limiter->getContactId()) { - $q->andWhere( - $q->expr()->eq('cl.lead_id', (int) $leadId) - ); - } elseif ($minContactId = $limiter->getMinContactId()) { - $q->andWhere( - 'cl.lead_id BETWEEN :minContactId AND :maxContactId' - ) - ->setParameter('minContactId', $minContactId) - ->setParameter('maxContactId', $limiter->getMaxContactId()); - } elseif ($contactIds = $limiter->getContactIdList()) { - $q->andWhere( - $q->expr()->in('cl.lead_id', $contactIds) - ); - } + $this->updateQueryFromContactLimiter('cl', $q, $limiter, true); if (count($pendingEvents) > 0) { $sq = $this->getEntityManager()->getConnection()->createQueryBuilder(); diff --git a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php index a7aa46d4358..96f6f7dd625 100644 --- a/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php +++ b/app/bundles/CampaignBundle/Entity/ContactLimiterTrait.php @@ -12,17 +12,19 @@ namespace Mautic\CampaignBundle\Entity; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Query\QueryBuilder; +use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder; +use Doctrine\ORM\QueryBuilder as OrmQueryBuilder; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; trait ContactLimiterTrait { /** - * @param string $alias - * @param QueryBuilder $qb - * @param ContactLimiter $contactLimiter + * @param string $alias + * @param DbalQueryBuilder $qb + * @param ContactLimiter $contactLimiter + * @param bool $isCount */ - private function updateQueryFromContactLimiter($alias, QueryBuilder $qb, ContactLimiter $contactLimiter) + private function updateQueryFromContactLimiter($alias, DbalQueryBuilder $qb, ContactLimiter $contactLimiter, $isCount = false) { $minContactId = $contactLimiter->getMinContactId(); $maxContactId = $contactLimiter->getMaxContactId(); @@ -54,15 +56,64 @@ private function updateQueryFromContactLimiter($alias, QueryBuilder $qb, Contact ->setParameter('maxContactId', $maxContactId); } - if ($threadId = $contactLimiter->getThreadId() && $maxThreadId = $contactLimiter->getMaxThreadId()) { - if ($threadId <= $maxThreadId) { - $qb->andWhere("MOD(($alias.lead_id + :threadShift), :maxThreadId) = 0") + if ($threadId = $contactLimiter->getThreadId() && $maxThreads = $contactLimiter->getMaxThreads()) { + if ($threadId <= $maxThreads) { + $qb->andWhere("MOD(($alias.lead_id + :threadShift), :maxThreads) = 0") ->setParameter('threadShift', $threadId - 1) - ->setParameter('maxThreadId', $maxThreadId); + ->setParameter('maxThreads', $maxThreads); } } - if ($limit = $contactLimiter->getBatchLimit()) { + if (!$isCount && $limit = $contactLimiter->getBatchLimit()) { + $qb->setMaxResults($limit); + } + } + + /** + * @param string $alias + * @param OrmQueryBuilder $qb + * @param ContactLimiter $contactLimiter + * @param bool $isCount + */ + private function updateOrmQueryFromContactLimiter($alias, OrmQueryBuilder $qb, ContactLimiter $contactLimiter, $isCount = false) + { + $minContactId = $contactLimiter->getMinContactId(); + $maxContactId = $contactLimiter->getMaxContactId(); + if ($contactId = $contactLimiter->getContactId()) { + $qb->andWhere( + $qb->expr()->eq("IDENTITY($alias.lead)", ':contact') + ) + ->setParameter('contact', $contactId); + } elseif ($contactIds = $contactLimiter->getContactIdList()) { + $qb->andWhere( + $qb->expr()->in("IDENTITY($alias.lead)", ':contactIds') + ) + ->setParameter('contactIds', $contactIds, Connection::PARAM_INT_ARRAY); + } elseif ($minContactId && $maxContactId) { + $qb->andWhere( + "IDENTITY($alias.lead) BETWEEN :minContactId AND :maxContactId" + ) + ->setParameter('minContactId', $minContactId) + ->setParameter('maxContactId', $maxContactId); + } elseif ($minContactId) { + $qb->andWhere( + $qb->expr()->gte("IDENTITY($alias.lead)", ':minContactId') + ) + ->setParameter('minContactId', $minContactId); + } elseif ($maxContactId) { + $qb->andWhere( + $qb->expr()->lte("IDENTITY($alias.lead)", ':maxContactId') + ) + ->setParameter('maxContactId', $maxContactId); + } + + if ($threadId = $contactLimiter->getThreadId() && $maxThreads = $contactLimiter->getMaxThreads()) { + $qb->andWhere("MOD((IDENTITY($alias.lead) + :threadShift), :maxThreads) = 0") + ->setParameter('threadShift', $threadId - 1) + ->setParameter('maxThreads', $maxThreads); + } + + if (!$isCount && $limit = $contactLimiter->getBatchLimit()) { $qb->setMaxResults($limit); } } diff --git a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php index a560bf845ab..15d0fde35d8 100644 --- a/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadEventLogRepository.php @@ -24,6 +24,7 @@ class LeadEventLogRepository extends CommonRepository { use TimelineTrait; + use ContactLimiterTrait; public function getEntities(array $args = []) { @@ -415,26 +416,7 @@ public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter) ->setParameter('now', $now) ->setParameter('true', true, Type::BOOLEAN); - if ($contactId = $limiter->getContactId()) { - $q->andWhere( - $q->expr()->eq('IDENTITY(o.lead)', ':contactId') - ) - ->setParameter('contactId', (int) $contactId); - } elseif ($minContactId = $limiter->getMinContactId()) { - $q->andWhere( - $q->expr()->between('IDENTITY(o.lead)', ':minContactId', ':maxContactId') - ) - ->setParameter('minContactId', $minContactId) - ->setParameter('maxContactId', $limiter->getMaxContactId()); - } elseif ($contactIds = $limiter->getContactIdList()) { - $q->andWhere( - $q->expr()->in('IDENTITY(o.lead)', $contactIds) - ); - } - - if ($limit = $limiter->getBatchLimit()) { - $q->setMaxResults($limit); - } + $this->updateOrmQueryFromContactLimiter('o', $q, $limiter); return new ArrayCollection($q->getQuery()->getResult()); } @@ -486,22 +468,7 @@ public function getScheduledCounts($campaignId, \DateTime $date, ContactLimiter $q->expr()->eq('c.is_published', 1) ); - if ($contactId = $limiter->getContactId()) { - $expr->add( - $q->expr()->eq('l.lead_id', ':contactId') - ); - $q->setParameter('contactId', (int) $contactId); - } elseif ($minContactId = $limiter->getMinContactId()) { - $expr->add( - 'l.lead_id BETWEEN :minContactId AND :maxContactId' - ); - $q->setParameter('minContactId', $minContactId) - ->setParameter('maxContactId', $limiter->getMaxContactId()); - } elseif ($contactIds = $limiter->getContactIdList()) { - $expr->add( - $q->expr()->in('l.lead_id', $contactIds) - ); - } + $this->updateQueryFromContactLimiter('l', $q, $limiter, true); $results = $q->select('COUNT(*) as event_count, l.event_id') ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'l') diff --git a/app/bundles/CampaignBundle/Entity/LeadRepository.php b/app/bundles/CampaignBundle/Entity/LeadRepository.php index 00461498915..61cdb70d995 100644 --- a/app/bundles/CampaignBundle/Entity/LeadRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadRepository.php @@ -207,6 +207,7 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); $q->select('l.lead_id, l.date_added') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') + ->where($q->expr()->eq('l.campaign_id', ':campaignId')) // Order by ID so we can query by greater than X contact ID when batching ->orderBy('l.lead_id') ->setMaxResults($limiter->getBatchLimit()) @@ -216,11 +217,6 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, // Contact IDs $this->updateQueryFromContactLimiter('l', $q, $limiter); - // Limit to specific campaign - $q->andWhere( - $q->expr()->eq('l.campaign_id', ':campaignId') - ); - // Limit to events that have not been executed or scheduled yet $eventQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); $eventQb->select('null') @@ -277,35 +273,13 @@ public function getInactiveContactCount($campaignId, array $decisionIds, Contact $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); $q->select('count(*)') ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') + ->where($q->expr()->eq('l.campaign_id', ':campaignId')) // Order by ID so we can query by greater than X contact ID when batching ->orderBy('l.lead_id') ->setParameter('campaignId', (int) $campaignId); // Contact IDs - $expr = $q->expr()->andX(); - if ($specificContactId = $limiter->getContactId()) { - // Still query for this ID in case the ID fed to the command no longer exists - $expr->add( - $q->expr()->eq('l.lead_id', ':contactId') - ); - $q->setParameter('contactId', $specificContactId); - } elseif ($minContactId = $limiter->getMinContactId()) { - // Still query for this ID in case the ID fed to the command no longer exists - $expr->add( - 'l.lead_id BETWEEN :minContactId AND :maxContactId' - ); - $q->setParameter('minContactId', $limiter->getMinContactId()) - ->setParameter('maxContactId', $limiter->getMaxContactId()); - } elseif ($contactIds = $limiter->getContactIdList()) { - $expr->add( - $q->expr()->in('l.lead_id', $contactIds) - ); - } - - // Limit to specific campaign - $expr->add( - $q->expr()->eq('l.campaign_id', ':campaignId') - ); + $this->updateQueryFromContactLimiter('l', $q, $limiter, true); // Limit to events that have not been executed or scheduled yet $eventQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); @@ -318,12 +292,10 @@ public function getInactiveContactCount($campaignId, array $decisionIds, Contact $eventQb->expr()->eq('log.rotation', 'l.rotation') ) ); - $expr->add( + $q->andWhere( sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) ); - $q->where($expr); - return (int) $q->execute()->fetchColumn(); } } diff --git a/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php b/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php index 05cee4dc2d3..5dadaa94bd0 100644 --- a/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php +++ b/app/bundles/CampaignBundle/Tests/Entity/ContactLimiterTraitTest.php @@ -14,7 +14,10 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; -use Doctrine\DBAL\Query\QueryBuilder; +use Doctrine\DBAL\Query\QueryBuilder as DbalQueryBuilder; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\Expr; +use Doctrine\ORM\QueryBuilder as OrmQueryBuilder; use Mautic\CampaignBundle\Entity\ContactLimiterTrait; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; @@ -27,6 +30,11 @@ class ContactLimiterTraitTest extends \PHPUnit_Framework_TestCase */ private $connection; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|EntityManagerInterface + */ + private $entityManager; + protected function setUp() { $this->connection = $this->createMock(Connection::class); @@ -38,78 +46,140 @@ protected function setUp() $platform = $this->createMock(AbstractPlatform::class); $this->connection->method('getDatabasePlatform') ->willReturn($platform); + + $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->entityManager->method('getExpressionBuilder') + ->willReturn(new Expr()); } public function testSpecificContactId() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, 1); - $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + $qb = new DbalQueryBuilder($this->connection); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); $this->assertEquals('SELECT WHERE l.lead_id = :contactId LIMIT 50', $qb->getSQL()); $this->assertEquals(['contactId' => 1], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) = :contact', $qb->getDQL()); + $this->assertEquals(1, $qb->getParameter('contact')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); } public function testListOfContacts() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, null, null, null, [1, 2, 3]); - $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + $qb = new DbalQueryBuilder($this->connection); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); $this->assertEquals('SELECT WHERE l.lead_id IN (:contactIds) LIMIT 50', $qb->getSQL()); $this->assertEquals(['contactIds' => [1, 2, 3]], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) IN(:contactIds)', $qb->getDQL()); + $this->assertEquals([1, 2, 3], $qb->getParameter('contactIds')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); } public function testMinContactId() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, null, 4, null); - $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + $qb = new DbalQueryBuilder($this->connection); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); $this->assertEquals('SELECT WHERE l.lead_id >= :minContactId LIMIT 50', $qb->getSQL()); $this->assertEquals(['minContactId' => 4], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) >= :minContactId', $qb->getDQL()); + $this->assertEquals(4, $qb->getParameter('minContactId')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); } public function testBatchMinContactId() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, null, 4, null); - $contactLimiter->setBatchMinContactId(10); + $qb = new DbalQueryBuilder($this->connection); + $contactLimiter->setBatchMinContactId(10); $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); - $this->assertEquals('SELECT WHERE l.lead_id >= :minContactId LIMIT 50', $qb->getSQL()); $this->assertEquals(['minContactId' => 10], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) >= :minContactId', $qb->getDQL()); + $this->assertEquals(10, $qb->getParameter('minContactId')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); } public function testMaxContactId() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, null, null, 10); + $qb = new DbalQueryBuilder($this->connection); $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); - $this->assertEquals('SELECT WHERE l.lead_id <= :maxContactId LIMIT 50', $qb->getSQL()); $this->assertEquals(['maxContactId' => 10], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) <= :maxContactId', $qb->getDQL()); + $this->assertEquals(10, $qb->getParameter('maxContactId')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); } public function testMinAndMaxContactId() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, null, 1, 10); - $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + $qb = new DbalQueryBuilder($this->connection); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); $this->assertEquals('SELECT WHERE l.lead_id BETWEEN :minContactId AND :maxContactId LIMIT 50', $qb->getSQL()); $this->assertEquals(['minContactId' => 1, 'maxContactId' => 10], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) BETWEEN :minContactId AND :maxContactId', $qb->getDQL()); + $this->assertEquals(1, $qb->getParameter('minContactId')->getValue()); + $this->assertEquals(10, $qb->getParameter('maxContactId')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); } public function testThreads() { - $qb = new QueryBuilder($this->connection); $contactLimiter = new ContactLimiter(50, null, null, null, [], 1, 5); + + $qb = new DbalQueryBuilder($this->connection); $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE MOD((l.lead_id + :threadShift), :maxThreads) = 0 LIMIT 50', $qb->getSQL()); + $this->assertEquals(['threadShift' => 0, 'maxThreads' => 5], $qb->getParameters()); + + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter); + $this->assertEquals('SELECT WHERE MOD((IDENTITY(l.lead) + :threadShift), :maxThreads) = 0', $qb->getDQL()); + $this->assertEquals(0, $qb->getParameter('threadShift')->getValue()); + $this->assertEquals(5, $qb->getParameter('maxThreads')->getValue()); + $this->assertEquals(50, $qb->getMaxResults()); + } + + public function testMaxResultsIgnoredForCountQueries() + { + $contactLimiter = new ContactLimiter(50, 1); + + $qb = new DbalQueryBuilder($this->connection); + $this->updateQueryFromContactLimiter('l', $qb, $contactLimiter, true); + $this->assertEquals('SELECT WHERE l.lead_id = :contactId', $qb->getSQL()); + $this->assertEquals(['contactId' => 1], $qb->getParameters()); - $this->assertEquals('SELECT WHERE MOD((l.lead_id + :threadShift), :maxThreadId) = 0 LIMIT 50', $qb->getSQL()); - $this->assertEquals(['threadShift' => 0, 'maxThreadId' => 5], $qb->getParameters()); + $qb = new OrmQueryBuilder($this->entityManager); + $this->updateOrmQueryFromContactLimiter('l', $qb, $contactLimiter, true); + $this->assertEquals('SELECT WHERE IDENTITY(l.lead) = :contact', $qb->getDQL()); + $this->assertEquals(1, $qb->getParameter('contact')->getValue()); + $this->assertEquals(null, $qb->getMaxResults()); } } diff --git a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php index 05ec7833a2b..47656183401 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/ContactFinder/Limiter/ContactLimiterTest.php @@ -71,4 +71,11 @@ public function testExceptionNotThrownIfIdEqualsMaxSoThatItsIsIncluded() $limiter = new ContactLimiter(1, 2, 3, 10, [1, 2, 3]); $limiter->setBatchMinContactId(10); } + + public function testExceptionThrownIfThreadIdLargerThanMaxThreads() + { + $this->expectException(\InvalidArgumentException::class); + + new ContactLimiter(1, null, null, null, [], 5, 3); + } } From cb0a3c3e8a24b1d9d59e85947fb7f6590eeddd32 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 18 May 2018 09:50:58 -0600 Subject: [PATCH 500/778] Fixed bad query that caused inactive count to be 0 which caused some inactive path events to not be executed --- .../CampaignBundle/Entity/LeadRepository.php | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/app/bundles/CampaignBundle/Entity/LeadRepository.php b/app/bundles/CampaignBundle/Entity/LeadRepository.php index 61cdb70d995..dfea8a886ca 100644 --- a/app/bundles/CampaignBundle/Entity/LeadRepository.php +++ b/app/bundles/CampaignBundle/Entity/LeadRepository.php @@ -269,33 +269,42 @@ public function getInactiveContacts($campaignId, $decisionId, $parentDecisionId, */ public function getInactiveContactCount($campaignId, array $decisionIds, ContactLimiter $limiter) { - // Main query - $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); - $q->select('count(*)') - ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') - ->where($q->expr()->eq('l.campaign_id', ':campaignId')) - // Order by ID so we can query by greater than X contact ID when batching - ->orderBy('l.lead_id') - ->setParameter('campaignId', (int) $campaignId); - - // Contact IDs - $this->updateQueryFromContactLimiter('l', $q, $limiter, true); - - // Limit to events that have not been executed or scheduled yet - $eventQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); - $eventQb->select('null') - ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') - ->where( - $eventQb->expr()->andX( - $eventQb->expr()->in('log.event_id', $decisionIds), - $eventQb->expr()->eq('log.lead_id', 'l.lead_id'), - $eventQb->expr()->eq('log.rotation', 'l.rotation') - ) + // We have to loop over each decision to get a count or else any contact that has executed any single one of the decision IDs + // will not be included potentially resulting in not having the inactive path analyzed + + $totalCount = 0; + + foreach ($decisionIds as $decisionId) { + // Main query + $q = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $q->select('count(*)') + ->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'l') + ->where($q->expr()->eq('l.campaign_id', ':campaignId')) + // Order by ID so we can query by greater than X contact ID when batching + ->orderBy('l.lead_id') + ->setParameter('campaignId', (int) $campaignId); + + // Contact IDs + $this->updateQueryFromContactLimiter('l', $q, $limiter, true); + + // Limit to events that have not been executed or scheduled yet + $eventQb = $this->getEntityManager()->getConnection()->createQueryBuilder(); + $eventQb->select('null') + ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log') + ->where( + $eventQb->expr()->andX( + $eventQb->expr()->eq('log.event_id', $decisionId), + $eventQb->expr()->eq('log.lead_id', 'l.lead_id'), + $eventQb->expr()->eq('log.rotation', 'l.rotation') + ) + ); + $q->andWhere( + sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) ); - $q->andWhere( - sprintf('NOT EXISTS (%s)', $eventQb->getSQL()) - ); - return (int) $q->execute()->fetchColumn(); + $totalCount += (int) $q->execute()->fetchColumn(); + } + + return $totalCount; } } From 46cb4c65a4e2961b7cebb7e2ed4d3e7959ca279c Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 21 May 2018 08:52:00 -0500 Subject: [PATCH 501/778] Increase wait time for slow servers to not fail tests --- .../Tests/Command/ExecuteEventCommandTest.php | 2 +- .../Tests/Command/ValidateEventCommandTest.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php index 48b5ef21be1..f9312fe97ab 100644 --- a/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php @@ -46,7 +46,7 @@ public function testEventsAreExecutedForInactiveEventWithSingleContact() $lastId = array_pop($logIds); // Wait 15 seconds to go past scheduled time - sleep(15); + sleep(20); $this->runCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => implode(',', $logIds)]); diff --git a/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php index 8bde63546be..233a6ea88cf 100644 --- a/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php @@ -18,7 +18,7 @@ public function testEventsAreExecutedForInactiveEventWithSingleContact() $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); // Wait 15 seconds then execute the campaign again to send scheduled events - sleep(15); + sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); // No open email decisions should be recorded yet @@ -26,7 +26,7 @@ public function testEventsAreExecutedForInactiveEventWithSingleContact() $this->assertCount(0, $byEvent[3]); // Wait 15 seconds to go beyond the inaction timeframe - sleep(15); + sleep(20); // Now they should be inactive $this->runCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-id' => 1]); @@ -42,7 +42,7 @@ public function testEventsAreExecutedForInactiveEventWithMultipleContact() $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); // Wait 15 seconds then execute the campaign again to send scheduled events - sleep(15); + sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); // No open email decisions should be recorded yet @@ -50,7 +50,7 @@ public function testEventsAreExecutedForInactiveEventWithMultipleContact() $this->assertCount(0, $byEvent[3]); // Wait 15 seconds to go beyond the inaction timeframe - sleep(15); + sleep(20); // Now they should be inactive $this->runCommand('mautic:campaigns:validate', ['--decision-id' => 3, '--contact-ids' => '1,2,3']); From ed336625cc517285a5d8682b2c92ff2065504b80 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 21 May 2018 11:43:08 -0500 Subject: [PATCH 502/778] Fixed failing tests after rebasing to staging due to changes in schema --- .../CampaignBundle/Tests/Command/campaign_schema.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql index 5eaeeb6ca47..14121df2afc 100644 --- a/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql +++ b/app/bundles/CampaignBundle/Tests/Command/campaign_schema.sql @@ -1,7 +1,7 @@ -INSERT INTO `#__emails` (`id`,`category_id`,`translation_parent_id`,`variant_parent_id`,`unsubscribeform_id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`subject`,`from_address`,`from_name`,`reply_to_address`,`bcc_address`,`template`,`content`,`utm_tags`,`plain_text`,`custom_html`,`email_type`,`publish_up`,`publish_down`,`read_count`,`sent_count`,`revision`,`lang`,`variant_settings`,`variant_start_date`,`dynamic_content`,`variant_sent_count`,`variant_read_count`,`preference_center_id`) +INSERT INTO `#__emails` (`headers`, `id`,`category_id`,`translation_parent_id`,`variant_parent_id`,`unsubscribeform_id`,`is_published`,`date_added`,`created_by`,`created_by_user`,`date_modified`,`modified_by`,`modified_by_user`,`checked_out`,`checked_out_by`,`checked_out_by_user`,`name`,`description`,`subject`,`from_address`,`from_name`,`reply_to_address`,`bcc_address`,`template`,`content`,`utm_tags`,`plain_text`,`custom_html`,`email_type`,`publish_up`,`publish_down`,`read_count`,`sent_count`,`revision`,`lang`,`variant_settings`,`variant_start_date`,`dynamic_content`,`variant_sent_count`,`variant_read_count`,`preference_center_id`) VALUES - (1,NULL,NULL,NULL,NULL,1,'2018-01-04 21:20:25',1,'Admin',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test Email 1',NULL,'Campaign Test Email 1',NULL,NULL,NULL,NULL,'blank','a:0:{}','a:4:{s:9:\"utmSource\";N;s:9:\"utmMedium\";N;s:11:\"utmCampaign\";N;s:10:\"utmContent\";N;}',NULL,'\n {subject}\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n
\n
\n
\n

Hello there!

\n
\n We haven\'t heard from you for a while...\n
\n
\n {unsubscribe_text} | {webview_text}\n
\n
\n
\n
\n
\n
\n','template',NULL,NULL,0,0,1,'en','a:0:{}',NULL,'a:1:{i:0;a:3:{s:9:\"tokenName\";s:17:\"Dynamic Content 1\";s:7:\"content\";s:23:\"Default Dynamic Content\";s:7:\"filters\";a:1:{i:0;a:2:{s:7:\"content\";N;s:7:\"filters\";a:0:{}}}}}',0,0,NULL), - (2,NULL,NULL,NULL,NULL,1,'2018-01-04 21:21:07',1,'Admin',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test Email 2',NULL,'Campaign Test Email 2',NULL,NULL,NULL,NULL,'blank','a:0:{}','a:4:{s:9:\"utmSource\";N;s:9:\"utmMedium\";N;s:11:\"utmCampaign\";N;s:10:\"utmContent\";N;}',NULL,'\n\n \n {subject}\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n
\n
\n
\n

Hello there!

\n
\n We haven\'t heard from you for a while...\n
\n
\n {unsubscribe_text} | {webview_text}\n
\n
\n
\n
\n
\n
\n \n','template',NULL,NULL,0,0,1,'en','a:0:{}',NULL,'a:1:{i:0;a:3:{s:9:\"tokenName\";s:17:\"Dynamic Content 1\";s:7:\"content\";s:23:\"Default Dynamic Content\";s:7:\"filters\";a:1:{i:0;a:2:{s:7:\"content\";N;s:7:\"filters\";a:0:{}}}}}',0,0,NULL); + ('[]',1,NULL,NULL,NULL,NULL,1,'2018-01-04 21:20:25',1,'Admin',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test Email 1',NULL,'Campaign Test Email 1',NULL,NULL,NULL,NULL,'blank','a:0:{}','a:4:{s:9:\"utmSource\";N;s:9:\"utmMedium\";N;s:11:\"utmCampaign\";N;s:10:\"utmContent\";N;}',NULL,'\n {subject}\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n
\n
\n
\n

Hello there!

\n
\n We haven\'t heard from you for a while...\n
\n
\n {unsubscribe_text} | {webview_text}\n
\n
\n
\n
\n
\n
\n','template',NULL,NULL,0,0,1,'en','a:0:{}',NULL,'a:1:{i:0;a:3:{s:9:\"tokenName\";s:17:\"Dynamic Content 1\";s:7:\"content\";s:23:\"Default Dynamic Content\";s:7:\"filters\";a:1:{i:0;a:2:{s:7:\"content\";N;s:7:\"filters\";a:0:{}}}}}',0,0,NULL), + ('[]',2,NULL,NULL,NULL,NULL,1,'2018-01-04 21:21:07',1,'Admin',NULL,NULL,NULL,NULL,NULL,NULL,'Campaign Test Email 2',NULL,'Campaign Test Email 2',NULL,NULL,NULL,NULL,'blank','a:0:{}','a:4:{s:9:\"utmSource\";N;s:9:\"utmMedium\";N;s:11:\"utmCampaign\";N;s:10:\"utmContent\";N;}',NULL,'\n\n \n {subject}\n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n
\n
\n
\n

Hello there!

\n
\n We haven\'t heard from you for a while...\n
\n
\n {unsubscribe_text} | {webview_text}\n
\n
\n
\n
\n
\n
\n \n','template',NULL,NULL,0,0,1,'en','a:0:{}',NULL,'a:1:{i:0;a:3:{s:9:\"tokenName\";s:17:\"Dynamic Content 1\";s:7:\"content\";s:23:\"Default Dynamic Content\";s:7:\"filters\";a:1:{i:0;a:2:{s:7:\"content\";N;s:7:\"filters\";a:0:{}}}}}',0,0,NULL); INSERT INTO `#__lead_tags` (`id`,`tag`) VALUES From 7e18b40d8071b5bc783b8f9614b5607f016ebc30 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Mon, 21 May 2018 11:43:24 -0500 Subject: [PATCH 503/778] Updated comment to reflect code --- .../Tests/Command/ExecuteEventCommandTest.php | 2 +- .../Tests/Command/TriggerCampaignCommandTest.php | 12 ++++++------ .../Tests/Command/ValidateEventCommandTest.php | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php index f9312fe97ab..6a1421cd22e 100644 --- a/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/ExecuteEventCommandTest.php @@ -45,7 +45,7 @@ public function testEventsAreExecutedForInactiveEventWithSingleContact() // Pop off the last so we can test that only the two given are executed $lastId = array_pop($logIds); - // Wait 15 seconds to go past scheduled time + // Wait 20 seconds to go past scheduled time sleep(20); $this->runCommand('mautic:campaigns:execute', ['--scheduled-log-ids' => implode(',', $logIds)]); diff --git a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php index cf6593b74c0..ca23bf7b414 100644 --- a/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/TriggerCampaignCommandTest.php @@ -60,7 +60,7 @@ public function testCampaignExecutionForAll() ->fetchAll(); $this->assertCount(0, $stats); - // Wait 15 seconds then execute the campaign again to send scheduled events + // Wait 20 seconds then execute the campaign again to send scheduled events sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); @@ -103,7 +103,7 @@ public function testCampaignExecutionForAll() $this->assertCount(25, $byEvent[3]); $this->assertCount(25, $byEvent[10]); - // Wait 15 seconds to go beyond the inaction timeframe + // Wait 20 seconds to go beyond the inaction timeframe sleep(20); // Execute the command again to trigger inaction related events @@ -221,7 +221,7 @@ public function testCampaignExecutionForOne() ->fetchAll(); $this->assertCount(0, $stats); - // Wait 15 seconds then execute the campaign again to send scheduled events + // Wait 20 seconds then execute the campaign again to send scheduled events sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); @@ -264,7 +264,7 @@ public function testCampaignExecutionForOne() $this->assertCount(1, $byEvent[3]); $this->assertCount(1, $byEvent[10]); - // Wait 15 seconds to go beyond the inaction timeframe + // Wait 20 seconds to go beyond the inaction timeframe sleep(20); // Execute the command again to trigger inaction related events @@ -376,7 +376,7 @@ public function testCampaignExecutionForSome() ->fetchAll(); $this->assertCount(0, $stats); - // Wait 15 seconds then execute the campaign again to send scheduled events + // Wait 20 seconds then execute the campaign again to send scheduled events sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); @@ -419,7 +419,7 @@ public function testCampaignExecutionForSome() $this->assertCount(2, $byEvent[3]); $this->assertCount(2, $byEvent[10]); - // Wait 15 seconds to go beyond the inaction timeframe + // Wait 20 seconds to go beyond the inaction timeframe sleep(20); // Execute the command again to trigger inaction related events diff --git a/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php b/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php index 233a6ea88cf..f69b7d94a57 100644 --- a/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php +++ b/app/bundles/CampaignBundle/Tests/Command/ValidateEventCommandTest.php @@ -17,7 +17,7 @@ public function testEventsAreExecutedForInactiveEventWithSingleContact() { $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); - // Wait 15 seconds then execute the campaign again to send scheduled events + // Wait 20 seconds then execute the campaign again to send scheduled events sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); @@ -25,7 +25,7 @@ public function testEventsAreExecutedForInactiveEventWithSingleContact() $byEvent = $this->getCampaignEventLogs([3]); $this->assertCount(0, $byEvent[3]); - // Wait 15 seconds to go beyond the inaction timeframe + // Wait 20 seconds to go beyond the inaction timeframe sleep(20); // Now they should be inactive @@ -41,7 +41,7 @@ public function testEventsAreExecutedForInactiveEventWithMultipleContact() { $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); - // Wait 15 seconds then execute the campaign again to send scheduled events + // Wait 20 seconds then execute the campaign again to send scheduled events sleep(20); $this->runCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3']); @@ -49,7 +49,7 @@ public function testEventsAreExecutedForInactiveEventWithMultipleContact() $byEvent = $this->getCampaignEventLogs([3]); $this->assertCount(0, $byEvent[3]); - // Wait 15 seconds to go beyond the inaction timeframe + // Wait 20 seconds to go beyond the inaction timeframe sleep(20); // Now they should be inactive From b8e855171742d99d8c1473bd5313abb75390f81f Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 22 May 2018 15:48:46 -0500 Subject: [PATCH 504/778] Citrix plugin doesn't have any decisions and thus should not be triggering the execution of one which picked up it's conditions instead leading to an exception. --- .../MauticCitrixBundle/Model/CitrixModel.php | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/plugins/MauticCitrixBundle/Model/CitrixModel.php b/plugins/MauticCitrixBundle/Model/CitrixModel.php index 90b60e81909..c8efbca4602 100644 --- a/plugins/MauticCitrixBundle/Model/CitrixModel.php +++ b/plugins/MauticCitrixBundle/Model/CitrixModel.php @@ -96,8 +96,6 @@ public function addEvent($product, $email, $eventName, $eventDesc, $eventType, $ $this->em->persist($citrixEvent); $this->em->flush(); - - $this->triggerCampaignEvents($product, $lead); } /** @@ -408,8 +406,6 @@ public function batchAddAndRemove( $this->dispatcher->dispatch(CitrixEvents::ON_CITRIX_EVENT_UPDATE, $citrixEvent); unset($citrixEvent); } - - $this->triggerCampaignEvents($product, $entity->getLead()); } } @@ -419,23 +415,6 @@ public function batchAddAndRemove( return $count; } - /** - * @param string $product - * @param string $lead - * - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException - */ - private function triggerCampaignEvents($product, $lead) - { - if (!CitrixProducts::isValidValue($product)) { - return; // is not a valid citrix product - } - - $this->leadModel->setSystemCurrentLead($lead); - $this->eventModel->triggerEvent('citrix.event.'.$product); - } - /** * @param $found * @param $known From 7a4265d8cff11fff451deece45820b50a5ca70ea Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 22 May 2018 15:49:42 -0500 Subject: [PATCH 505/778] Prevent plugins from triggering non-decision events with the RealTimeExecutioner --- .../Executioner/RealTimeExecutioner.php | 10 ++++++ .../Executioner/RealTimeExecutionerTest.php | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php index 3ce65362c08..65635d3109f 100644 --- a/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/RealTimeExecutioner.php @@ -223,6 +223,16 @@ private function evaluateDecisionForContact(Event $event, $passthrough = null, $ { $this->logger->debug('CAMPAIGN: Executing '.$event->getType().' ID '.$event->getId().' for contact ID '.$this->contact->getId()); + if ($event->getEventType() !== Event::TYPE_DECISION) { + @trigger_error( + "{$event->getType()} is not assigned to a decision and no longer supported. ". + 'Check that you are executing RealTimeExecutioner::execute for an event registered as a decision.', + E_USER_DEPRECATED + ); + + throw new DecisionNotApplicableException("Event {$event->getId()} is not a decision."); + } + // If channels do not match up, there's no need to go further if ($channel && $event->getChannel() && $channel !== $event->getChannel()) { throw new DecisionNotApplicableException("Channels, $channel and {$event->getChannel()}, do not match."); diff --git a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php index e78c12bbec8..bda32f4ecec 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php @@ -303,6 +303,38 @@ public function testAssociatedEventsAreExecuted() $this->assertEquals(0, $responses->containsResponses()); } + public function testNonDecisionEventsAreIgnored() + { + $lead = $this->getMockBuilder(Lead::class) + ->getMock(); + $lead->expects($this->exactly(5)) + ->method('getId') + ->willReturn(10); + $lead->expects($this->once()) + ->method('getChanges') + ->willReturn(['notempty' => true]); + + $this->contactTracker->expects($this->once()) + ->method('getContact') + ->willReturn($lead); + + $event = $this->getMockBuilder(Event::class) + ->getMock(); + $event->method('getEventType') + ->willReturn(Event::TYPE_CONDITION); + + $event->expects($this->never()) + ->method('getPositiveChildren'); + + $this->eventRepository->expects($this->once()) + ->method('getContactPendingEvents') + ->willReturn([$event]); + + $responses = $this->getExecutioner()->execute('something'); + + $this->assertEquals(0, $responses->containsResponses()); + } + /** * @return RealTimeExecutioner */ From 02a5a49ddad51da0e71b0be4cf536dd6317d2a51 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 22 May 2018 19:32:03 -0500 Subject: [PATCH 506/778] Fixed SQL error for attempted to save channelId as an array --- .../Helper/ChannelExtractor.php | 39 +++++++++++++------ .../EventListener/CampaignSubscriber.php | 2 +- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/bundles/CampaignBundle/Helper/ChannelExtractor.php b/app/bundles/CampaignBundle/Helper/ChannelExtractor.php index b3555b45f0d..fa6572cfa38 100644 --- a/app/bundles/CampaignBundle/Helper/ChannelExtractor.php +++ b/app/bundles/CampaignBundle/Helper/ChannelExtractor.php @@ -38,18 +38,33 @@ public static function setChannel(ChannelInterface $entity, Event $event, Abstra return; } - $properties = $event->getProperties(); - if (!empty($properties['properties'][$channelIdField])) { - if (is_array($properties['properties'][$channelIdField])) { - if (count($properties['properties'][$channelIdField]) === 1) { - // Only store channel ID if a single item was selected - $entity->setChannelId($properties['properties'][$channelIdField]); - } - - return; - } - - $entity->setChannelId($properties['properties'][$channelIdField]); + $entity->setChannelId( + self::getChannelId($event->getProperties()['properties'], $channelIdField) + ); + } + + /** + * @param array $properties + * @param string $channelIdField + * + * @return null|int + */ + private static function getChannelId(array $properties, $channelIdField) + { + if (empty($properties[$channelIdField])) { + return null; } + + $channelId = $properties[$channelIdField]; + if (is_array($channelId) && (count($channelId) === 1)) { + // Only store channel ID if a single item was selected + $channelId = reset($channelId); + } + + if (!is_numeric($channelId)) { + return null; + } + + return (int) $channelId; } } diff --git a/plugins/MauticCitrixBundle/EventListener/CampaignSubscriber.php b/plugins/MauticCitrixBundle/EventListener/CampaignSubscriber.php index 5918ecafa6c..d0c17871212 100644 --- a/plugins/MauticCitrixBundle/EventListener/CampaignSubscriber.php +++ b/plugins/MauticCitrixBundle/EventListener/CampaignSubscriber.php @@ -113,7 +113,6 @@ public function onCitrixAction($product, CampaignExecutionEvent $event) $criteria = $config['event-criteria-'.$product]; /** @var array $list */ $list = $config[$product.'-list']; - $emailId = $config['template']; $actionId = 'citrix.action.'.$product; try { $productlist = CitrixHelper::getCitrixChoices($product); @@ -134,6 +133,7 @@ public function onCitrixAction($product, CampaignExecutionEvent $event) $this->registerProduct($product, $event->getLead(), $products); } else { if (in_array($criteria, ['assist_screensharing', 'training_start', 'meeting_start'], true)) { + $emailId = $config['template']; $this->startProduct($product, $event->getLead(), $products, $emailId, $actionId); } } From 4aafdf3ae4dc9ad3cab047e5516606dbe4ca6ef8 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 22 May 2018 21:34:50 -0500 Subject: [PATCH 507/778] Fixed logging the appropriate error for push to integration so that it doesn't show as "Generic error" in the timeline --- .../EventListener/CampaignSubscriber.php | 12 ++++++++---- .../EventListener/PushToIntegrationTrait.php | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/bundles/PluginBundle/EventListener/CampaignSubscriber.php b/app/bundles/PluginBundle/EventListener/CampaignSubscriber.php index 8d80ac915d9..70753b414ef 100644 --- a/app/bundles/PluginBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/PluginBundle/EventListener/CampaignSubscriber.php @@ -53,8 +53,6 @@ public function onCampaignBuild(CampaignBuilderEvent $event) /** * @param CampaignExecutionEvent $event - * - * @return $this */ public function onCampaignTriggerAction(CampaignExecutionEvent $event) { @@ -64,9 +62,15 @@ public function onCampaignTriggerAction(CampaignExecutionEvent $event) $success = $this->pushToIntegration($config, $lead, $errors); if (count($errors)) { - $event->setFailed(implode('
', $errors)); + $log = $event->getLogEntry(); + $log->appendToMetadata( + [ + 'failed' => 1, + 'reason' => implode('
', $errors), + ] + ); } - return $event->setResult($success); + $event->setResult($success); } } diff --git a/app/bundles/PluginBundle/EventListener/PushToIntegrationTrait.php b/app/bundles/PluginBundle/EventListener/PushToIntegrationTrait.php index 200193085c7..5e396ad1de1 100644 --- a/app/bundles/PluginBundle/EventListener/PushToIntegrationTrait.php +++ b/app/bundles/PluginBundle/EventListener/PushToIntegrationTrait.php @@ -76,8 +76,9 @@ protected static function pushIt($config, $lead, &$errors) $services = static::$integrationHelper->getIntegrationObjects($integration); $success = true; - /* - * @var AbstractIntegration + /** + * @var string + * @var AbstractIntegration $s */ foreach ($services as $name => $s) { $settings = $s->getIntegrationSettings(); From 69e574d0cd99c02edb5a64df5bcf176b4c85f492 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 22 May 2018 23:09:06 -0500 Subject: [PATCH 508/778] Ensure that scheduled conditions are marked as not scheduled after being processed and that both conditions and decisions have the date triggered timestamp updated --- .../CampaignBundle/Executioner/Event/ConditionExecutioner.php | 3 +++ .../CampaignBundle/Executioner/Event/DecisionExecutioner.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php index c94e4fcda73..b36aec26333 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/ConditionExecutioner.php @@ -62,6 +62,9 @@ public function execute(AbstractEventAccessor $config, ArrayCollection $logs) $evaluatedContacts->fail($log->getLead()); $log->setNonActionPathTaken(true); } + + // Unschedule the condition and update date triggered timestamp + $log->setDateTriggered(new \DateTime()); } return $evaluatedContacts; diff --git a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php index 959ab486737..9f901f67263 100644 --- a/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/Event/DecisionExecutioner.php @@ -102,6 +102,9 @@ public function execute(AbstractEventAccessor $config, ArrayCollection $logs) /* @var DecisionAccessor $config */ $this->dispatchEvent($config, $log); $evaluatedContacts->pass($log->getLead()); + + // Update the date triggered timestamp + $log->setDateTriggered(new \DateTime()); } catch (DecisionNotApplicableException $exception) { // Fail the contact but remove the log from being processed upstream // active/positive/green path while letting the InactiveExecutioner handle the inactive/negative/red paths From 77c4df403d252947ee2ee3c23ee11406309bc7cf Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Wed, 23 May 2018 18:00:05 -0500 Subject: [PATCH 509/778] Fixed exceptions when a campaign event is configured incorrectly --- .../Event/CampaignBuilderEvent.php | 8 +++---- .../KeyAlreadyRegisteredException.php | 23 +++++++++++++++++++ .../Event/ComponentValidationTrait.php | 8 ++++--- 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 app/bundles/CampaignBundle/Event/Exception/KeyAlreadyRegisteredException.php diff --git a/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php b/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php index a1979c48d2c..7d3fe5abb9b 100644 --- a/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php +++ b/app/bundles/CampaignBundle/Event/CampaignBuilderEvent.php @@ -11,9 +11,9 @@ namespace Mautic\CampaignBundle\Event; +use Mautic\CampaignBundle\Event\Exception\KeyAlreadyRegisteredException; use Mautic\CoreBundle\Event\ComponentValidationTrait; use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Translation\TranslatorInterface; /** @@ -83,7 +83,7 @@ public function __construct(TranslatorInterface $translator) public function addDecision($key, array $decision) { if (array_key_exists($key, $this->decisions)) { - throw new InvalidArgumentException("The key, '$key' is already used by another contact action. Please use a different key."); + throw new KeyAlreadyRegisteredException("The key, '$key' is already used by another contact action. Please use a different key."); } //check for required keys and that given functions are callable @@ -153,7 +153,7 @@ public function getLeadDecisions() public function addCondition($key, array $event) { if (array_key_exists($key, $this->conditions)) { - throw new InvalidArgumentException("The key, '$key' is already used by another contact action. Please use a different key."); + throw new KeyAlreadyRegisteredException("The key, '$key' is already used by another contact action. Please use a different key."); } //check for required keys and that given functions are callable @@ -224,7 +224,7 @@ public function getLeadConditions() public function addAction($key, array $action) { if (array_key_exists($key, $this->actions)) { - throw new InvalidArgumentException("The key, '$key' is already used by another action. Please use a different key."); + throw new KeyAlreadyRegisteredException("The key, '$key' is already used by another action. Please use a different key."); } //check for required keys and that given functions are callable diff --git a/app/bundles/CampaignBundle/Event/Exception/KeyAlreadyRegisteredException.php b/app/bundles/CampaignBundle/Event/Exception/KeyAlreadyRegisteredException.php new file mode 100644 index 00000000000..47a3fcfdeb7 --- /dev/null +++ b/app/bundles/CampaignBundle/Event/Exception/KeyAlreadyRegisteredException.php @@ -0,0 +1,23 @@ + Date: Wed, 23 May 2018 18:01:05 -0500 Subject: [PATCH 510/778] Fixed issue that stopped campaigns when the delete contact action is used --- .../ContactFinder/KickoffContactFinder.php | 1 - .../Executioner/EventExecutioner.php | 11 ++- .../Helper/RemovedContactTracker.php | 19 ++++ app/bundles/LeadBundle/Config/config.php | 7 ++ .../LeadBundle/Entity/CompanyRepository.php | 13 ++- .../LeadBundle/Entity/LeadRepository.php | 21 ++-- .../CampaignActionDeleteContactSubscriber.php | 97 +++++++++++++++++++ .../EventListener/CampaignSubscriber.php | 32 +----- app/bundles/LeadBundle/LeadEvents.php | 9 ++ 9 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 app/bundles/LeadBundle/EventListener/CampaignActionDeleteContactSubscriber.php diff --git a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php index d96aece2d93..93417bcb44b 100644 --- a/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php +++ b/app/bundles/CampaignBundle/Executioner/ContactFinder/KickoffContactFinder.php @@ -15,7 +15,6 @@ use Mautic\CampaignBundle\Entity\CampaignRepository; use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter; use Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException; -use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Entity\LeadRepository; use Psr\Log\LoggerInterface; diff --git a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php index 25dfc400443..e7b958398bf 100644 --- a/app/bundles/CampaignBundle/Executioner/EventExecutioner.php +++ b/app/bundles/CampaignBundle/Executioner/EventExecutioner.php @@ -183,8 +183,6 @@ public function executeLogs(Event $event, ArrayCollection $logs, Counter $counte $config = $this->collector->getEventConfig($event); - $this->checkForRemovedContacts($logs); - if ($counter) { // Must pass $counter around rather than setting it as a class property as this class is used // circularly to process children of parent events thus counter must be kept track separately @@ -285,6 +283,8 @@ private function persistLogs(ArrayCollection $logs) $this->responses->setFromLogs($logs); } + $this->checkForRemovedContacts($logs); + // Save updated log entries and clear from memory $this->eventLogger->persistCollection($logs) ->clear(); @@ -300,12 +300,17 @@ private function checkForRemovedContacts(ArrayCollection $logs) * @var LeadEventLog $log */ foreach ($logs as $key => $log) { - $contactId = $log->getLead()->getId(); + // Use the deleted ID if the contact was removed by the delete contact action + $contact = $log->getLead(); + $contactId = (!empty($contact->deletedId)) ? $contact->deletedId : $contact->getId(); $campaignId = $log->getCampaign()->getId(); if ($this->removedContactTracker->wasContactRemoved($campaignId, $contactId)) { $this->logger->debug("CAMPAIGN: Contact ID# $contactId has been removed from campaign ID $campaignId"); $logs->remove($key); + + // Clear out removed contacts to prevent a memory leak + $this->removedContactTracker->clearRemovedContact($campaignId, $contactId); } } } diff --git a/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php b/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php index 53d9158f4f7..3a73461d11a 100644 --- a/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php +++ b/app/bundles/CampaignBundle/Helper/RemovedContactTracker.php @@ -31,6 +31,25 @@ public function addRemovedContact($campaignId, $contactId) $this->removedContacts[$campaignId][$contactId] = $contactId; } + /** + * @param int $campaignId + * @param array $contacts + */ + public function addRemovedContacts($campaignId, array $contactIds) + { + foreach ($contactIds as $contactId) { + $this->addRemovedContact($campaignId, $contactId); + } + } + + /** + * @param int $campaignId + */ + public function clearRemovedContact($campaignId, $contactId) + { + unset($this->removedContacts[$campaignId][$contactId]); + } + /** * @param int $campaignId */ diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 1fe269edca3..8a64dd9f2b2 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -378,6 +378,13 @@ 'mautic.campaign.model.campaign', ], ], + 'mautic.lead.campaignbundle.action_delete_contacts.subscriber' => [ + 'class' => \Mautic\LeadBundle\EventListener\CampaignActionDeleteContactSubscriber::class, + 'arguments' => [ + 'mautic.lead.model.lead', + 'mautic.campaign.helper.removed_contact_tracker', + ], + ], 'mautic.lead.reportbundle.subscriber' => [ 'class' => \Mautic\LeadBundle\EventListener\ReportSubscriber::class, 'arguments' => [ diff --git a/app/bundles/LeadBundle/Entity/CompanyRepository.php b/app/bundles/LeadBundle/Entity/CompanyRepository.php index 7e488980c0b..35510e334a7 100644 --- a/app/bundles/LeadBundle/Entity/CompanyRepository.php +++ b/app/bundles/LeadBundle/Entity/CompanyRepository.php @@ -45,11 +45,18 @@ public function getEntity($id = 0) $entity = null; } - if ($entity != null) { - $fieldValues = $this->getFieldValues($id, true, 'company'); - $entity->setFields($fieldValues); + if (null === $entity) { + return $entity; } + if ($entity->getFields()) { + // Pulled from Doctrine memory so don't make unnecessary queries as this has already happened + return $entity; + } + + $fieldValues = $this->getFieldValues($id, true, 'company'); + $entity->setFields($fieldValues); + return $entity; } diff --git a/app/bundles/LeadBundle/Entity/LeadRepository.php b/app/bundles/LeadBundle/Entity/LeadRepository.php index fa77b58bce7..bb2dfd1a7a9 100755 --- a/app/bundles/LeadBundle/Entity/LeadRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadRepository.php @@ -367,17 +367,24 @@ public function getEntity($id = 0) $entity = null; } - if ($entity != null) { - if (!empty($this->triggerModel)) { - $entity->setColor($this->triggerModel->getColorForLeadPoints($entity->getPoints())); - } + if (null === $entity) { + return $entity; + } - $fieldValues = $this->getFieldValues($id); - $entity->setFields($fieldValues); + if ($entity->getFields()) { + // Pulled from Doctrine memory so don't make unnecessary queries as this has already happened + return $entity; + } - $entity->setAvailableSocialFields($this->availableSocialFields); + if (!empty($this->triggerModel)) { + $entity->setColor($this->triggerModel->getColorForLeadPoints($entity->getPoints())); } + $fieldValues = $this->getFieldValues($id); + $entity->setFields($fieldValues); + + $entity->setAvailableSocialFields($this->availableSocialFields); + return $entity; } diff --git a/app/bundles/LeadBundle/EventListener/CampaignActionDeleteContactSubscriber.php b/app/bundles/LeadBundle/EventListener/CampaignActionDeleteContactSubscriber.php new file mode 100644 index 00000000000..b7ac083ae56 --- /dev/null +++ b/app/bundles/LeadBundle/EventListener/CampaignActionDeleteContactSubscriber.php @@ -0,0 +1,97 @@ +leadModel = $leadModel; + $this->removedContactTracker = $removedContactTracker; + } + + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + CampaignEvents::CAMPAIGN_ON_BUILD => ['configureAction', 0], + LeadEvents::ON_CAMPAIGN_ACTION_DELETE_CONTACT => ['deleteContacts', 0], + ]; + } + + /** + * @param CampaignBuilderEvent $event + */ + public function configureAction(CampaignBuilderEvent $event) + { + $event->addAction( + 'lead.deletecontact', + [ + 'label' => 'mautic.lead.lead.events.delete', + 'description' => 'mautic.lead.lead.events.delete_descr', + // Kept for BC in case plugins are listening to the shared trigger + 'eventName' => LeadEvents::ON_CAMPAIGN_TRIGGER_ACTION, + 'batchEventName' => LeadEvents::ON_CAMPAIGN_ACTION_DELETE_CONTACT, + 'connectionRestrictions' => [ + 'target' => [ + 'decision' => ['none'], + 'action' => ['none'], + 'condition' => ['none'], + ], + ], + ] + ); + } + + /** + * @param PendingEvent $event + */ + public function deleteContacts(PendingEvent $event) + { + $contactIds = $event->getContactIds(); + + $this->removedContactTracker->addRemovedContacts( + $event->getEvent()->getCampaign()->getId(), + $contactIds + ); + + $this->leadModel->deleteEntities($contactIds); + + $event->passAll(); + } +} diff --git a/app/bundles/LeadBundle/EventListener/CampaignSubscriber.php b/app/bundles/LeadBundle/EventListener/CampaignSubscriber.php index 7307e8ad909..8557f34f925 100644 --- a/app/bundles/LeadBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/LeadBundle/EventListener/CampaignSubscriber.php @@ -87,7 +87,6 @@ public static function getSubscribedEvents() ['onCampaignTriggerActionUpdateTags', 3], ['onCampaignTriggerActionAddToCompany', 4], ['onCampaignTriggerActionChangeCompanyScore', 4], - ['onCampaignTriggerActionDeleteContact', 6], ['onCampaignTriggerActionChangeOwner', 7], ], LeadEvents::ON_CAMPAIGN_TRIGGER_CONDITION => ['onCampaignTriggerCondition', 0], @@ -159,20 +158,6 @@ public function onCampaignBuild(CampaignBuilderEvent $event) ]; $event->addAction('lead.scorecontactscompanies', $action); - $trigger = [ - 'label' => 'mautic.lead.lead.events.delete', - 'description' => 'mautic.lead.lead.events.delete_descr', - 'eventName' => LeadEvents::ON_CAMPAIGN_TRIGGER_ACTION, - 'connectionRestrictions' => [ - 'target' => [ - 'decision' => ['none'], - 'action' => ['none'], - 'condition' => ['none'], - ], - ], - ]; - $event->addAction('lead.deletecontact', $trigger); - $trigger = [ 'label' => 'mautic.lead.lead.events.field_value', 'description' => 'mautic.lead.lead.events.field_value_descr', @@ -308,6 +293,9 @@ public function onCampaignTriggerActionUpdateLead(CampaignExecutionEvent $event) return $event->setResult(true); } + /** + * @param CampaignExecutionEvent $event + */ public function onCampaignTriggerActionChangeOwner(CampaignExecutionEvent $event) { if (!$event->checkContext(self::ACTION_LEAD_CHANGE_OWNER)) { @@ -382,20 +370,6 @@ public function onCampaignTriggerActionChangeCompanyScore(CampaignExecutionEvent } } - /** - * @param CampaignExecutionEvent $event - */ - public function onCampaignTriggerActionDeleteContact(CampaignExecutionEvent $event) - { - if (!$event->checkContext('lead.deletecontact')) { - return; - } - - $this->leadModel->deleteEntity($event->getLead()); - - return $event->setResult(true); - } - /** * @param CampaignExecutionEvent $event */ diff --git a/app/bundles/LeadBundle/LeadEvents.php b/app/bundles/LeadBundle/LeadEvents.php index c69a0358949..6f6e4e5831f 100644 --- a/app/bundles/LeadBundle/LeadEvents.php +++ b/app/bundles/LeadBundle/LeadEvents.php @@ -458,6 +458,15 @@ final class LeadEvents */ const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.lead.on_campaign_trigger_action'; + /** + * The mautic.lead.on_campaign_action_delete_contact event is dispatched when the campaign action to delete a contact is executed. + * + * The event listener receives a Mautic\CampaignBundle\Event\PendingEvent + * + * @var string + */ + const ON_CAMPAIGN_ACTION_DELETE_CONTACT = 'mautic.lead.on_campaign_action_delete_contact'; + /** * The mautic.lead.on_campaign_trigger_condition event is fired when the campaign condition triggers. * From 6a7097ce4669b39c1706792c3e25c3ff45552b4b Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 24 May 2018 09:18:02 -0500 Subject: [PATCH 511/778] Fixed tests --- .../Tests/Executioner/RealTimeExecutionerTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php index bda32f4ecec..5686c54e124 100644 --- a/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php +++ b/app/bundles/CampaignBundle/Tests/Executioner/RealTimeExecutionerTest.php @@ -148,6 +148,8 @@ public function testChannelMisMatchResultsInEmptyResponses() $event->expects($this->exactly(3)) ->method('getChannel') ->willReturn('email'); + $event->method('getEventType') + ->willReturn(Event::TYPE_DECISION); $this->eventRepository->expects($this->once()) ->method('getContactPendingEvents') @@ -181,6 +183,8 @@ public function testChannelIdMisMatchResultsInEmptyResponses() $event->expects($this->exactly(3)) ->method('getChannelId') ->willReturn(3); + $event->method('getEventType') + ->willReturn(Event::TYPE_DECISION); $this->eventRepository->expects($this->once()) ->method('getContactPendingEvents') @@ -217,6 +221,8 @@ public function testEmptyPositiveactionsResultsInEmptyResponses() $event->expects($this->once()) ->method('getPositiveChildren') ->willReturn(new ArrayCollection()); + $event->method('getEventType') + ->willReturn(Event::TYPE_DECISION); $this->eventRepository->expects($this->once()) ->method('getContactPendingEvents') @@ -265,6 +271,8 @@ public function testAssociatedEventsAreExecuted() $event->expects($this->exactly(2)) ->method('getChannelId') ->willReturn(3); + $event->method('getEventType') + ->willReturn(Event::TYPE_DECISION); $event->expects($this->once()) ->method('getPositiveChildren') ->willReturn(new ArrayCollection([$action1, $action2])); From 456d7ea9afdb4e4e8b309cb45600abd7cef07de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Sun, 27 May 2018 14:06:12 +0200 Subject: [PATCH 512/778] Add translations --- .../Translations/en_US/messages.ini | 14 ++++++++++++++ .../EmailBundle/Translations/en_US/messages.ini | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/app/bundles/DashboardBundle/Translations/en_US/messages.ini b/app/bundles/DashboardBundle/Translations/en_US/messages.ini index e054c595dfd..9428066f59d 100644 --- a/app/bundles/DashboardBundle/Translations/en_US/messages.ini +++ b/app/bundles/DashboardBundle/Translations/en_US/messages.ini @@ -14,6 +14,20 @@ mautic.dashboard.menu.index="Dashboard" mautic.dashboard.update.past.tense="updated" mautic.note.no.upcoming.emails="No emails are scheduled to be sent." mautic.dashboard.label.created.leads="Created leads" +mautic.dashboard.label.url="URL" +mautic.dashboard.label.unique.hit.count="Unique hit count" +mautic.dashboard.label.total.hit.count="Total hit count" +mautic.dashboard.label.email.id="Email ID" +mautic.dashboard.label.email.name="Email name" +mautic.dashboard.label.contact.id="Contact ID" +mautic.dashboard.label.contact.email.address="Contact email address" +mautic.dashboard.label.contact.open="Contact open" +mautic.dashboard.label.contact.click="Clicks" +mautic.dashboard.label.contact.links.clicked="Links clicked" +mautic.dashboard.label.segment.id="Segment ID" +mautic.dashboard.label.segment.name="Segment name" +mautic.dashboard.label.company.id="Company ID" +mautic.dashboard.label.company.name="Company name" mautic.dashboard.widget.add="Add widget" mautic.dashboard.export.widgets="Export" mautic.dashboard.widget.import="Import" diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index ab93166edd8..7b1d80ecf93 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -56,6 +56,7 @@ mautic.email.complaint.reason.fraud="Mail provider indicated some kind of fraud mautic.email.complaint.reason.virus="Mail provider reports that a virus is found in the originating message" mautic.email.builder.addcontent="Click to add content" mautic.email.builder.index="Extras" +mautic.email.campaignId.filter="Campaign" mautic.email.campaign.event.open="Opens email" mautic.email.campaign.event.open_descr="Trigger actions when an email is opened. Connect a "Send Email" action to the top of this decision." mautic.email.campaign.event.click="Clicks email" @@ -69,6 +70,7 @@ mautic.email.campaign.event.validate_address="Has valid email address" mautic.email.campaign.event.validate_address_descr="Attempt to validate contact's email address. This may not be 100% accurate." mautic.form.action.send.email.to.owner="Send email to contact's owner" mautic.email.choose.emails_descr="Choose the email to be sent." +mautic.email.companyId.filter="Company" mautic.email.config.header.mail="Mail Send Settings" mautic.email.config.header.message="Message Settings" mautic.email.config.header.monitored_email="Monitored Inbox Settings" @@ -291,6 +293,7 @@ mautic.email.report.variant_read_count="A/B test read count" mautic.email.report.variant_sent_count="A/B test sent count" mautic.email.report.variant_start_date="A/B test start date" mautic.email.resubscribed.success="%email% has been re-subscribed. If this was by mistake, click here to unsubscribe." +mautic.email.segmentId.filter="Segment" mautic.email.send="Send" mautic.email.send.emailtype="Email type" mautic.email.send.emailtype.tooltip="Transactional emails can be sent to the same contact multiple times across campaigns. Marketing emails will be sent only once to the contact even if it was sent from another campaign." @@ -385,6 +388,8 @@ mautic.email.webhook.event.open="Email Open Event" mautic.email.webview.text="Having trouble reading this email? Click here." mautic.widget.created.emails="Created emails" mautic.widget.emails.in.time="Emails in time" +mautic.widget.most.hit.email.redirects="Most hit email redirects" +mautic.widget.sent.email.to.contacts="Sent email to contacts" mautic.widget.ignored.vs.read.emails="Ignored vs read" mautic.widget.most.read.emails="Most read emails" mautic.widget.most.sent.emails="Most sent emails" From f34993799073e3b68c9ada2c839b527838ed2c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Mon, 28 May 2018 13:31:39 +0200 Subject: [PATCH 513/778] Add campaign and segment filter --- app/bundles/CampaignBundle/Config/config.php | 9 +++ app/bundles/EmailBundle/Config/config.php | 2 + .../EventListener/DashboardSubscriber.php | 6 ++ .../Type/DashboardEmailsInTimeWidgetType.php | 59 +++++++++++++- app/bundles/EmailBundle/Model/EmailModel.php | 77 +++++++++++++++++-- .../Translations/en_US/messages.ini | 2 + app/bundles/LeadBundle/Config/config.php | 7 ++ 7 files changed, 153 insertions(+), 9 deletions(-) diff --git a/app/bundles/CampaignBundle/Config/config.php b/app/bundles/CampaignBundle/Config/config.php index 93c44bb6616..1ab8dba08de 100644 --- a/app/bundles/CampaignBundle/Config/config.php +++ b/app/bundles/CampaignBundle/Config/config.php @@ -238,6 +238,15 @@ ], ], ], + 'repositories' => [ + 'mautic.campaign.repository.campaign' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\CampaignBundle\Entity\Campaign::class, + ], + ], + ], ], 'parameters' => [ 'campaign_time_wait_on_event_false' => 'PT1H', diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 986b6234cdd..bf5f309fccb 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -316,7 +316,9 @@ 'class' => 'Mautic\EmailBundle\Form\Type\DashboardEmailsInTimeWidgetType', 'alias' => 'email_dashboard_emails_in_time_widget', 'arguments' => [ + 'mautic.campaign.repository.campaign', 'mautic.lead.repository.company', + 'mautic.lead.repository.lead_list', ], ], 'mautic.form.type.email_to_user' => [ diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index cd29cdfd211..c1d51358cde 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -89,6 +89,12 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['companyId'])) { $params['filter']['companyId'] = $params['companyId']; } + if (isset($params['campaignId'])) { + $params['filter']['campaignId'] = $params['campaignId']; + } + if (isset($params['segmentId'])) { + $params['filter']['segmentId'] = $params['segmentId']; + } if (!$event->isCached()) { $event->setTemplateData([ diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index 6c49a8148a9..38d1089c0bd 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -11,7 +11,11 @@ namespace Mautic\EmailBundle\Form\Type; +use Mautic\CampaignBundle\Entity\Campaign; +use Mautic\CampaignBundle\Entity\CampaignRepository; use Mautic\LeadBundle\Entity\CompanyRepository; +use Mautic\LeadBundle\Entity\LeadList; +use Mautic\LeadBundle\Entity\LeadListRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -20,19 +24,36 @@ */ class DashboardEmailsInTimeWidgetType extends AbstractType { + /** + * @var CampaignRepository + */ + private $campaignRepository; + /** * @var CompanyRepository */ private $companyRepository; + /** + * @var LeadListRepository + */ + private $segmentsRepository; + /** * DashboardEmailsInTimeWidgetType constructor. * - * @param CompanyRepository $companyRepository + * @param CampaignRepository $campaignRepository + * @param CompanyRepository $companyRepository + * @param LeadListRepository $leadListRepository */ - public function __construct(CompanyRepository $companyRepository) - { - $this->companyRepository = $companyRepository; + public function __construct( + CampaignRepository $campaignRepository, + CompanyRepository $companyRepository, + LeadListRepository $leadListRepository + ) { + $this->campaignRepository = $campaignRepository; + $this->companyRepository = $companyRepository; + $this->segmentsRepository = $leadListRepository; } /** @@ -69,6 +90,36 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'empty_data' => '', 'required' => false, ]); + /** @var Campaign[] $campaigns */ + $campaigns = $this->campaignRepository->findAll(); + $campaignsChoices = []; + foreach ($campaigns as $campaign) { + $campaignsChoices[$campaign->getId()] = $campaign->getName(); + } + $builder->add('campaignId', 'choice', [ + 'label' => 'mautic.email.campaignId.filter', + 'choices' => $campaignsChoices, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); + /** @var LeadList[] $segments */ + $segments = $this->segmentsRepository->findAll(); + $segmentsChoices = []; + foreach ($segments as $segment) { + $segmentsChoices[$segment->getId()] = $segment->getName(); + } + $builder->add('segmentId', 'choice', [ + 'label' => 'mautic.email.segmentId.filter', + 'choices' => $segmentsChoices, + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + ] + ); } /** diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 4486613e110..833ba138d83 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1713,8 +1713,10 @@ public function limitQueryToCreator(QueryBuilder &$q) */ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true) { - $flag = null; - $companyId = null; + $flag = null; + $companyId = null; + $campaignId = null; + $segmentId = null; if (isset($filter['flag'])) { $flag = $filter['flag']; @@ -1724,6 +1726,14 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $companyId = $filter['companyId']; unset($filter['companyId']); } + if (isset($filter['campaignId'])) { + $campaignId = $filter['campaignId']; + unset($filter['campaignId']); + } + if (isset($filter['segmentId'])) { + $segmentId = $filter['segmentId']; + unset($filter['segmentId']); + } $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); @@ -1738,6 +1748,17 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da ->andWhere('company_lead.company_id = :companyId') ->setParameter('companyId', $companyId); } + if ($campaignId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } + if ($segmentId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') + ->andWhere('t.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.sent.emails'), $data); } @@ -1752,6 +1773,17 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da ->andWhere('company_lead.company_id = :companyId') ->setParameter('companyId', $companyId); } + if ($campaignId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } + if ($segmentId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') + ->andWhere('t.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data); } @@ -1768,6 +1800,17 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da ->andWhere('company_lead.company_id = :companyId') ->setParameter('companyId', $companyId); } + if ($campaignId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } + if ($segmentId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') + ->andWhere('t.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.failed.emails'), $data); } @@ -1795,18 +1838,29 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da ->andWhere('company_lead.company_id = :companyId') ->setParameter('companyId', $companyId); } + if ($campaignId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } + if ($segmentId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') + ->andWhere('t.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.clicked'), $data); } if ($flag == 'all' || $flag == 'unsubscribed') { - $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers, $companyId); + $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers, $companyId, $campaignId, $segmentId); $chart->setDataset($this->translator->trans('mautic.email.unsubscribed'), $data); } if ($flag == 'all' || $flag == 'bounced') { - $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers, $companyId); + $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers, $companyId, $campaignId, $segmentId); $chart->setDataset($this->translator->trans('mautic.email.bounced'), $data); } @@ -1821,10 +1875,12 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da * @param $reason * @param $canViewOthers * @param int|null $companyId + * @param int|null $campaignId + * @param int|null $segmentId * * @return array */ - public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reason, $canViewOthers, $companyId = null) + public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reason, $canViewOthers, $companyId = null, $campaignId = null, $segmentId = null) { $dncFilter = isset($filter['email_id']) ? ['channel_id' => $filter['email_id']] : []; $q = $query->prepareTimeDataQuery('lead_donotcontact', 'date_added', $dncFilter); @@ -1841,6 +1897,17 @@ public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reaso ->andWhere('company_lead.company_id = :companyId') ->setParameter('companyId', $companyId); } + if ($campaignId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } + if ($segmentId !== null) { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') + ->andWhere('t.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } return $data = $query->loadAndBuildTimeData($q); } diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index d086b2da2c5..a7364949cc5 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -67,6 +67,7 @@ mautic.email.campaign.event.send.to.user="Send email to user" mautic.email.campaign.event.send.to.user_descr="Send email to user, owner or other email addresses" mautic.email.campaign.event.validate_address="Has valid email address" mautic.email.campaign.event.validate_address_descr="Attempt to validate contact's email address. This may not be 100% accurate." +mautic.email.campaignId.filter="Campaign filter" mautic.form.action.send.email.to.owner="Send email to contact's owner" mautic.email.choose.emails_descr="Choose the email to be sent." mautic.email.companyId.filter="Company filter" @@ -292,6 +293,7 @@ mautic.email.report.variant_read_count="A/B test read count" mautic.email.report.variant_sent_count="A/B test sent count" mautic.email.report.variant_start_date="A/B test start date" mautic.email.resubscribed.success="%email% has been re-subscribed. If this was by mistake, click here to unsubscribe." +mautic.email.segmentId.filter="Campaign filter" mautic.email.send="Send" mautic.email.send.emailtype="Email type" mautic.email.send.emailtype.tooltip="Transactional emails can be sent to the same contact multiple times across campaigns. Marketing emails will be sent only once to the contact even if it was sent from another campaign." diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index df26cd25e2c..3d965e40f9c 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -760,6 +760,13 @@ \Mautic\LeadBundle\Entity\LeadDevice::class, ], ], + 'mautic.lead.repository.lead_list' => [ + 'class' => Doctrine\ORM\EntityRepository::class, + 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], + 'arguments' => [ + \Mautic\LeadBundle\Entity\LeadList::class, + ], + ], 'mautic.lead.repository.merged_records' => [ 'class' => Doctrine\ORM\EntityRepository::class, 'factory' => ['@doctrine.orm.entity_manager', 'getRepository'], From 39c185179df8a921bf3b5f980374a7eb49b34777 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Mon, 28 May 2018 15:06:46 +0200 Subject: [PATCH 514/778] Fix Foreign QB for different filters on same table --- .../Query/Filter/ForeignValueFilterQueryBuilder.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 96b3ce5eddc..0bd76269a08 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -72,7 +72,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil case 'neq': case 'notLike': $tableAlias = $this->generateRandomParameterName(); - $queryBuilder = $queryBuilder->leftJoin( + $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, @@ -82,12 +82,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->addLogic($queryBuilder->expr()->isNull($tableAlias.'.lead_id'), 'and'); break; default: - $tableAlias = $queryBuilder->getTableAlias($filter->getTable(), 'left'); - - if (!$tableAlias) { - $tableAlias = $this->generateRandomParameterName(); - } - $queryBuilder = $queryBuilder->leftJoin( + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin( $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'leads'), $filter->getTable(), $tableAlias, From 117f52204af1b523ae0ac6ccb0af52d2cc82ccb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Mon, 28 May 2018 15:26:59 +0200 Subject: [PATCH 515/778] Fix test dependencies --- app/bundles/EmailBundle/Model/EmailModel.php | 9 ----- .../Tests/Model/EmailModelTest.php | 36 +++++++++++++------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 34b343bc326..d834a356a61 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -37,7 +37,6 @@ use Mautic\EmailBundle\Exception\FailedToSendToContactException; use Mautic\EmailBundle\Helper\MailHelper; use Mautic\EmailBundle\MonitoredEmail\Mailbox; -use Mautic\LeadBundle\Entity\CompanyRepository; use Mautic\LeadBundle\Entity\DoNotContact; use Mautic\LeadBundle\Entity\Lead; use Mautic\LeadBundle\Model\CompanyModel; @@ -126,11 +125,6 @@ class EmailModel extends FormModel implements AjaxLookupModelInterface */ private $deviceTracker; - /** - * @var CompanyRepository - */ - private $companyRepository; - /** * @var RedirectRepository */ @@ -150,7 +144,6 @@ class EmailModel extends FormModel implements AjaxLookupModelInterface * @param MessageQueueModel $messageQueueModel * @param SendEmailToContact $sendModel * @param DeviceTracker $deviceTracker - * @param CompanyRepository $companyRepository * @param RedirectRepository $redirectRepository */ public function __construct( @@ -165,7 +158,6 @@ public function __construct( MessageQueueModel $messageQueueModel, SendEmailToContact $sendModel, DeviceTracker $deviceTracker, - CompanyRepository $companyRepository, RedirectRepository $redirectRepository ) { $this->ipLookupHelper = $ipLookupHelper; @@ -179,7 +171,6 @@ public function __construct( $this->messageQueueModel = $messageQueueModel; $this->sendModel = $sendModel; $this->deviceTracker = $deviceTracker; - $this->companyRepository = $companyRepository; $this->redirectRepository = $redirectRepository; } diff --git a/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php b/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php index 019b7d85649..0df8994c76f 100644 --- a/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php +++ b/app/bundles/EmailBundle/Tests/Model/EmailModelTest.php @@ -35,6 +35,7 @@ use Mautic\LeadBundle\Model\DoNotContact; use Mautic\LeadBundle\Model\LeadModel; use Mautic\LeadBundle\Tracker\DeviceTracker; +use Mautic\PageBundle\Entity\RedirectRepository; use Mautic\PageBundle\Model\TrackableModel; use Mautic\UserBundle\Model\UserModel; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -228,7 +229,8 @@ public function testVariantEmailWeightsAreAppropriateForMultipleContacts() $sendToContactModel = new SendEmailToContact($mailHelper, $statHelper, $dncModel, $translator); - $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $redirectRepositoryMock = $this->createMock(RedirectRepository::class); $emailModel = new \Mautic\EmailBundle\Model\EmailModel( $ipLookupHelper, @@ -241,7 +243,8 @@ public function testVariantEmailWeightsAreAppropriateForMultipleContacts() $userModel, $messageModel, $sendToContactModel, - $deviceTrackerMock + $deviceTrackerMock, + $redirectRepositoryMock ); $emailModel->setTranslator($translator); @@ -468,7 +471,8 @@ public function testVariantEmailWeightsAreAppropriateForMultipleContactsSentOneA $sendToContactModel = new SendEmailToContact($mailHelper, $statHelper, $dncModel, $translator); - $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $redirectRepositoryMock = $this->createMock(RedirectRepository::class); $emailModel = new \Mautic\EmailBundle\Model\EmailModel( $ipLookupHelper, @@ -481,7 +485,8 @@ public function testVariantEmailWeightsAreAppropriateForMultipleContactsSentOneA $userModel, $messageModel, $sendToContactModel, - $deviceTrackerMock + $deviceTrackerMock, + $redirectRepositoryMock ); $emailModel->setTranslator($translator); @@ -631,7 +636,8 @@ public function testProcessMailerCallbackWithEmails() ->disableOriginalConstructor() ->getMock(); - $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $redirectRepositoryMock = $this->createMock(RedirectRepository::class); $emailModel = new \Mautic\EmailBundle\Model\EmailModel( $ipLookupHelper, @@ -644,7 +650,8 @@ public function testProcessMailerCallbackWithEmails() $userModel, $messageModel, $sendToContactModel, - $deviceTrackerMock + $deviceTrackerMock, + $redirectRepositoryMock ); $emailModel->setTranslator($translator); @@ -774,7 +781,8 @@ public function testProcessMailerCallbackWithHashIds() ->disableOriginalConstructor() ->getMock(); - $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $redirectRepositoryMock = $this->createMock(RedirectRepository::class); $emailModel = new \Mautic\EmailBundle\Model\EmailModel( $ipLookupHelper, @@ -787,7 +795,8 @@ public function testProcessMailerCallbackWithHashIds() $userModel, $messageModel, $sendToContactModel, - $deviceTrackerMock + $deviceTrackerMock, + $redirectRepositoryMock ); $emailModel->setTranslator($translator); @@ -897,6 +906,8 @@ public function testDoNotContactIsHonored() $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $redirectRepositoryMock = $this->createMock(RedirectRepository::class); + $emailModel = new \Mautic\EmailBundle\Model\EmailModel( $ipLookupHelper, $themeHelper, @@ -908,7 +919,8 @@ public function testDoNotContactIsHonored() $userModel, $messageModel, $sendToContactModel, - $deviceTrackerMock + $deviceTrackerMock, + $redirectRepositoryMock ); $emailModel->setTranslator($translator); @@ -1034,7 +1046,8 @@ public function testFrequencyRulesAreAppliedAndMessageGetsQueued() ->disableOriginalConstructor() ->getMock(); - $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $deviceTrackerMock = $this->createMock(DeviceTracker::class); + $redirectRepositoryMock = $this->createMock(RedirectRepository::class); $emailModel = new \Mautic\EmailBundle\Model\EmailModel( $ipLookupHelper, @@ -1047,7 +1060,8 @@ public function testFrequencyRulesAreAppliedAndMessageGetsQueued() $userModel, $messageModel, $sendToContactModel, - $deviceTrackerMock + $deviceTrackerMock, + $redirectRepositoryMock ); $emailModel->setTranslator($translator); From fbb5bf60bb7a19df0533d42f5a8d7993b2ad24fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Mon, 28 May 2018 15:31:10 +0200 Subject: [PATCH 516/778] Fix config dependency --- app/bundles/EmailBundle/Config/config.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 7a342e11a67..55410768bff 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -677,7 +677,6 @@ 'mautic.channel.model.queue', 'mautic.email.model.send_email_to_contacts', 'mautic.tracker.device', - 'mautic.lead.repository.company', 'mautic.page.repository.redirect', ], ], From b142806c7add164f102876d400b984d1ddf786c2 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Wed, 9 May 2018 13:55:09 +0200 Subject: [PATCH 517/778] Fixed condition that worked only for one context and not for the other --- .../EventListener/ReportSubscriber.php | 6 +- .../EventListener/ReportSubscriberTest.php | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/bundles/EmailBundle/Tests/EventListener/ReportSubscriberTest.php diff --git a/app/bundles/EmailBundle/EventListener/ReportSubscriber.php b/app/bundles/EmailBundle/EventListener/ReportSubscriber.php index 00ba176f06b..795fe883928 100644 --- a/app/bundles/EmailBundle/EventListener/ReportSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/ReportSubscriber.php @@ -406,7 +406,11 @@ public function onReportGraphGenerate(ReportGraphEvent $event) { $graphs = $event->getRequestedGraphs(); - if (!$event->checkContext(self::CONTEXT_EMAIL_STATS) || ($event->checkContext(self::CONTEXT_EMAILS) && !in_array('mautic.email.graph.pie.read.ingored.unsubscribed.bounced', $graphs))) { + if (!$event->checkContext([self::CONTEXT_EMAIL_STATS, self::CONTEXT_EMAILS])) { + return; + } + + if ($event->checkContext(self::CONTEXT_EMAILS) && !in_array('mautic.email.graph.pie.read.ingored.unsubscribed.bounced', $graphs)) { return; } diff --git a/app/bundles/EmailBundle/Tests/EventListener/ReportSubscriberTest.php b/app/bundles/EmailBundle/Tests/EventListener/ReportSubscriberTest.php new file mode 100644 index 00000000000..21b09678dc5 --- /dev/null +++ b/app/bundles/EmailBundle/Tests/EventListener/ReportSubscriberTest.php @@ -0,0 +1,85 @@ +connectionMock = $this->createMock(Connection::class); + $this->companyReportDataMock = $this->createMock(CompanyReportData::class); + $this->emMock = $this->createMock(EntityManager::class); + $this->subscriber = new ReportSubscriber($this->connectionMock, $this->companyReportDataMock); + $this->subscriber->setEntityManager($this->emMock); + } + + public function testOnReportGraphGenerateForEmailContextWithEmailGraph() + { + $eventMock = $this->createMock(ReportGraphEvent::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $chartQueryMock = $this->createMock(ChartQuery::class); + $statementMock = $this->createMock(Statement::class); + $translatorMock = $this->createMock(TranslatorInterface::class); + + $queryBuilderMock->method('execute')->willReturn($statementMock); + + $eventMock->expects($this->at(0)) + ->method('getRequestedGraphs') + ->willReturn(['mautic.email.graph.pie.read.ingored.unsubscribed.bounced']); + + $eventMock->expects($this->at(1)) + ->method('checkContext') + ->with(['email.stats', 'emails']) + ->willReturn(true); + + $eventMock->expects($this->at(2)) + ->method('checkContext') + ->with('emails') + ->willReturn(true); + + $eventMock->expects($this->at(3)) + ->method('getQueryBuilder') + ->willReturn($queryBuilderMock); + + $eventMock->expects($this->at(4)) + ->method('getOptions') + ->willReturn(['chartQuery' => $chartQueryMock, 'translator' => $translatorMock]); + + $queryBuilderMock->expects($this->once()) + ->method('select') + ->with('SUM(DISTINCT e.sent_count) as sent_count, SUM(DISTINCT e.read_count) as read_count, count(CASE WHEN dnc.id and dnc.reason = '.DoNotContact::UNSUBSCRIBED.' THEN 1 ELSE null END) as unsubscribed, count(CASE WHEN dnc.id and dnc.reason = '.DoNotContact::BOUNCED.' THEN 1 ELSE null END) as bounced'); + + $this->subscriber->onReportGraphGenerate($eventMock); + } +} From 00c3033e7bef0561a05c43510e4709b2199aea7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Tue, 29 May 2018 13:33:51 +0200 Subject: [PATCH 518/778] User conditioned companies and segments --- app/bundles/EmailBundle/Config/config.php | 1 + .../Type/DashboardEmailsInTimeWidgetType.php | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 5bac39cb1b4..788e60a9d3c 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -319,6 +319,7 @@ 'mautic.campaign.repository.campaign', 'mautic.lead.repository.company', 'mautic.lead.repository.lead_list', + 'mautic.helper.user', ], ], 'mautic.form.type.email_to_user' => [ diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index 38d1089c0bd..ecd22406f40 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -13,6 +13,7 @@ use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\CampaignRepository; +use Mautic\CoreBundle\Helper\UserHelper; use Mautic\LeadBundle\Entity\CompanyRepository; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListRepository; @@ -39,21 +40,29 @@ class DashboardEmailsInTimeWidgetType extends AbstractType */ private $segmentsRepository; + /** + * @var UserHelper + */ + private $userHelper; + /** * DashboardEmailsInTimeWidgetType constructor. * * @param CampaignRepository $campaignRepository * @param CompanyRepository $companyRepository * @param LeadListRepository $leadListRepository + * @param UserHelper $userHelper */ public function __construct( CampaignRepository $campaignRepository, CompanyRepository $companyRepository, - LeadListRepository $leadListRepository + LeadListRepository $leadListRepository, + UserHelper $userHelper ) { $this->campaignRepository = $campaignRepository; $this->companyRepository = $companyRepository; $this->segmentsRepository = $leadListRepository; + $this->userHelper = $userHelper; } /** @@ -77,7 +86,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ] ); - $companies = $this->companyRepository->getCompanies(); + $user = $this->userHelper->getUser(); + $companies = $this->companyRepository->getCompanies($user); $companiesChoises = []; foreach ($companies as $company) { $companiesChoises[$company['id']] = $company['companyname']; @@ -106,7 +116,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); /** @var LeadList[] $segments */ - $segments = $this->segmentsRepository->findAll(); + $segments = $this->segmentsRepository->getLists($user); $segmentsChoices = []; foreach ($segments as $segment) { $segmentsChoices[$segment->getId()] = $segment->getName(); From 9c3d11044142643cf4e14543be35a07188f41e56 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 29 May 2018 13:41:58 +0200 Subject: [PATCH 519/778] remove forgotten debug line --- app/bundles/LeadBundle/Model/ListModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 20c7e02751d..caf88ff02c9 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -820,7 +820,7 @@ public function getVersionNew(LeadList $entity) { $id = $entity->getId(); $list = ['id' => $id, 'filters' => $entity->getFilters()]; - $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); + $dtHelper = new DateTimeHelper(); $batchLimiters = [ 'dateTime' => $dtHelper->toUtcString(), @@ -838,7 +838,7 @@ public function getVersionOld(LeadList $entity) { $id = $entity->getId(); $list = ['id' => $id, 'filters' => $entity->getFilters()]; - $dtHelper = new DateTimeHelper('2017-10-01 00:00:00'); + $dtHelper = new DateTimeHelper(); $batchLimiters = [ 'dateTime' => $dtHelper->toUtcString(), From d0da325255d05c51278761aac664fa14c34e573e Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 29 May 2018 14:03:59 +0200 Subject: [PATCH 520/778] fix error causing excluding visitors if date limiter is set for batch --- app/bundles/LeadBundle/Segment/ContactSegmentService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index dc7c68c25ee..20df16bbb08 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -142,7 +142,10 @@ public function getNewLeadListLeads(LeadList $segment, array $batchLimiters, $li if (!empty($batchLimiters['dateTime'])) { // Only leads in the list at the time of count $queryBuilder->andWhere( - $queryBuilder->expr()->lte('l.date_added', $queryBuilder->expr()->literal($batchLimiters['dateTime'])) + $queryBuilder->expr()->orX( + $queryBuilder->expr()->lte('l.date_added', $queryBuilder->expr()->literal($batchLimiters['dateTime'])), + $queryBuilder->expr()->isNull('l.date_added') + ) ); } From 4fc367d0aaeaabf8f234e66b6984b362bbe404a0 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Tue, 29 May 2018 14:13:18 +0200 Subject: [PATCH 521/778] fix error causing excluding visitors if date limiter is set for batch, 2nd run --- .../LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index d8a322dfc57..480ad357e73 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -182,7 +182,10 @@ public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentI $expression = $queryBuilder->expr()->andX( $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId), - $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$batchRestrictions['dateTime']."'") + $queryBuilder->expr()->orX( + $queryBuilder->expr()->isNull($tableAlias.'.date_added'), + $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$batchRestrictions['dateTime']."'") + ) ); $queryBuilder->addJoinCondition($tableAlias, $expression); From 03049c019476832172b2e130d43aaf3f9dedb990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Tue, 29 May 2018 14:19:00 +0200 Subject: [PATCH 522/778] User conditioned companies and segments --- app/bundles/EmailBundle/Config/config.php | 2 ++ .../DashboardMostHitEmailRedirectsWidgetType.php | 16 +++++++++++++--- .../DashboardSentEmailToContactsWidgetType.php | 16 +++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 55410768bff..a3b6dc97465 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -323,6 +323,7 @@ 'mautic.campaign.repository.campaign', 'mautic.lead.repository.company', 'mautic.lead.repository.lead_list', + 'mautic.helper.user', ], ], 'mautic.form.type.email_dashboard_most_hit_email_redirects_widget' => [ @@ -332,6 +333,7 @@ 'mautic.campaign.repository.campaign', 'mautic.lead.repository.company', 'mautic.lead.repository.lead_list', + 'mautic.helper.user', ], ], 'mautic.form.type.email_to_user' => [ diff --git a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php index a0adee0d943..0f6363192d6 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php @@ -4,6 +4,7 @@ use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\CampaignRepository; +use Mautic\CoreBundle\Helper\UserHelper; use Mautic\LeadBundle\Entity\CompanyRepository; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListRepository; @@ -30,21 +31,29 @@ class DashboardMostHitEmailRedirectsWidgetType extends AbstractType */ private $segmentsRepository; + /** + * @var UserHelper + */ + private $userHelper; + /** * DashboardMostHitEmailRedirectsWidgetType constructor. * * @param CampaignRepository $campaignRepository * @param CompanyRepository $companyRepository * @param LeadListRepository $leadListRepository + * @param UserHelper $userHelper */ public function __construct( CampaignRepository $campaignRepository, CompanyRepository $companyRepository, - LeadListRepository $leadListRepository + LeadListRepository $leadListRepository, + UserHelper $userHelper ) { $this->campaignRepository = $campaignRepository; $this->companyRepository = $companyRepository; $this->segmentsRepository = $leadListRepository; + $this->userHelper = $userHelper; } /** @@ -53,7 +62,8 @@ public function __construct( */ public function buildForm(FormBuilderInterface $builder, array $options) { - $companies = $this->companyRepository->getCompanies(); + $user = $this->userHelper->getUser(); + $companies = $this->companyRepository->getCompanies($user); $companiesChoises = []; foreach ($companies as $company) { $companiesChoises[$company['id']] = $company['companyname']; @@ -83,7 +93,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); /** @var LeadList[] $segments */ - $segments = $this->segmentsRepository->findAll(); + $segments = $this->segmentsRepository->getLists($user); $segmentsChoices = []; foreach ($segments as $segment) { $segmentsChoices[$segment->getId()] = $segment->getName(); diff --git a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php index 5d73f619bab..204ccb011aa 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php @@ -4,6 +4,7 @@ use Mautic\CampaignBundle\Entity\Campaign; use Mautic\CampaignBundle\Entity\CampaignRepository; +use Mautic\CoreBundle\Helper\UserHelper; use Mautic\LeadBundle\Entity\CompanyRepository; use Mautic\LeadBundle\Entity\LeadList; use Mautic\LeadBundle\Entity\LeadListRepository; @@ -30,21 +31,29 @@ class DashboardSentEmailToContactsWidgetType extends AbstractType */ private $segmentsRepository; + /** + * @var UserHelper + */ + private $userHelper; + /** * DashboardSentEmailToContactsWidgetType constructor. * * @param CampaignRepository $campaignRepository * @param CompanyRepository $companyRepository * @param LeadListRepository $leadListRepository + * @param UserHelper $userHelper */ public function __construct( CampaignRepository $campaignRepository, CompanyRepository $companyRepository, - LeadListRepository $leadListRepository + LeadListRepository $leadListRepository, + UserHelper $userHelper ) { $this->campaignRepository = $campaignRepository; $this->companyRepository = $companyRepository; $this->segmentsRepository = $leadListRepository; + $this->userHelper = $userHelper; } /** @@ -53,7 +62,8 @@ public function __construct( */ public function buildForm(FormBuilderInterface $builder, array $options) { - $companies = $this->companyRepository->getCompanies(); + $user = $this->userHelper->getUser(); + $companies = $this->companyRepository->getCompanies($user); $companiesChoises = []; foreach ($companies as $company) { $companiesChoises[$company['id']] = $company['companyname']; @@ -83,7 +93,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); /** @var LeadList[] $segments */ - $segments = $this->segmentsRepository->findAll(); + $segments = $this->segmentsRepository->getLists($user); $segmentsChoices = []; foreach ($segments as $segment) { $segmentsChoices[$segment->getId()] = $segment->getName(); From 3fdca58c4c15524bb2f37bb097050a1ee4c3611d Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Tue, 29 May 2018 11:49:56 -0500 Subject: [PATCH 523/778] Fixed tests --- .../DeviceTrackingServiceTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php b/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php index 8fbd7dbb161..fb606f36b96 100644 --- a/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php +++ b/app/bundles/LeadBundle/Tests/Tracker/Service/DeviceTrackingService/DeviceTrackingServiceTest.php @@ -85,6 +85,10 @@ public function testIsTrackedTrue() ->willReturn($trackingId); $leadDeviceMock = $this->createMock(LeadDevice::class); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $this->leadDeviceRepositoryMock->expects($this->at(0)) ->method('getByTrackingId') ->with($trackingId) @@ -111,6 +115,10 @@ public function testIsTrackedFalse() ->with('mautic_device_id', null) ->willReturn($trackingId); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $this->leadDeviceRepositoryMock->expects($this->at(0)) ->method('getByTrackingId') ->with($trackingId) @@ -137,6 +145,10 @@ public function testGetTrackedDeviceCookie() ->with('mautic_device_id', null) ->willReturn($trackingId); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $leadDeviceMock = $this->createMock(LeadDevice::class); $this->leadDeviceRepositoryMock->expects($this->at(0)) ->method('getByTrackingId') @@ -168,6 +180,10 @@ public function testGetTrackedDeviceGetFromRequest() ->with('mautic_device_id', null) ->willReturn($trackingId); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $leadDeviceMock = $this->createMock(LeadDevice::class); $this->leadDeviceRepositoryMock->expects($this->at(0)) ->method('getByTrackingId') @@ -196,6 +212,10 @@ public function testGetTrackedDeviceNoTrackingId() ->with('mautic_device_id', null) ->willReturn(null); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $this->leadDeviceRepositoryMock->expects($this->never()) ->method('getByTrackingId'); @@ -237,6 +257,10 @@ public function testTrackCurrentDeviceAlreadyTracked() ->with('mautic_device_id', null) ->willReturn($trackingId); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $this->leadDeviceRepositoryMock->expects($this->at(0)) ->method('getByTrackingId') ->with($trackingId) @@ -271,6 +295,10 @@ public function testTrackCurrentDeviceAlreadyTrackedReplaceExistingTracking() ->with('mautic_device_id', null) ->willReturn($trackingId); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + $this->leadDeviceRepositoryMock->expects($this->at(0)) ->method('getByTrackingId') ->with($trackingId) @@ -345,6 +373,10 @@ public function testTrackCurrentDeviceNotTrackedYet() ->with(23) ->willReturn($uniqueTrackingIdentifier); + $this->security->expects($this->once()) + ->method('isAnonymous') + ->willReturn(true); + // index 0-3 for leadDeviceRepository::findOneBy $leadDeviceMock->expects($this->at(4)) ->method('getTrackingId') @@ -377,6 +409,11 @@ public function testUserIsNotTracked() $this->leadDeviceRepositoryMock->expects($this->never()) ->method('getByTrackingId'); + $requestMock = $this->createMock(Request::class); + $this->requestStackMock->expects($this->at(0)) + ->method('getCurrentRequest') + ->willReturn($requestMock); + $this->security->expects($this->once()) ->method('isAnonymous') ->willReturn(false); From f5fd2cf553eec037f7b6211c193f8675d2331b40 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 30 May 2018 15:01:00 +0200 Subject: [PATCH 524/778] Fix for multiselect custom fields --- .../Segment/ContactSegmentFilterCrate.php | 20 ++++++- .../Segment/ContactSegmentFilterOperator.php | 4 ++ .../Segment/Decorator/BaseDecorator.php | 9 +++ .../Query/Expression/ExpressionBuilder.php | 8 +++ .../Query/Filter/BaseFilterQueryBuilder.php | 9 +++ .../Segment/ContactSegmentFilterCrateTest.php | 56 +++++++++++++++++++ .../Segment/Decorator/BaseDecoratorTest.php | 29 ++++++++++ 7 files changed, 134 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 69c3b5c2b26..1dcb7d8f41d 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -62,9 +62,10 @@ public function __construct(array $filter) $this->field = isset($filter['field']) ? $filter['field'] : null; $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; - $this->operator = isset($filter['operator']) ? $filter['operator'] : null; $this->filter = isset($filter['filter']) ? $filter['filter'] : null; $this->sourceArray = $filter; + + $this->setOperator($filter); } /** @@ -179,4 +180,21 @@ public function getArray() { return $this->sourceArray; } + + /** + * @param array $filter + */ + private function setOperator(array $filter) + { + $operator = isset($filter['operator']) ? $filter['operator'] : null; + + if ($this->getType() === 'multiselect') { + $neg = strpos($operator, '!') === false ? '' : '!'; + $this->operator = $neg.$this->getType(); + + return; + } + + $this->operator = $operator; + } } diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php index 3115e30c297..04b1e545fe5 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterOperator.php @@ -67,6 +67,10 @@ public function fixOperator($operator) $this->dispatcher->dispatch(LeadEvents::LIST_FILTERS_OPERATORS_ON_GENERATE, $event); $options = $event->getOperators(); + if (empty($options[$operator])) { + return $operator; + } + $operatorDetails = $options[$operator]; return $operatorDetails['expr']; diff --git a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php index 82bac336833..3df5f4fbdff 100644 --- a/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php +++ b/app/bundles/LeadBundle/Segment/Decorator/BaseDecorator.php @@ -140,6 +140,15 @@ public function getParameterValue(ContactSegmentFilterCrate $contactSegmentFilte case 'regexp': case '!regexp': return $this->prepareRegex($filter); + case 'multiselect': + case '!multiselect': + $filter = (array) $filter; + + foreach ($filter as $key => $value) { + $filter[$key] = sprintf('(([|]|^)%s([|]|$))', $value); + } + + return $filter; } return $filter; diff --git a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php index 8013c79d054..6685ffafe15 100644 --- a/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Expression/ExpressionBuilder.php @@ -75,6 +75,10 @@ public function __construct(Connection $connection) */ public function andX($x = null) { + if (is_array($x)) { + return new CompositeExpression(CompositeExpression::TYPE_AND, $x); + } + return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); } @@ -94,6 +98,10 @@ public function andX($x = null) */ public function orX($x = null) { + if (is_array($x)) { + return new CompositeExpression(CompositeExpression::TYPE_OR, $x); + } + return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args()); } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index d6d3cc16e6d..5072dbd2052 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -146,7 +146,16 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryBuilder->expr()->$filterOperator($tableAlias.'.'.$filter->getField(), $filterParametersHolder), $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()) ); + break; + case 'multiselect': + case '!multiselect': + $operator = $filterOperator === 'multiselect' ? 'regexp' : 'notRegexp'; + $expressions = []; + foreach ($filterParametersHolder as $parameter) { + $expressions[] = $queryBuilder->expr()->$operator($tableAlias.'.'.$filter->getField(), $parameter); + } + $expression = $queryBuilder->expr()->andX($expressions); break; default: throw new \Exception('Dunno how to handle operator "'.$filterOperator.'"'); diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php index ff6fcf8cf54..d184cde4056 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php @@ -143,4 +143,60 @@ public function testCompanyTypeFilter() $this->assertFalse($contactSegmentFilterCrate->isContactType()); $this->assertTrue($contactSegmentFilterCrate->isCompanyType()); } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testMultiselectFilter() + { + $filter = [ + 'glue' => 'and', + 'field' => 'multiselect_cf', + 'object' => 'lead', + 'type' => 'multiselect', + 'filter' => [2, 4], + 'display' => null, + 'operator' => 'in', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertSame('and', $contactSegmentFilterCrate->getGlue()); + $this->assertSame('multiselect_cf', $contactSegmentFilterCrate->getField()); + $this->assertTrue($contactSegmentFilterCrate->isContactType()); + $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); + $this->assertSame([2, 4], $contactSegmentFilterCrate->getFilter()); + $this->assertSame('multiselect', $contactSegmentFilterCrate->getOperator()); + $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); + $this->assertFalse($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testNotMultiselectFilter() + { + $filter = [ + 'glue' => 'and', + 'field' => 'multiselect_cf', + 'object' => 'lead', + 'type' => 'multiselect', + 'filter' => [2, 4], + 'display' => null, + 'operator' => '!in', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertSame('and', $contactSegmentFilterCrate->getGlue()); + $this->assertSame('multiselect_cf', $contactSegmentFilterCrate->getField()); + $this->assertTrue($contactSegmentFilterCrate->isContactType()); + $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); + $this->assertSame([2, 4], $contactSegmentFilterCrate->getFilter()); + $this->assertSame('!multiselect', $contactSegmentFilterCrate->getOperator()); + $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); + $this->assertFalse($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + } } diff --git a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php index f7e4a93d5b9..ae5da5ddd10 100644 --- a/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/Decorator/BaseDecoratorTest.php @@ -396,6 +396,35 @@ public function testGetParameterValueNotRegex() $this->assertSame('Test \s string', $baseDecorator->getParameterValue($contactSegmentFilterCrate)); } + /** + * @covers \Mautic\LeadBundle\Segment\Decorator\BaseDecorator::getParameterValue + */ + public function testGetParameterValueMultiselect() + { + $baseDecorator = $this->getDecorator(); + + $expected = [ + '(([|]|^)2([|]|$))', + '(([|]|^)4([|]|$))', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'multiselect', + 'filter' => [2, 4], + 'operator' => 'in', + ]); + + $this->assertSame($expected, $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate([ + 'type' => 'multiselect', + 'filter' => [2, 4], + 'operator' => '!in', + ]); + + $this->assertSame($expected, $baseDecorator->getParameterValue($contactSegmentFilterCrate)); + } + /** * @return BaseDecorator */ From 1d3873208bff7a68abbdcb77e00017545edc64e7 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Wed, 30 May 2018 18:16:35 +0200 Subject: [PATCH 525/778] Fix for integration campaigns --- app/bundles/LeadBundle/Config/config.php | 4 ++ .../Segment/ContactSegmentFilter.php | 6 ++ .../IntegrationCampaignParts.php | 57 +++++++++++++++ .../IntegrationCampaignFilterQueryBuilder.php | 70 +++++++++++++++++++ .../ContactSegmentFilterDictionary.php | 5 ++ .../IntegrationCampaignPartsTest.php | 56 +++++++++++++++ 6 files changed, 198 insertions(+) create mode 100644 app/bundles/LeadBundle/Segment/IntegrationCampaign/IntegrationCampaignParts.php create mode 100644 app/bundles/LeadBundle/Segment/Query/Filter/IntegrationCampaignFilterQueryBuilder.php create mode 100644 app/bundles/LeadBundle/Tests/Segment/IntegrationCampaign/IntegrationCampaignPartsTest.php diff --git a/app/bundles/LeadBundle/Config/config.php b/app/bundles/LeadBundle/Config/config.php index 7ac2c58a3a5..a84ad261f85 100644 --- a/app/bundles/LeadBundle/Config/config.php +++ b/app/bundles/LeadBundle/Config/config.php @@ -784,6 +784,10 @@ 'class' => \Mautic\LeadBundle\Segment\Query\Filter\DoNotContactFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], ], + 'mautic.lead.query.builder.special.integration' => [ + 'class' => \Mautic\LeadBundle\Segment\Query\Filter\IntegrationCampaignFilterQueryBuilder::class, + 'arguments' => ['mautic.lead.model.random_parameter_name'], + ], 'mautic.lead.query.builder.special.sessions' => [ 'class' => \Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder::class, 'arguments' => ['mautic.lead.model.random_parameter_name'], diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 7e11ab69c00..804b9dd85cc 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -12,6 +12,7 @@ use Mautic\LeadBundle\Segment\Decorator\FilterDecoratorInterface; use Mautic\LeadBundle\Segment\DoNotContact\DoNotContactParts; +use Mautic\LeadBundle\Segment\IntegrationCampaign\IntegrationCampaignParts; use Mautic\LeadBundle\Segment\Query\Filter\FilterQueryBuilderInterface; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; @@ -196,4 +197,9 @@ public function getDoNotContactParts() { return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); } + + public function getIntegrationCampaignParts() + { + return new IntegrationCampaignParts($this->getParameterValue()); + } } diff --git a/app/bundles/LeadBundle/Segment/IntegrationCampaign/IntegrationCampaignParts.php b/app/bundles/LeadBundle/Segment/IntegrationCampaign/IntegrationCampaignParts.php new file mode 100644 index 00000000000..281589b3c75 --- /dev/null +++ b/app/bundles/LeadBundle/Segment/IntegrationCampaign/IntegrationCampaignParts.php @@ -0,0 +1,57 @@ +integrationName = $integrationName; + $this->campaignId = $campaignId; + } + + /** + * @return string + */ + public function getIntegrationName() + { + return $this->integrationName; + } + + /** + * @return string + */ + public function getCampaignId() + { + return $this->campaignId; + } +} diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/IntegrationCampaignFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/IntegrationCampaignFilterQueryBuilder.php new file mode 100644 index 00000000000..c004a84385f --- /dev/null +++ b/app/bundles/LeadBundle/Segment/Query/Filter/IntegrationCampaignFilterQueryBuilder.php @@ -0,0 +1,70 @@ +getIntegrationCampaignParts(); + + $integrationNameParameter = $this->generateRandomParameterName(); + $campaignIdParameter = $this->generateRandomParameterName(); + + $tableAlias = $this->generateRandomParameterName(); + + $queryBuilder->leftJoin( + 'l', + MAUTIC_TABLE_PREFIX.'integration_entity', + $tableAlias, + $tableAlias.'.integration_entity = "CampaignMember" AND '. + $tableAlias.".internal_entity = 'lead' AND ". + $tableAlias.'.internal_entity_id = l.id' + ); + + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($tableAlias.'.integration', ":$integrationNameParameter"), + $queryBuilder->expr()->eq($tableAlias.'.integration_entity_id', ":$campaignIdParameter") + ); + + $queryBuilder->addJoinCondition($tableAlias, $expression); + + if ($filter->getOperator() === 'eq') { + $queryType = $filter->getParameterValue() ? 'isNotNull' : 'isNull'; + } else { + $queryType = $filter->getParameterValue() ? 'isNull' : 'isNotNull'; + } + + $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); + + $queryBuilder->setParameter($integrationNameParameter, $integrationCampaignParts->getIntegrationName()); + $queryBuilder->setParameter($campaignIdParameter, $integrationCampaignParts->getCampaignId()); + + return $queryBuilder; + } +} diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index 51ad12b0794..47db70038c3 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -15,6 +15,7 @@ use Mautic\LeadBundle\Segment\Query\Filter\DoNotContactFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\ForeignFuncFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\ForeignValueFilterQueryBuilder; +use Mautic\LeadBundle\Segment\Query\Filter\IntegrationCampaignFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\SegmentReferenceFilterQueryBuilder; use Mautic\LeadBundle\Segment\Query\Filter\SessionsFilterQueryBuilder; @@ -187,6 +188,10 @@ public function __construct() 'type' => SessionsFilterQueryBuilder::getServiceId(), ]; + $this->translations['integration_campaigns'] = [ + 'type' => IntegrationCampaignFilterQueryBuilder::getServiceId(), + ]; + $this->translations['utm_campaign'] = [ 'type' => ForeignValueFilterQueryBuilder::getServiceId(), 'foreign_table' => 'lead_utmtags', diff --git a/app/bundles/LeadBundle/Tests/Segment/IntegrationCampaign/IntegrationCampaignPartsTest.php b/app/bundles/LeadBundle/Tests/Segment/IntegrationCampaign/IntegrationCampaignPartsTest.php new file mode 100644 index 00000000000..ff1c69cf25e --- /dev/null +++ b/app/bundles/LeadBundle/Tests/Segment/IntegrationCampaign/IntegrationCampaignPartsTest.php @@ -0,0 +1,56 @@ +assertSame('Connectwise', $doNotContactParts->getIntegrationName()); + $this->assertSame('283', $doNotContactParts->getCampaignId()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\IntegrationCampaign\IntegrationCampaignParts::getIntegrationName() + * @covers \Mautic\LeadBundle\Segment\IntegrationCampaign\IntegrationCampaignParts::getCampaignId() + */ + public function testSalesforceExplicit() + { + $field = 'Salesforce::22'; + $doNotContactParts = new IntegrationCampaignParts($field); + + $this->assertSame('Salesforce', $doNotContactParts->getIntegrationName()); + $this->assertSame('22', $doNotContactParts->getCampaignId()); + } + + /** + * @covers \Mautic\LeadBundle\Segment\IntegrationCampaign\IntegrationCampaignParts::getIntegrationName() + * @covers \Mautic\LeadBundle\Segment\IntegrationCampaign\IntegrationCampaignParts::getCampaignId() + */ + public function testSalesforceDefault() + { + $field = '44'; + $doNotContactParts = new IntegrationCampaignParts($field); + + $this->assertSame('Salesforce', $doNotContactParts->getIntegrationName()); + $this->assertSame('44', $doNotContactParts->getCampaignId()); + } +} From 50e6dea7772ff8f2b45a950e3e4cef264379fbce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Wed, 30 May 2018 23:59:25 +0200 Subject: [PATCH 526/778] DRY and datasets --- .../Controller/Api/WidgetApiController.php | 2 + .../EventListener/DashboardSubscriber.php | 3 + app/bundles/EmailBundle/Model/EmailModel.php | 108 +++++++++--------- 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php index e2aea4f9dfe..025aca3eb8d 100644 --- a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php +++ b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php @@ -67,6 +67,7 @@ public function getDataAction($type) $to = InputHelper::clean($this->request->get('dateTo', null)); $dataFormat = InputHelper::clean($this->request->get('dataFormat', null)); $unit = InputHelper::clean($this->request->get('timeUnit', 'Y')); + $dataset = InputHelper::clean($this->request->get('dataset', [])); $response = ['success' => 0]; try { @@ -90,6 +91,7 @@ public function getDataAction($type) 'dateTo' => $toDate, 'limit' => (int) $this->request->get('limit', null), 'filter' => $this->request->get('filter', []), + 'dataset' => $dataset, ]; $cacheTimeout = (int) $this->request->get('cacheTimeout', null); diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index c1d51358cde..8391d7aa5db 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -86,6 +86,9 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['flag'])) { $params['filter']['flag'] = $params['flag']; } + if (isset($params['dataset'])) { + $params['filter']['dataset'] = $params['dataset']; + } if (isset($params['companyId'])) { $params['filter']['companyId'] = $params['companyId']; } diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 833ba138d83..ef5c7c14687 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1713,6 +1713,7 @@ public function limitQueryToCreator(QueryBuilder &$q) */ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true) { + $datasets = []; $flag = null; $companyId = null; $campaignId = null; @@ -1722,6 +1723,10 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $flag = $filter['flag']; unset($filter['flag']); } + if (isset($filter['dataset'])) { + $datasets = array_merge($datasets, $filter['dataset']); + unset($filter['dataset']); + } if (isset($filter['companyId'])) { $companyId = $filter['companyId']; unset($filter['companyId']); @@ -1738,51 +1743,37 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat); $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo); - if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'sent_and_opened' || !$flag) { + if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'sent_and_opened' || !$flag || in_array('sent', $datasets)) { $q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $filter); if (!$canViewOthers) { $this->limitQueryToCreator($q); } if ($companyId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') - ->andWhere('company_lead.company_id = :companyId') - ->setParameter('companyId', $companyId); + $this->addCompanyFilter($q, $companyId); } if ($campaignId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId); + $this->addCampaignFilter($q, $campaignId); } if ($segmentId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') - ->andWhere('t.list_id = :segmentId') - ->setParameter('segmentId', $segmentId); + $this->addSegmentFilter($q, $segmentId); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.sent.emails'), $data); } - if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'sent_and_opened' || $flag == 'opened') { + if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'sent_and_opened' || $flag == 'opened' || in_array('opened', $datasets)) { $q = $query->prepareTimeDataQuery('email_stats', 'date_read', $filter); if (!$canViewOthers) { $this->limitQueryToCreator($q); } if ($companyId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') - ->andWhere('company_lead.company_id = :companyId') - ->setParameter('companyId', $companyId); + $this->addCompanyFilter($q, $companyId); } if ($campaignId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId); + $this->addCampaignFilter($q, $campaignId); } if ($segmentId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') - ->andWhere('t.list_id = :segmentId') - ->setParameter('segmentId', $segmentId); + $this->addSegmentFilter($q, $segmentId); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data); @@ -1796,26 +1787,19 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $q->andWhere($q->expr()->eq('t.is_failed', ':true')) ->setParameter('true', true, 'boolean'); if ($companyId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') - ->andWhere('company_lead.company_id = :companyId') - ->setParameter('companyId', $companyId); + $this->addCompanyFilter($q, $companyId); } if ($campaignId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId); + $this->addCampaignFilter($q, $campaignId); } if ($segmentId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') - ->andWhere('t.list_id = :segmentId') - ->setParameter('segmentId', $segmentId); + $this->addSegmentFilter($q, $segmentId); } $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.failed.emails'), $data); } - if ($flag == 'all' || $flag == 'clicked') { + if ($flag == 'all' || $flag == 'clicked' || in_array('clicked', $datasets)) { $q = $query->prepareTimeDataQuery('page_hits', 'date_hit', []); $q->andWhere('t.source = :source'); $q->setParameter('source', 'email'); @@ -1834,15 +1818,10 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $this->limitQueryToCreator($q); } if ($companyId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') - ->andWhere('company_lead.company_id = :companyId') - ->setParameter('companyId', $companyId); + $this->addCompanyFilter($q, $companyId); } if ($campaignId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId); + $this->addCampaignFilter($q, $campaignId); } if ($segmentId !== null) { $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') @@ -1854,7 +1833,7 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $chart->setDataset($this->translator->trans('mautic.email.clicked'), $data); } - if ($flag == 'all' || $flag == 'unsubscribed') { + if ($flag == 'all' || $flag == 'unsubscribed' || in_array('unsubscribed', $datasets)) { $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers, $companyId, $campaignId, $segmentId); $chart->setDataset($this->translator->trans('mautic.email.unsubscribed'), $data); } @@ -1893,25 +1872,52 @@ public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reaso $this->limitQueryToCreator($q); } if ($companyId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') - ->andWhere('company_lead.company_id = :companyId') - ->setParameter('companyId', $companyId); + $this->addCompanyFilter($q, $companyId); } if ($campaignId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId); + $this->addCampaignFilter($q, $campaignId); } if ($segmentId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') - ->andWhere('t.list_id = :segmentId') - ->setParameter('segmentId', $segmentId); + $this->addSegmentFilter($q, $segmentId); } return $data = $query->loadAndBuildTimeData($q); } + /** + * @param QueryBuilder $q + * @param int $companyId + */ + private function addCompanyFilter(QueryBuilder $q, $companyId) + { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } + + /** + * @param QueryBuilder $q + * @param int $campaignId + */ + private function addCampaignFilter(QueryBuilder $q, $campaignId) + { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } + + /** + * @param QueryBuilder $q + * @param int $segmentId + */ + private function addSegmentFilter(QueryBuilder $q, $segmentId) + { + $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') + ->andWhere('t.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } + /** * Get pie chart data of ignored vs opened emails. * From 7a29f837831655171fe544cc1e15ba638ef8db82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 00:16:11 +0200 Subject: [PATCH 527/778] PHPDoc --- app/bundles/EmailBundle/Model/EmailModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index d834a356a61..72b58f25d16 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -603,7 +603,7 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime } /** - * @param $limit + * @param int $limit * @param \DateTime $dateFrom * @param \DateTime $dateTo * @param array $options From 95dde82a54eedff1389bbb346576482caef0f8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 00:16:34 +0200 Subject: [PATCH 528/778] Remove ID from select --- app/bundles/PageBundle/Entity/RedirectRepository.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/bundles/PageBundle/Entity/RedirectRepository.php b/app/bundles/PageBundle/Entity/RedirectRepository.php index 3eaa581a43f..97d9419590c 100644 --- a/app/bundles/PageBundle/Entity/RedirectRepository.php +++ b/app/bundles/PageBundle/Entity/RedirectRepository.php @@ -112,8 +112,7 @@ public function getMostHitEmailRedirects( $segmentId = null ) { $q = $this->_em->getConnection()->createQueryBuilder(); - $q->addSelect('pr.id') - ->addSelect('pr.url') + $q->addSelect('pr.url') ->addSelect('pr.hits') ->addSelect('pr.unique_hits') ->from(MAUTIC_TABLE_PREFIX.'page_redirects', 'pr') From 3a58ffb70518b2ea4a5918f15eee5a9aa2cb401d Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 31 May 2018 10:27:00 +0200 Subject: [PATCH 529/778] Fix for Do not Contact - do not reuse same JOIN for different condition --- .../Query/Filter/DoNotContactFilterQueryBuilder.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index fc5f4018411..8e833cf0fb6 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -33,12 +33,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil { $doNotContactParts = $filter->getDoNotContactParts(); - $tableAlias = $queryBuilder->getTableAlias(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'left'); - - if (!$tableAlias) { - $tableAlias = $this->generateRandomParameterName(); - $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, $tableAlias.'.lead_id = l.id'); - } + $tableAlias = $this->generateRandomParameterName(); + $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_donotcontact', $tableAlias, $tableAlias.'.lead_id = l.id'); $exprParameter = $this->generateRandomParameterName(); $channelParameter = $this->generateRandomParameterName(); From 80b97f18931ba9089df0bc5e5e1b9d5aaca6a7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 10:54:57 +0200 Subject: [PATCH 530/778] Campaign info --- app/bundles/EmailBundle/Model/EmailModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index f95bcfe7233..199a54f9d29 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -576,10 +576,10 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime if ($stat['link_url'] !== null) { $item['links_clicked'][] = $stat['link_url']; } - /*if (isset($stat['campaign_id'])) { + if (isset($stat['campaign_id'])) { $item['campaign_id'] = $stat['campaign_id']; $item['campaign_name'] = $stat['campaign_name']; - }*/ + } if (isset($stat['segment_id'])) { $item['segment_id'] = $stat['segment_id']; $item['segment_name'] = $stat['segment_name']; From 673a08ce66b59ae5cab79a244f098f41b947e360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 11:55:42 +0200 Subject: [PATCH 531/778] Fix segments choices --- .../Form/Type/DashboardMostHitEmailRedirectsWidgetType.php | 2 +- .../Form/Type/DashboardSentEmailToContactsWidgetType.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php index 0f6363192d6..51bb5cb021b 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php @@ -96,7 +96,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $segments = $this->segmentsRepository->getLists($user); $segmentsChoices = []; foreach ($segments as $segment) { - $segmentsChoices[$segment->getId()] = $segment->getName(); + $segmentsChoices[$segment['id']] = $segment['name']; } $builder->add('segmentId', 'choice', [ 'label' => 'mautic.email.segmentId.filter', diff --git a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php index 204ccb011aa..6978977c140 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php @@ -96,7 +96,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $segments = $this->segmentsRepository->getLists($user); $segmentsChoices = []; foreach ($segments as $segment) { - $segmentsChoices[$segment->getId()] = $segment->getName(); + $segmentsChoices[$segment['id']] = $segment['name']; } $builder->add('segmentId', 'choice', [ 'label' => 'mautic.email.segmentId.filter', From db1ef8c54ce91b90567653b56c1e9dd418e2f74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 11:56:32 +0200 Subject: [PATCH 532/778] Fix segments choices --- .../EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index ecd22406f40..326500b3e82 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -119,7 +119,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $segments = $this->segmentsRepository->getLists($user); $segmentsChoices = []; foreach ($segments as $segment) { - $segmentsChoices[$segment->getId()] = $segment->getName(); + $segmentsChoices[$segment['id']] = $segment['name']; } $builder->add('segmentId', 'choice', [ 'label' => 'mautic.email.segmentId.filter', From 5c2c1f6684b0bd09e925bef6816b79bd0aa35b06 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 31 May 2018 11:59:23 +0200 Subject: [PATCH 533/778] bugfix #375 - improve logic processing and describe the process. fix error in DNC filter, where queries got written into query builder using andWhere instead of addLogic method --- .../Filter/DoNotContactFilterQueryBuilder.php | 2 +- .../LeadBundle/Segment/Query/QueryBuilder.php | 65 +++++++++---------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index 8e833cf0fb6..2252f6b80cb 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -53,7 +53,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryType = $filter->getParameterValue() ? 'isNull' : 'isNotNull'; } - $queryBuilder->andWhere($queryBuilder->expr()->$queryType($tableAlias.'.id')); + $queryBuilder->addLogic($queryBuilder->expr()->$queryType($tableAlias.'.id'),'and'); $queryBuilder->setParameter($exprParameter, $doNotContactParts->getParameterType()); $queryBuilder->setParameter($channelParameter, $doNotContactParts->getChannel()); diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index be287104aec..5b2240b548f 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1646,7 +1646,7 @@ public function popLogicStack() * * @return $this */ - public function addLogicStack($expression) + private function addLogicStack($expression) { $this->logicStack[] = $expression; @@ -1655,55 +1655,48 @@ public function addLogicStack($expression) /** * This function assembles correct logic for segment processing, this is to replace andWhere and orWhere (virtualy - * as they need to be kept). + * as they need to be kept). You may not use andWhere in filters!!! * * @param $expression * @param $glue - * - * @return $this */ - public function addLogic($expression, $glue) - { - if ($this->hasLogicStack() && $glue == 'and') { - $this->addLogicStack($expression); - } elseif ($this->hasLogicStack()) { - if ($glue == 'or') { - $this->applyStackLogic(); + public function addLogic($expression, $glue) { + // little setup + $glue = strtolower($glue); + + // Different handling + if ($glue == 'or') { + // Is this the first condition in query builder? + if (!is_null($this->sqlParts['where'])) { + // Are the any queued conditions? + if ($this->hasLogicStack()) { + // We need to apply current stack to the query builder + $this->applyStackLogic(); + } + // We queue current expression to stack + $this->addLogicStack($expression); + } else { + $this->andWhere($expression); } - $this->addLogicStack($expression); - } elseif ($glue == 'or') { - $this->addLogicStack($expression); } else { - $this->andWhere($expression); + // Glue is AND + if ($this->hasLogicStack()) { + $this->addLogicStack($expression); + } else { + $this->andWhere($expression); + } } - - return $this; } /** - * Convert stored logic into regular where condition. + * Apply content of stack * * @return $this */ - public function applyStackLogic() - { + public function applyStackLogic() { if ($this->hasLogicStack()) { - $parts = $this->getQueryParts(); - - if (!is_null($parts['where']) && !is_null($parts['having'])) { - $where = $parts['where']; - - $whereConditionAlias = 'wh_'.substr(md5($where->__toString()), 0, 10); - $selectCondition = sprintf('(%s) AS %s', $where->__toString(), $whereConditionAlias); - - $this->orHaving($whereConditionAlias); - - $this->addSelect($selectCondition); - - $this->resetQueryPart('where'); - } else { - $this->orWhere(new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack())); - } + $stackGroupExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack()); + $this->orWhere($stackGroupExpression); } return $this; From a0889ad9ff9e551677e00b307060bf7425bc2150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 12:25:23 +0200 Subject: [PATCH 534/778] Fix translation --- app/bundles/EmailBundle/Translations/en_US/messages.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/EmailBundle/Translations/en_US/messages.ini b/app/bundles/EmailBundle/Translations/en_US/messages.ini index 68b56c2c723..887842649f6 100644 --- a/app/bundles/EmailBundle/Translations/en_US/messages.ini +++ b/app/bundles/EmailBundle/Translations/en_US/messages.ini @@ -300,7 +300,7 @@ mautic.email.report.variant_read_count="A/B test read count" mautic.email.report.variant_sent_count="A/B test sent count" mautic.email.report.variant_start_date="A/B test start date" mautic.email.resubscribed.success="%email% has been re-subscribed. If this was by mistake, click here to unsubscribe." -mautic.email.segmentId.filter="Campaign filter" +mautic.email.segmentId.filter="Segment filter" mautic.email.send="Send" mautic.email.send.emailtype="Email type" mautic.email.send.emailtype.tooltip="Transactional emails can be sent to the same contact multiple times across campaigns. Marketing emails will be sent only once to the contact even if it was sent from another campaign." From 81973e462c479cf0335832efb652aa499b814e35 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 31 May 2018 13:35:43 +0200 Subject: [PATCH 535/778] bugfix #381 - respect glue in DNCFilter, consider empty string a notSet value --- .../Query/Filter/BaseFilterQueryBuilder.php | 16 ++++++++++++++-- .../Filter/DoNotContactFilterQueryBuilder.php | 2 +- .../Filter/ForeignValueFilterQueryBuilder.php | 4 ++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index 5072dbd2052..e79ca6414e2 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -12,6 +12,7 @@ use Mautic\LeadBundle\Segment\ContactSegmentFilter; use Mautic\LeadBundle\Segment\Exception\InvalidUseException; +use Mautic\LeadBundle\Segment\Query\Expression\CompositeExpression; use Mautic\LeadBundle\Segment\Query\QueryBuilder; use Mautic\LeadBundle\Segment\Query\QueryException; use Mautic\LeadBundle\Segment\RandomParameterName; @@ -108,10 +109,21 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil switch ($filterOperator) { case 'empty': - $expression = $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()); + $expression = new CompositeExpression(CompositeExpression::TYPE_OR, + [ + $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')) + ] + ); break; case 'notEmpty': - $expression = $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()); + $expression = new CompositeExpression(CompositeExpression::TYPE_AND, + [ + $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()), + $queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')) + ] + ); + break; case 'neq': $expression = $queryBuilder->expr()->orX( diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index 2252f6b80cb..cc805a58c35 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -53,7 +53,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryType = $filter->getParameterValue() ? 'isNull' : 'isNotNull'; } - $queryBuilder->addLogic($queryBuilder->expr()->$queryType($tableAlias.'.id'),'and'); + $queryBuilder->addLogic($queryBuilder->expr()->$queryType($tableAlias.'.id'),$filter->getGlue()); $queryBuilder->setParameter($exprParameter, $doNotContactParts->getParameterType()); $queryBuilder->setParameter($channelParameter, $doNotContactParts->getChannel()); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php index 0bd76269a08..6eb14d79c91 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignValueFilterQueryBuilder.php @@ -79,7 +79,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $tableAlias.'.lead_id = l.id' ); - $queryBuilder->addLogic($queryBuilder->expr()->isNull($tableAlias.'.lead_id'), 'and'); + $queryBuilder->addLogic($queryBuilder->expr()->isNull($tableAlias.'.lead_id'), $filter->getGlue()); break; default: $tableAlias = $this->generateRandomParameterName(); @@ -90,7 +90,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $tableAlias.'.lead_id = l.id' ); - $queryBuilder->addLogic($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id'), 'and'); + $queryBuilder->addLogic($queryBuilder->expr()->isNotNull($tableAlias.'.lead_id'), $filter->getGlue()); } switch ($filterOperator) { From 4caa6d9c1f822260d472b135100039b138e3014e Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 31 May 2018 14:18:35 +0200 Subject: [PATCH 536/778] fix null value considered false for sum aggregations, implement default null_value handling to filter object --- .../LeadBundle/Segment/ContactSegmentFilter.php | 9 +++++++++ .../Segment/ContactSegmentFilterCrate.php | 14 ++++++++++++++ .../Filter/ForeignFuncFilterQueryBuilder.php | 17 +++++++++++++---- .../Services/ContactSegmentFilterDictionary.php | 1 + 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 804b9dd85cc..a926f6785fd 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -190,6 +190,14 @@ public function isColumnTypeBoolean() return $this->contactSegmentFilterCrate->isBooleanType(); } + /** + * @return mixed + */ + public function getNullValue() + { + return $this->contactSegmentFilterCrate->getNullValue(); + } + /** * @return DoNotContactParts */ @@ -198,6 +206,7 @@ public function getDoNotContactParts() return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); } + public function getIntegrationCampaignParts() { return new IntegrationCampaignParts($this->getParameterValue()); diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 1dcb7d8f41d..314db29a293 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -51,6 +51,11 @@ class ContactSegmentFilterCrate */ private $sourceArray; + /** + * @var + */ + private $nullValue; + /** * ContactSegmentFilterCrate constructor. * @@ -63,6 +68,7 @@ public function __construct(array $filter) $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; $this->filter = isset($filter['filter']) ? $filter['filter'] : null; + $this->nullValue = isset($filter['null_value']) ? $filter['null_value'] : null; $this->sourceArray = $filter; $this->setOperator($filter); @@ -197,4 +203,12 @@ private function setOperator(array $filter) $this->operator = $operator; } + + /** + * @return mixed + */ + public function getNullValue() + { + return $this->nullValue; + } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 06da7e65e54..23e7bb76d0c 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -107,10 +107,19 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil break; default: if ($filterAggr) { - $expression = $queryBuilder->expr()->$filterOperator( - sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), - $filterParametersHolder - ); + if (!is_null($filter)) { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(ifnull(%s,%s))', $filterAggr, $tableAlias.'.'.$filter->getField(), + ((intval($filter->getNullValue()) == $filter->getNullValue() && is_numeric($filter->getNullValue())) ? $filter->getNullValue() : "'" . $filter->getNullValue() ."'" )), + $filterParametersHolder + + ); + } else { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), + $filterParametersHolder + ); + } } else { $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index 47db70038c3..a62a2bed4af 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -33,6 +33,7 @@ public function __construct() 'table_field' => 'id', 'func' => 'sum', 'field' => 'open_count', + 'null_value' => 0 ]; $this->translations['lead_email_received'] = [ From 0f1aca9f5574a54f519c31e3ffc5e85bccd2e4a0 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 31 May 2018 14:18:35 +0200 Subject: [PATCH 537/778] bugfix #384 null value considered false for sum aggregations, implement default null_value handling to filter object --- .../LeadBundle/Segment/ContactSegmentFilter.php | 9 +++++++++ .../Segment/ContactSegmentFilterCrate.php | 14 ++++++++++++++ .../Filter/ForeignFuncFilterQueryBuilder.php | 17 +++++++++++++---- .../Services/ContactSegmentFilterDictionary.php | 1 + 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index 804b9dd85cc..a926f6785fd 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -190,6 +190,14 @@ public function isColumnTypeBoolean() return $this->contactSegmentFilterCrate->isBooleanType(); } + /** + * @return mixed + */ + public function getNullValue() + { + return $this->contactSegmentFilterCrate->getNullValue(); + } + /** * @return DoNotContactParts */ @@ -198,6 +206,7 @@ public function getDoNotContactParts() return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); } + public function getIntegrationCampaignParts() { return new IntegrationCampaignParts($this->getParameterValue()); diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 1dcb7d8f41d..314db29a293 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -51,6 +51,11 @@ class ContactSegmentFilterCrate */ private $sourceArray; + /** + * @var + */ + private $nullValue; + /** * ContactSegmentFilterCrate constructor. * @@ -63,6 +68,7 @@ public function __construct(array $filter) $this->object = isset($filter['object']) ? $filter['object'] : self::CONTACT_OBJECT; $this->type = isset($filter['type']) ? $filter['type'] : null; $this->filter = isset($filter['filter']) ? $filter['filter'] : null; + $this->nullValue = isset($filter['null_value']) ? $filter['null_value'] : null; $this->sourceArray = $filter; $this->setOperator($filter); @@ -197,4 +203,12 @@ private function setOperator(array $filter) $this->operator = $operator; } + + /** + * @return mixed + */ + public function getNullValue() + { + return $this->nullValue; + } } diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 06da7e65e54..23e7bb76d0c 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -107,10 +107,19 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil break; default: if ($filterAggr) { - $expression = $queryBuilder->expr()->$filterOperator( - sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), - $filterParametersHolder - ); + if (!is_null($filter)) { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(ifnull(%s,%s))', $filterAggr, $tableAlias.'.'.$filter->getField(), + ((intval($filter->getNullValue()) == $filter->getNullValue() && is_numeric($filter->getNullValue())) ? $filter->getNullValue() : "'" . $filter->getNullValue() ."'" )), + $filterParametersHolder + + ); + } else { + $expression = $queryBuilder->expr()->$filterOperator( + sprintf('%s(%s)', $filterAggr, $tableAlias.'.'.$filter->getField()), + $filterParametersHolder + ); + } } else { $expression = $queryBuilder->expr()->$filterOperator( $tableAlias.'.'.$filter->getField(), diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index 47db70038c3..a62a2bed4af 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -33,6 +33,7 @@ public function __construct() 'table_field' => 'id', 'func' => 'sum', 'field' => 'open_count', + 'null_value' => 0 ]; $this->translations['lead_email_received'] = [ From 1971466d5b5db03cca263bca2d9d9201569bc5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Thu, 31 May 2018 14:29:06 +0200 Subject: [PATCH 538/778] Add support for bounced and failed dataset --- app/bundles/EmailBundle/Model/EmailModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 29adfa42c1e..16fa284ad1b 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1785,7 +1785,7 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data); } - if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'failed') { + if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'failed' || in_array('failed', $datasets)) { $q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $filter); if (!$canViewOthers) { $this->limitQueryToCreator($q); @@ -1844,7 +1844,7 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da $chart->setDataset($this->translator->trans('mautic.email.unsubscribed'), $data); } - if ($flag == 'all' || $flag == 'bounced') { + if ($flag == 'all' || $flag == 'bounced' || in_array('bounced', $datasets)) { $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers, $companyId, $campaignId, $segmentId); $chart->setDataset($this->translator->trans('mautic.email.bounced'), $data); } From e24d3bea5b091c4da1e41aceed10b3a7cdfc8179 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 31 May 2018 15:19:04 +0200 Subject: [PATCH 539/778] add additional test to the segments test command --- .../Command/CheckQueryBuildersCommand.php | 17 ++++++++++++++++- app/bundles/LeadBundle/Model/ListModel.php | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 40ae17f660c..83e5cc2da81 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -153,6 +153,21 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $output->writeln(''); } - return !((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); + $failed = ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); + + if (!$failed) { + $result = $listModel->getSegmentTotal($l); + $expected = $result[$l->getId()]['count']; + + $lists = $listModel->getLeadsByList(['id'=>$l->getId()]); + $real = count($lists[$l->getId()]); + + if ($expected!=$real) { + echo "ERROR: database contains $real records but query proposes $expected results\n"; + $failed = true; + } + } + + return !$failed; } } diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index caf88ff02c9..972fe572eda 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -829,6 +829,25 @@ public function getVersionNew(LeadList $entity) return $this->leadSegmentService->getNewLeadListLeadsCount($entity, $batchLimiters); } + /** + * @deprecated this method will be removed very soon, do not use it + * + * @param LeadList $entity + * @return array + * @throws \Exception + */ + public function getSegmentTotal(LeadList $entity) { + $id = $entity->getId(); + $list = ['id' => $id, 'filters' => $entity->getFilters()]; + $dtHelper = new DateTimeHelper(); + + $batchLimiters = [ + 'dateTime' => $dtHelper->toUtcString(), + ]; + + return $this->leadSegmentService->getTotalLeadListLeadsCount($entity, $batchLimiters); + } + /** * @param LeadList $entity * From 3ea139d63011970423b58a195e5bbb9a8d103e61 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Thu, 31 May 2018 15:26:01 +0200 Subject: [PATCH 540/778] fix reporting error for segments with no filter --- app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 83e5cc2da81..63385c61bb4 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -162,7 +162,7 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $lists = $listModel->getLeadsByList(['id'=>$l->getId()]); $real = count($lists[$l->getId()]); - if ($expected!=$real) { + if ($expected!=$real and count($l->getFilters())) { echo "ERROR: database contains $real records but query proposes $expected results\n"; $failed = true; } From 556eea4ed7e478101cdb49c1bb902f6386cd8cff Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 31 May 2018 15:43:40 +0200 Subject: [PATCH 541/778] Show all the columns no matter what filter is selected --- .../Translations/en_US/messages.ini | 2 + .../EmailBundle/Entity/StatRepository.php | 43 +++++++++++-------- .../EventListener/DashboardSubscriber.php | 15 +++---- app/bundles/EmailBundle/Model/EmailModel.php | 24 +++++------ 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/app/bundles/DashboardBundle/Translations/en_US/messages.ini b/app/bundles/DashboardBundle/Translations/en_US/messages.ini index 9428066f59d..606210d5387 100644 --- a/app/bundles/DashboardBundle/Translations/en_US/messages.ini +++ b/app/bundles/DashboardBundle/Translations/en_US/messages.ini @@ -28,6 +28,8 @@ mautic.dashboard.label.segment.id="Segment ID" mautic.dashboard.label.segment.name="Segment name" mautic.dashboard.label.company.id="Company ID" mautic.dashboard.label.company.name="Company name" +mautic.dashboard.label.campaign.id="Company ID" +mautic.dashboard.label.campaign.name="Company name" mautic.dashboard.widget.add="Add widget" mautic.dashboard.export.widgets="Export" mautic.dashboard.widget.import="Import" diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index 72fce5695a4..ce85bd9bd27 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -76,36 +76,45 @@ public function getSentEmailToContactData( ->leftJoin('ph', MAUTIC_TABLE_PREFIX.'page_redirects', 'pr', 'ph.redirect_id = pr.redirect_id') ->addSelect('pr.url AS link_url') ->addSelect('pr.hits AS link_hits'); + if ($createdByUserId !== null) { $q->andWhere('e.created_by = :userId') ->setParameter('userId', $createdByUserId); } + $q->andWhere('s.date_sent BETWEEN :dateFrom AND :dateTo') ->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s')) ->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s')); + + $q->leftJoin('s', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 's.lead_id = cl.lead_id') + ->leftJoin('s', MAUTIC_TABLE_PREFIX.'companies', 'c', 'cl.company_id = c.id') + ->addSelect('c.id AS company_id') + ->addSelect('c.companyname AS company_name'); + if ($companyId !== null) { - $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 's.lead_id = cl.lead_id') - ->andWhere('cl.company_id = :companyId') - ->setParameter('companyId', $companyId) - ->innerJoin('s', MAUTIC_TABLE_PREFIX.'companies', 'c', 'cl.company_id = c.id') - ->addSelect('c.id AS company_id') - ->addSelect('c.companyname AS company_name'); + $q->andWhere('cl.company_id = :companyId') + ->setParameter('companyId', $companyId); } + + $q->leftJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source_id = ce.id AND s.source = "campaign.event"') + ->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->addSelect('campaign.id AS campaign_id') + ->addSelect('campaign.name AS campaign_name'); + if ($campaignId !== null) { - $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source_id = ce.id AND s.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId) - ->addSelect('campaign.id AS campaign_id') - ->addSelect('campaign.name AS campaign_name'); + $q->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); } + + $q->leftJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id') + ->addSelect('ll.id AS segment_id') + ->addSelect('ll.name AS segment_name'); + if ($segmentId !== null) { - $q->innerJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id') - ->andWhere('s.list_id = :segmentId') - ->setParameter('segmentId', $segmentId) - ->addSelect('ll.id AS segment_id') - ->addSelect('ll.name AS segment_name'); + $q->andWhere('s.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); } + $q->setMaxResults($limit); return $q->execute()->fetchAll(); diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index 0dbc2844d71..af5bcb1e433 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -143,15 +143,14 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) 'mautic.dashboard.label.email.name', 'mautic.dashboard.label.contact.click', 'mautic.dashboard.label.contact.links.clicked', + 'mautic.dashboard.label.segment.id', + 'mautic.dashboard.label.segment.name', + 'mautic.dashboard.label.company.id', + 'mautic.dashboard.label.company.name', + 'mautic.dashboard.label.campaign.id', + 'mautic.dashboard.label.campaign.name', ]; - if ($segmentId !== null) { - $headItems[] = 'mautic.dashboard.label.segment.id'; - $headItems[] = 'mautic.dashboard.label.segment.name'; - } - if ($companyId !== null) { - $headItems[] = 'mautic.dashboard.label.company.id'; - $headItems[] = 'mautic.dashboard.label.company.name'; - } + $event->setTemplateData( [ 'headItems' => $headItems, diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 199a54f9d29..88a7002e178 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -568,26 +568,22 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime 'contact_id' => $stat['lead_id'], 'contact_email' => $stat['email_address'], 'open' => $stat['is_read'], + 'click' => ($stat['link_hits'] !== null) ? $stat['link_hits'] : 0, + 'links_clicked' => [], 'email_id' => $stat['email_id'], 'email_name' => $stat['email_name'], + 'campaign_id' => $stat['campaign_id'], + 'campaign_name' => $stat['campaign_name'], + 'segment_id' => $stat['segment_id'], + 'segment_name' => $stat['segment_name'], + 'company_id' => $stat['company_id'], + 'company_name' => $stat['company_name'], ]; - $item['click'] = ($stat['link_hits'] !== null) ? $stat['link_hits'] : 0; - $item['links_clicked'] = []; + if ($stat['link_url'] !== null) { $item['links_clicked'][] = $stat['link_url']; } - if (isset($stat['campaign_id'])) { - $item['campaign_id'] = $stat['campaign_id']; - $item['campaign_name'] = $stat['campaign_name']; - } - if (isset($stat['segment_id'])) { - $item['segment_id'] = $stat['segment_id']; - $item['segment_name'] = $stat['segment_name']; - } - if (isset($stat['company_id'])) { - $item['company_id'] = $stat['company_id']; - $item['company_name'] = $stat['company_name']; - } + $data[$statId] = $item; } else { if ($stat['link_hits'] !== null) { From e7047d9fdda683b32bfbbfdeb2adb5012d5c453c Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 31 May 2018 16:49:38 +0200 Subject: [PATCH 542/778] Fix for old segment which has = operator instead of in stored id DB --- .../Segment/ContactSegmentFilterCrate.php | 18 +++++++----- .../Segment/ContactSegmentFilterCrateTest.php | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php index 314db29a293..0594d7b5ca4 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilterCrate.php @@ -95,7 +95,7 @@ public function getField() */ public function isContactType() { - return $this->object === self::CONTACT_OBJECT; + return self::CONTACT_OBJECT === $this->object; } /** @@ -103,7 +103,7 @@ public function isContactType() */ public function isCompanyType() { - return $this->object === self::COMPANY_OBJECT; + return self::COMPANY_OBJECT === $this->object; } /** @@ -134,7 +134,7 @@ public function getOperator() */ public function isBooleanType() { - return $this->getType() === 'boolean'; + return 'boolean' === $this->getType(); } /** @@ -142,7 +142,7 @@ public function isBooleanType() */ public function isNumberType() { - return $this->getType() === 'number'; + return 'number' === $this->getType(); } /** @@ -150,7 +150,7 @@ public function isNumberType() */ public function isDateType() { - return $this->getType() === 'date' || $this->hasTimeParts(); + return 'date' === $this->getType() || $this->hasTimeParts(); } /** @@ -158,7 +158,7 @@ public function isDateType() */ public function hasTimeParts() { - return $this->getType() === 'datetime'; + return 'datetime' === $this->getType(); } /** @@ -194,13 +194,17 @@ private function setOperator(array $filter) { $operator = isset($filter['operator']) ? $filter['operator'] : null; - if ($this->getType() === 'multiselect') { + if ('multiselect' === $this->getType()) { $neg = strpos($operator, '!') === false ? '' : '!'; $this->operator = $neg.$this->getType(); return; } + if ('=' === $operator && is_array($this->getFilter())) { //Fix for old segments which can have stored = instead on in operator + $operator = 'in'; + } + $this->operator = $operator; } diff --git a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php index d184cde4056..4909995505a 100644 --- a/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php +++ b/app/bundles/LeadBundle/Tests/Segment/ContactSegmentFilterCrateTest.php @@ -199,4 +199,32 @@ public function testNotMultiselectFilter() $this->assertFalse($contactSegmentFilterCrate->isDateType()); $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); } + + /** + * @covers \Mautic\LeadBundle\Segment\ContactSegmentFilterCrate + */ + public function testOldEqualInsteadOfInOperator() + { + $filter = [ + 'glue' => 'and', + 'field' => 'tags', + 'object' => 'lead', + 'type' => 'tags', + 'filter' => [3], + 'display' => null, + 'operator' => '=', + ]; + + $contactSegmentFilterCrate = new ContactSegmentFilterCrate($filter); + + $this->assertSame('and', $contactSegmentFilterCrate->getGlue()); + $this->assertSame('tags', $contactSegmentFilterCrate->getField()); + $this->assertTrue($contactSegmentFilterCrate->isContactType()); + $this->assertFalse($contactSegmentFilterCrate->isCompanyType()); + $this->assertSame([3], $contactSegmentFilterCrate->getFilter()); + $this->assertSame('in', $contactSegmentFilterCrate->getOperator()); + $this->assertFalse($contactSegmentFilterCrate->isBooleanType()); + $this->assertFalse($contactSegmentFilterCrate->isDateType()); + $this->assertFalse($contactSegmentFilterCrate->hasTimeParts()); + } } From 159d3699e8656481a2dcbb1ed7738ec3a479e368 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Thu, 31 May 2018 17:09:18 +0200 Subject: [PATCH 543/778] Fix CSfixer issues --- .../LeadBundle/Command/CheckQueryBuildersCommand.php | 6 +++--- app/bundles/LeadBundle/Model/ListModel.php | 5 ++++- app/bundles/LeadBundle/Segment/ContactSegmentFilter.php | 1 - .../Segment/Query/Filter/BaseFilterQueryBuilder.php | 4 ++-- .../Query/Filter/DoNotContactFilterQueryBuilder.php | 2 +- .../Query/Filter/ForeignFuncFilterQueryBuilder.php | 3 +-- app/bundles/LeadBundle/Segment/Query/QueryBuilder.php | 8 +++++--- .../Services/ContactSegmentFilterDictionary.php | 2 +- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 63385c61bb4..4019997ca2f 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -156,13 +156,13 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $failed = ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); if (!$failed) { - $result = $listModel->getSegmentTotal($l); + $result = $listModel->getSegmentTotal($l); $expected = $result[$l->getId()]['count']; $lists = $listModel->getLeadsByList(['id'=>$l->getId()]); - $real = count($lists[$l->getId()]); + $real = count($lists[$l->getId()]); - if ($expected!=$real and count($l->getFilters())) { + if ($expected != $real and count($l->getFilters())) { echo "ERROR: database contains $real records but query proposes $expected results\n"; $failed = true; } diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 972fe572eda..70b05e2e1ea 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -833,10 +833,13 @@ public function getVersionNew(LeadList $entity) * @deprecated this method will be removed very soon, do not use it * * @param LeadList $entity + * * @return array + * * @throws \Exception */ - public function getSegmentTotal(LeadList $entity) { + public function getSegmentTotal(LeadList $entity) + { $id = $entity->getId(); $list = ['id' => $id, 'filters' => $entity->getFilters()]; $dtHelper = new DateTimeHelper(); diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php index a926f6785fd..4cdb0f6d476 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentFilter.php @@ -206,7 +206,6 @@ public function getDoNotContactParts() return new DoNotContactParts($this->contactSegmentFilterCrate->getField()); } - public function getIntegrationCampaignParts() { return new IntegrationCampaignParts($this->getParameterValue()); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php index e79ca6414e2..ebbda67a078 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/BaseFilterQueryBuilder.php @@ -112,7 +112,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $expression = new CompositeExpression(CompositeExpression::TYPE_OR, [ $queryBuilder->expr()->isNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')) + $queryBuilder->expr()->eq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')), ] ); break; @@ -120,7 +120,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $expression = new CompositeExpression(CompositeExpression::TYPE_AND, [ $queryBuilder->expr()->isNotNull($tableAlias.'.'.$filter->getField()), - $queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')) + $queryBuilder->expr()->neq($tableAlias.'.'.$filter->getField(), $queryBuilder->expr()->literal('')), ] ); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php index cc805a58c35..d7755815a4b 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/DoNotContactFilterQueryBuilder.php @@ -53,7 +53,7 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil $queryType = $filter->getParameterValue() ? 'isNull' : 'isNotNull'; } - $queryBuilder->addLogic($queryBuilder->expr()->$queryType($tableAlias.'.id'),$filter->getGlue()); + $queryBuilder->addLogic($queryBuilder->expr()->$queryType($tableAlias.'.id'), $filter->getGlue()); $queryBuilder->setParameter($exprParameter, $doNotContactParts->getParameterType()); $queryBuilder->setParameter($channelParameter, $doNotContactParts->getChannel()); diff --git a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php index 23e7bb76d0c..8a02432c345 100644 --- a/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/Filter/ForeignFuncFilterQueryBuilder.php @@ -110,9 +110,8 @@ public function applyQuery(QueryBuilder $queryBuilder, ContactSegmentFilter $fil if (!is_null($filter)) { $expression = $queryBuilder->expr()->$filterOperator( sprintf('%s(ifnull(%s,%s))', $filterAggr, $tableAlias.'.'.$filter->getField(), - ((intval($filter->getNullValue()) == $filter->getNullValue() && is_numeric($filter->getNullValue())) ? $filter->getNullValue() : "'" . $filter->getNullValue() ."'" )), + ((intval($filter->getNullValue()) == $filter->getNullValue() && is_numeric($filter->getNullValue())) ? $filter->getNullValue() : "'".$filter->getNullValue()."'")), $filterParametersHolder - ); } else { $expression = $queryBuilder->expr()->$filterOperator( diff --git a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php index 5b2240b548f..712690907f4 100644 --- a/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/QueryBuilder.php @@ -1660,7 +1660,8 @@ private function addLogicStack($expression) * @param $expression * @param $glue */ - public function addLogic($expression, $glue) { + public function addLogic($expression, $glue) + { // little setup $glue = strtolower($glue); @@ -1689,11 +1690,12 @@ public function addLogic($expression, $glue) { } /** - * Apply content of stack + * Apply content of stack. * * @return $this */ - public function applyStackLogic() { + public function applyStackLogic() + { if ($this->hasLogicStack()) { $stackGroupExpression = new CompositeExpression(CompositeExpression::TYPE_AND, $this->popLogicStack()); $this->orWhere($stackGroupExpression); diff --git a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php index a62a2bed4af..cb374f9eba2 100644 --- a/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php +++ b/app/bundles/LeadBundle/Services/ContactSegmentFilterDictionary.php @@ -33,7 +33,7 @@ public function __construct() 'table_field' => 'id', 'func' => 'sum', 'field' => 'open_count', - 'null_value' => 0 + 'null_value' => 0, ]; $this->translations['lead_email_received'] = [ From 696141fbd6459c3f821373c8a172d06426ea1bc6 Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 31 May 2018 18:39:27 +0200 Subject: [PATCH 544/778] Queries fixed, template added --- .../Translations/en_US/messages.ini | 2 +- .../EmailBundle/Entity/StatRepository.php | 39 +++++++- .../EventListener/DashboardSubscriber.php | 6 +- app/bundles/EmailBundle/Model/EmailModel.php | 9 +- .../Dashboard/Sent.email.to.contacts.html.php | 98 +++++++++++++++++++ .../PageBundle/Entity/RedirectRepository.php | 37 ++++--- 6 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Sent.email.to.contacts.html.php diff --git a/app/bundles/DashboardBundle/Translations/en_US/messages.ini b/app/bundles/DashboardBundle/Translations/en_US/messages.ini index 606210d5387..5cc8bbe6237 100644 --- a/app/bundles/DashboardBundle/Translations/en_US/messages.ini +++ b/app/bundles/DashboardBundle/Translations/en_US/messages.ini @@ -29,7 +29,7 @@ mautic.dashboard.label.segment.name="Segment name" mautic.dashboard.label.company.id="Company ID" mautic.dashboard.label.company.name="Company name" mautic.dashboard.label.campaign.id="Company ID" -mautic.dashboard.label.campaign.name="Company name" +mautic.dashboard.label.campaign.name="Campaign name" mautic.dashboard.widget.add="Add widget" mautic.dashboard.export.widgets="Export" mautic.dashboard.widget.import="Import" diff --git a/app/bundles/EmailBundle/Entity/StatRepository.php b/app/bundles/EmailBundle/Entity/StatRepository.php index ce85bd9bd27..a590820e584 100755 --- a/app/bundles/EmailBundle/Entity/StatRepository.php +++ b/app/bundles/EmailBundle/Entity/StatRepository.php @@ -47,6 +47,35 @@ public function getEmailStatus($trackingHash) return (!empty($result)) ? $result[0] : null; } + /** + * @param int $contactId + * @param int $emailId + * + * @return array + */ + public function getUniqueClickedLinksPerContactAndEmail($contactId, $emailId) + { + $q = $this->_em->getConnection()->createQueryBuilder(); + $q->select('ph.url') + ->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph') + ->where('ph.email_id = :emailId') + ->andWhere('ph.lead_id = :leadId') + ->setParameter('leadId', $contactId) + ->setParameter('emailId', $emailId) + ->groupBy('ph.url'); + + $result = $q->execute()->fetchAll(); + $data = []; + + if ($result) { + foreach ($result as $row) { + $data[] = $row['url']; + } + } + + return $data; + } + /** * @param int $limit * @param \DateTime $dateFrom @@ -68,14 +97,12 @@ public function getSentEmailToContactData( $segmentId = null ) { $q = $this->_em->getConnection()->createQueryBuilder(); - $q->select('s.*') + $q->select('s.id, s.lead_id, s.email_address, s.is_read, s.email_id') ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's') - ->innerJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id') + ->leftJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id') ->addSelect('e.name AS email_name') ->leftJoin('s', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 's.email_id = ph.email_id AND s.lead_id = ph.lead_id') - ->leftJoin('ph', MAUTIC_TABLE_PREFIX.'page_redirects', 'pr', 'ph.redirect_id = pr.redirect_id') - ->addSelect('pr.url AS link_url') - ->addSelect('pr.hits AS link_hits'); + ->addSelect('COUNT(ph.id) AS link_hits'); if ($createdByUserId !== null) { $q->andWhere('e.created_by = :userId') @@ -116,6 +143,8 @@ public function getSentEmailToContactData( } $q->setMaxResults($limit); + $q->groupBy('s.id'); + $q->orderBy('s.id', 'DESC'); return $q->execute()->fetchAll(); } diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index af5bcb1e433..d348e39ef38 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -139,10 +139,10 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) 'mautic.dashboard.label.contact.id', 'mautic.dashboard.label.contact.email.address', 'mautic.dashboard.label.contact.open', - 'mautic.dashboard.label.email.id', - 'mautic.dashboard.label.email.name', 'mautic.dashboard.label.contact.click', 'mautic.dashboard.label.contact.links.clicked', + 'mautic.dashboard.label.email.id', + 'mautic.dashboard.label.email.name', 'mautic.dashboard.label.segment.id', 'mautic.dashboard.label.segment.name', 'mautic.dashboard.label.company.id', @@ -167,7 +167,7 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) ); } - $event->setTemplate('MauticCoreBundle:Helper:table.html.php'); + $event->setTemplate('MauticEmailBundle:SubscribedEvents:Dashboard/Sent.email.to.contacts.html.php'); $event->stopPropagation(); } diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 88a7002e178..0f0c21cbedc 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -561,6 +561,7 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime } $stats = $this->getStatRepository()->getSentEmailToContactData($limit, $dateFrom, $dateTo, $createdByUserId, $companyId, $campaignId, $segmentId); $data = []; + foreach ($stats as $stat) { $statId = $stat['id']; if (!array_key_exists($statId, $data)) { @@ -572,16 +573,16 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime 'links_clicked' => [], 'email_id' => $stat['email_id'], 'email_name' => $stat['email_name'], - 'campaign_id' => $stat['campaign_id'], - 'campaign_name' => $stat['campaign_name'], 'segment_id' => $stat['segment_id'], 'segment_name' => $stat['segment_name'], 'company_id' => $stat['company_id'], 'company_name' => $stat['company_name'], + 'campaign_id' => $stat['campaign_id'], + 'campaign_name' => $stat['campaign_name'], ]; - if ($stat['link_url'] !== null) { - $item['links_clicked'][] = $stat['link_url']; + if ($item['click'] && $item['email_id'] && $item['contact_id']) { + $item['links_clicked'] = $this->getStatRepository()->getUniqueClickedLinksPerContactAndEmail($item['contact_id'], $item['email_id']); } $data[$statId] = $item; diff --git a/app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Sent.email.to.contacts.html.php b/app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Sent.email.to.contacts.html.php new file mode 100644 index 00000000000..fd95d171636 --- /dev/null +++ b/app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Sent.email.to.contacts.html.php @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + $row) : ?> + + + $item) : ?> + + + + + + + + + +
trans($headItem); ?>
+ + + shortenText($item, $shortenLinkText); ?> + + + + shortenText($item, $shortenLinkText); ?> + + + + shortenText($item, $shortenLinkText); ?> + + + + shortenText($item, $shortenLinkText); ?> + + + + shortenText($item, $shortenLinkText); ?> + + + + +
diff --git a/app/bundles/PageBundle/Entity/RedirectRepository.php b/app/bundles/PageBundle/Entity/RedirectRepository.php index 97d9419590c..7d2a0d22d95 100644 --- a/app/bundles/PageBundle/Entity/RedirectRepository.php +++ b/app/bundles/PageBundle/Entity/RedirectRepository.php @@ -116,39 +116,46 @@ public function getMostHitEmailRedirects( ->addSelect('pr.hits') ->addSelect('pr.unique_hits') ->from(MAUTIC_TABLE_PREFIX.'page_redirects', 'pr') - ->innerJoin('pr', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'pr.redirect_id = ph.redirect_id') - ->innerJoin('ph', MAUTIC_TABLE_PREFIX.'emails', 'e', 'ph.email_id = e.id') + ->leftJoin('pr', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'pr.redirect_id = ph.redirect_id') + ->leftJoin('ph', MAUTIC_TABLE_PREFIX.'emails', 'e', 'ph.email_id = e.id') ->addSelect('e.id AS email_id') ->addSelect('e.name AS email_name'); + if ($createdByUserId !== null) { $q->andWhere('e.created_by = :userId') ->setParameter('userId', $createdByUserId); } + $q->andWhere('pr.date_added BETWEEN :dateFrom AND :dateTo') ->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s')) ->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s')); + if ($companyId !== null) { - $q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'ph.lead_id = cl.lead_id') + $q->leftJoin('ph', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', 'ph.lead_id = cl.lead_id') ->andWhere('cl.company_id = :companyId') ->setParameter('companyId', $companyId); } + $q->leftJoin('ph', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 'ph.source_id = ce.id AND ph.source = "campaign.event"') + ->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->addSelect('campaign.id AS campaign_id') + ->addSelect('campaign.name AS campaign_name'); + if ($campaignId !== null) { - $q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 'ph.source_id = ce.id AND ph.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId) - ->addSelect('campaign.id AS campaign_id') - ->addSelect('campaign.name AS campaign_name'); + $q->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); } + + $q->leftJoin('ph', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll', 'ph.lead_id = lll.lead_id') + ->leftJoin('lll', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'lll.leadlist_id = ll.id') + ->addSelect('ll.id AS segment_id') + ->addSelect('ll.name AS segment_name'); + if ($segmentId !== null) { - $q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll', 'ph.lead_id = lll.lead_id') - ->innerJoin('lll', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'lll.leadlist_id = ll.id') - ->andWhere('lll.leadlist_id = :segmentId') - ->setParameter('segmentId', $segmentId) - ->addSelect('ll.id AS segment_id') - ->addSelect('ll.name AS segment_name'); + $q->andWhere('lll.leadlist_id = :segmentId') + ->setParameter('segmentId', $segmentId); } + $q->setMaxResults($limit); return $q->execute()->fetchAll(); From edabbbd77b481aad371420ae83026305a19b8e4f Mon Sep 17 00:00:00 2001 From: John Linhart Date: Thu, 31 May 2018 18:46:48 +0200 Subject: [PATCH 545/778] Added widget template and ordering --- .../EventListener/DashboardSubscriber.php | 2 +- .../Most.hit.email.redirects.html.php | 68 +++++++++++++++++++ .../PageBundle/Entity/RedirectRepository.php | 2 + 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Most.hit.email.redirects.html.php diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index d348e39ef38..d028ea5c496 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -214,7 +214,7 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) ]); } - $event->setTemplate('MauticCoreBundle:Helper:table.html.php'); + $event->setTemplate('MauticEmailBundle:SubscribedEvents:Dashboard/Most.hit.email.redirects.html.php'); $event->stopPropagation(); } diff --git a/app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Most.hit.email.redirects.html.php b/app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Most.hit.email.redirects.html.php new file mode 100644 index 00000000000..f560593509d --- /dev/null +++ b/app/bundles/EmailBundle/Views/SubscribedEvents/Dashboard/Most.hit.email.redirects.html.php @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + $row) : ?> + + + $item) : ?> + + + + + + + + + +
trans($headItem); ?>
+ + + shortenText($item, $shortenLinkText); ?> + + + + +
diff --git a/app/bundles/PageBundle/Entity/RedirectRepository.php b/app/bundles/PageBundle/Entity/RedirectRepository.php index 7d2a0d22d95..dd04c6fcd3e 100644 --- a/app/bundles/PageBundle/Entity/RedirectRepository.php +++ b/app/bundles/PageBundle/Entity/RedirectRepository.php @@ -157,6 +157,8 @@ public function getMostHitEmailRedirects( } $q->setMaxResults($limit); + $q->groupBy('pr.id'); + $q->orderBy('pr.hits', 'DESC'); return $q->execute()->fetchAll(); } From 7d5ee831c6c91ef3ce6a6610ea8857785c043342 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 31 May 2018 16:17:33 -0500 Subject: [PATCH 546/778] Use existing lookup field types instead of rebuilding so that ajax is used in the case of companies where there could be tens of thousands of results and also ACL is applied to campaigns and segments. --- app/bundles/EmailBundle/Config/config.php | 6 -- .../Type/DashboardEmailsInTimeWidgetType.php | 88 +++---------------- 2 files changed, 13 insertions(+), 81 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index fd7cc77af31..dc2c780c6ec 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -316,12 +316,6 @@ 'mautic.form.type.email_dashboard_emails_in_time_widget' => [ 'class' => 'Mautic\EmailBundle\Form\Type\DashboardEmailsInTimeWidgetType', 'alias' => 'email_dashboard_emails_in_time_widget', - 'arguments' => [ - 'mautic.campaign.repository.campaign', - 'mautic.lead.repository.company', - 'mautic.lead.repository.lead_list', - 'mautic.helper.user', - ], ], 'mautic.form.type.email_to_user' => [ 'class' => Mautic\EmailBundle\Form\Type\EmailToUserType::class, diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index 326500b3e82..14bd7c19043 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -11,12 +11,6 @@ namespace Mautic\EmailBundle\Form\Type; -use Mautic\CampaignBundle\Entity\Campaign; -use Mautic\CampaignBundle\Entity\CampaignRepository; -use Mautic\CoreBundle\Helper\UserHelper; -use Mautic\LeadBundle\Entity\CompanyRepository; -use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Entity\LeadListRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -25,46 +19,6 @@ */ class DashboardEmailsInTimeWidgetType extends AbstractType { - /** - * @var CampaignRepository - */ - private $campaignRepository; - - /** - * @var CompanyRepository - */ - private $companyRepository; - - /** - * @var LeadListRepository - */ - private $segmentsRepository; - - /** - * @var UserHelper - */ - private $userHelper; - - /** - * DashboardEmailsInTimeWidgetType constructor. - * - * @param CampaignRepository $campaignRepository - * @param CompanyRepository $companyRepository - * @param LeadListRepository $leadListRepository - * @param UserHelper $userHelper - */ - public function __construct( - CampaignRepository $campaignRepository, - CompanyRepository $companyRepository, - LeadListRepository $leadListRepository, - UserHelper $userHelper - ) { - $this->campaignRepository = $campaignRepository; - $this->companyRepository = $companyRepository; - $this->segmentsRepository = $leadListRepository; - $this->userHelper = $userHelper; - } - /** * @param FormBuilderInterface $builder * @param array $options @@ -86,44 +40,28 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'required' => false, ] ); - $user = $this->userHelper->getUser(); - $companies = $this->companyRepository->getCompanies($user); - $companiesChoises = []; - foreach ($companies as $company) { - $companiesChoises[$company['id']] = $company['companyname']; - } - $builder->add('companyId', 'choice', [ - 'label' => 'mautic.email.companyId.filter', - 'choices' => $companiesChoises, - 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], - 'empty_data' => '', - 'required' => false, + + $builder->add('companyId', 'company_list', [ + 'label' => 'mautic.email.companyId.filter', + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + 'multiple' => false, + 'modal_route' => null, ]); - /** @var Campaign[] $campaigns */ - $campaigns = $this->campaignRepository->findAll(); - $campaignsChoices = []; - foreach ($campaigns as $campaign) { - $campaignsChoices[$campaign->getId()] = $campaign->getName(); - } - $builder->add('campaignId', 'choice', [ + + $builder->add('campaignId', 'campaign_list', [ 'label' => 'mautic.email.campaignId.filter', - 'choices' => $campaignsChoices, 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', 'required' => false, ] ); - /** @var LeadList[] $segments */ - $segments = $this->segmentsRepository->getLists($user); - $segmentsChoices = []; - foreach ($segments as $segment) { - $segmentsChoices[$segment['id']] = $segment['name']; - } - $builder->add('segmentId', 'choice', [ + + $builder->add('segmentId', 'leadlist_choices', [ 'label' => 'mautic.email.segmentId.filter', - 'choices' => $segmentsChoices, 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', From 7cce5a2fffd6a537174466bb66eb0c16e9bb9233 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 31 May 2018 16:23:16 -0500 Subject: [PATCH 547/778] Use existing lookup field types instead of rebuilding so that ajax is used in the case of companies where there could be tens of thousands of results and also ACL is applied to campaigns and segments. --- app/bundles/EmailBundle/Config/config.php | 12 --- ...shboardMostHitEmailRedirectsWidgetType.php | 87 +++-------------- ...DashboardSentEmailToContactsWidgetType.php | 96 ++++--------------- 3 files changed, 33 insertions(+), 162 deletions(-) diff --git a/app/bundles/EmailBundle/Config/config.php b/app/bundles/EmailBundle/Config/config.php index 53dbc872e2f..747bee36fdf 100644 --- a/app/bundles/EmailBundle/Config/config.php +++ b/app/bundles/EmailBundle/Config/config.php @@ -320,22 +320,10 @@ 'mautic.form.type.email_dashboard_sent_email_to_contacts_widget' => [ 'class' => \Mautic\EmailBundle\Form\Type\DashboardSentEmailToContactsWidgetType::class, 'alias' => 'email_dashboard_sent_email_to_contacts_widget', - 'arguments' => [ - 'mautic.campaign.repository.campaign', - 'mautic.lead.repository.company', - 'mautic.lead.repository.lead_list', - 'mautic.helper.user', - ], ], 'mautic.form.type.email_dashboard_most_hit_email_redirects_widget' => [ 'class' => \Mautic\EmailBundle\Form\Type\DashboardMostHitEmailRedirectsWidgetType::class, 'alias' => 'email_dashboard_most_hit_email_redirects_widget', - 'arguments' => [ - 'mautic.campaign.repository.campaign', - 'mautic.lead.repository.company', - 'mautic.lead.repository.lead_list', - 'mautic.helper.user', - ], ], 'mautic.form.type.email_to_user' => [ 'class' => Mautic\EmailBundle\Form\Type\EmailToUserType::class, diff --git a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php index 51bb5cb021b..cd608c27c89 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php @@ -2,12 +2,6 @@ namespace Mautic\EmailBundle\Form\Type; -use Mautic\CampaignBundle\Entity\Campaign; -use Mautic\CampaignBundle\Entity\CampaignRepository; -use Mautic\CoreBundle\Helper\UserHelper; -use Mautic\LeadBundle\Entity\CompanyRepository; -use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Entity\LeadListRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -16,91 +10,34 @@ */ class DashboardMostHitEmailRedirectsWidgetType extends AbstractType { - /** - * @var CampaignRepository - */ - private $campaignRepository; - - /** - * @var CompanyRepository - */ - private $companyRepository; - - /** - * @var LeadListRepository - */ - private $segmentsRepository; - - /** - * @var UserHelper - */ - private $userHelper; - - /** - * DashboardMostHitEmailRedirectsWidgetType constructor. - * - * @param CampaignRepository $campaignRepository - * @param CompanyRepository $companyRepository - * @param LeadListRepository $leadListRepository - * @param UserHelper $userHelper - */ - public function __construct( - CampaignRepository $campaignRepository, - CompanyRepository $companyRepository, - LeadListRepository $leadListRepository, - UserHelper $userHelper - ) { - $this->campaignRepository = $campaignRepository; - $this->companyRepository = $companyRepository; - $this->segmentsRepository = $leadListRepository; - $this->userHelper = $userHelper; - } - /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { - $user = $this->userHelper->getUser(); - $companies = $this->companyRepository->getCompanies($user); - $companiesChoises = []; - foreach ($companies as $company) { - $companiesChoises[$company['id']] = $company['companyname']; - } - $builder->add('companyId', 'choice', [ - 'label' => 'mautic.email.companyId.filter', - 'choices' => $companiesChoises, - 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], - 'empty_data' => '', - 'required' => false, + $builder->add('companyId', 'company_list', [ + 'label' => 'mautic.email.companyId.filter', + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + 'multiple' => false, + 'modal_route' => null, // disable "Add new" option in ajax lookup ] ); - /** @var Campaign[] $campaigns */ - $campaigns = $this->campaignRepository->findAll(); - $campaignsChoices = []; - foreach ($campaigns as $campaign) { - $campaignsChoices[$campaign->getId()] = $campaign->getName(); - } - $builder->add('campaignId', 'choice', [ + + $builder->add('campaignId', 'campaign_list', [ 'label' => 'mautic.email.campaignId.filter', - 'choices' => $campaignsChoices, 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', 'required' => false, ] ); - /** @var LeadList[] $segments */ - $segments = $this->segmentsRepository->getLists($user); - $segmentsChoices = []; - foreach ($segments as $segment) { - $segmentsChoices[$segment['id']] = $segment['name']; - } - $builder->add('segmentId', 'choice', [ + + $builder->add('segmentId', 'leadlist_choices', [ 'label' => 'mautic.email.segmentId.filter', - 'choices' => $segmentsChoices, 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', diff --git a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php index 6978977c140..fded85c2fed 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php @@ -2,12 +2,6 @@ namespace Mautic\EmailBundle\Form\Type; -use Mautic\CampaignBundle\Entity\Campaign; -use Mautic\CampaignBundle\Entity\CampaignRepository; -use Mautic\CoreBundle\Helper\UserHelper; -use Mautic\LeadBundle\Entity\CompanyRepository; -use Mautic\LeadBundle\Entity\LeadList; -use Mautic\LeadBundle\Entity\LeadListRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -16,91 +10,43 @@ */ class DashboardSentEmailToContactsWidgetType extends AbstractType { - /** - * @var CampaignRepository - */ - private $campaignRepository; - - /** - * @var CompanyRepository - */ - private $companyRepository; - - /** - * @var LeadListRepository - */ - private $segmentsRepository; - - /** - * @var UserHelper - */ - private $userHelper; - - /** - * DashboardSentEmailToContactsWidgetType constructor. - * - * @param CampaignRepository $campaignRepository - * @param CompanyRepository $companyRepository - * @param LeadListRepository $leadListRepository - * @param UserHelper $userHelper - */ - public function __construct( - CampaignRepository $campaignRepository, - CompanyRepository $companyRepository, - LeadListRepository $leadListRepository, - UserHelper $userHelper - ) { - $this->campaignRepository = $campaignRepository; - $this->companyRepository = $companyRepository; - $this->segmentsRepository = $leadListRepository; - $this->userHelper = $userHelper; - } - /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { - $user = $this->userHelper->getUser(); - $companies = $this->companyRepository->getCompanies($user); - $companiesChoises = []; - foreach ($companies as $company) { - $companiesChoises[$company['id']] = $company['companyname']; - } - $builder->add('companyId', 'choice', [ - 'label' => 'mautic.email.companyId.filter', - 'choices' => $companiesChoises, - 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], - 'empty_data' => '', - 'required' => false, + $builder->add( + 'companyId', + 'company_list', + [ + 'label' => 'mautic.email.companyId.filter', + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + 'multiple' => false, + 'modal_route' => null, // disable "Add new" option in ajax lookup ] ); - /** @var Campaign[] $campaigns */ - $campaigns = $this->campaignRepository->findAll(); - $campaignsChoices = []; - foreach ($campaigns as $campaign) { - $campaignsChoices[$campaign->getId()] = $campaign->getName(); - } - $builder->add('campaignId', 'choice', [ + + $builder->add( + 'campaignId', + 'campaign_list', + [ 'label' => 'mautic.email.campaignId.filter', - 'choices' => $campaignsChoices, 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', 'required' => false, ] ); - /** @var LeadList[] $segments */ - $segments = $this->segmentsRepository->getLists($user); - $segmentsChoices = []; - foreach ($segments as $segment) { - $segmentsChoices[$segment['id']] = $segment['name']; - } - $builder->add('segmentId', 'choice', [ + + $builder->add( + 'segmentId', + 'leadlist_choices', + [ 'label' => 'mautic.email.segmentId.filter', - 'choices' => $segmentsChoices, 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', From 9515851dd1ebbc7c98d15942345d4a602b518a04 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 31 May 2018 16:36:07 -0500 Subject: [PATCH 548/778] Fixed multiple on campaign list --- .../DashboardMostHitEmailRedirectsWidgetType.php | 16 +++++++++++++--- .../DashboardSentEmailToContactsWidgetType.php | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php index cd608c27c89..26665abc753 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardMostHitEmailRedirectsWidgetType.php @@ -16,7 +16,10 @@ class DashboardMostHitEmailRedirectsWidgetType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('companyId', 'company_list', [ + $builder->add( + 'companyId', + 'company_list', + [ 'label' => 'mautic.email.companyId.filter', 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], @@ -27,16 +30,23 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $builder->add('campaignId', 'campaign_list', [ + $builder->add( + 'campaignId', + 'campaign_list', + [ 'label' => 'mautic.email.campaignId.filter', 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', 'required' => false, + 'multiple' => false, ] ); - $builder->add('segmentId', 'leadlist_choices', [ + $builder->add( + 'segmentId', + 'leadlist_choices', + [ 'label' => 'mautic.email.segmentId.filter', 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], diff --git a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php index fded85c2fed..23b6f028b32 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardSentEmailToContactsWidgetType.php @@ -39,6 +39,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'attr' => ['class' => 'form-control'], 'empty_data' => '', 'required' => false, + 'multiple' => false, ] ); From fb3f3c22edfa6d8f95517d8a0fd26f45fe68fd78 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 31 May 2018 16:37:18 -0500 Subject: [PATCH 549/778] Fixed multiple on campaign list --- .../Type/DashboardEmailsInTimeWidgetType.php | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index 14bd7c19043..9513f3d55d3 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -25,9 +25,12 @@ class DashboardEmailsInTimeWidgetType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('flag', 'choice', [ - 'label' => 'mautic.email.flag.filter', - 'choices' => [ + $builder->add( + 'flag', + 'choice', + [ + 'label' => 'mautic.email.flag.filter', + 'choices' => [ '' => 'mautic.email.flag.sent', 'opened' => 'mautic.email.flag.opened', 'failed' => 'mautic.email.flag.failed', @@ -41,26 +44,37 @@ public function buildForm(FormBuilderInterface $builder, array $options) ] ); - $builder->add('companyId', 'company_list', [ - 'label' => 'mautic.email.companyId.filter', - 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], - 'empty_data' => '', - 'required' => false, - 'multiple' => false, - 'modal_route' => null, - ]); + $builder->add( + 'companyId', + 'company_list', + [ + 'label' => 'mautic.email.companyId.filter', + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_data' => '', + 'required' => false, + 'multiple' => false, + 'modal_route' => null, + ] + ); - $builder->add('campaignId', 'campaign_list', [ + $builder->add( + 'campaignId', + 'campaign_list', + [ 'label' => 'mautic.email.campaignId.filter', 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], 'empty_data' => '', 'required' => false, + 'multiple' => false, ] ); - $builder->add('segmentId', 'leadlist_choices', [ + $builder->add( + 'segmentId', + 'leadlist_choices', + [ 'label' => 'mautic.email.segmentId.filter', 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], From 40d33dbd47d151dfca395638ccef9eb9d8416ef7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 31 May 2018 17:18:11 -0500 Subject: [PATCH 550/778] Use empty value to prevent 'The value of type "array" cannot be converted to a valid array key"' error and to allow removal of a selected filter. --- .../Type/DashboardEmailsInTimeWidgetType.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php index 9513f3d55d3..87d0bfb0444 100644 --- a/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php +++ b/app/bundles/EmailBundle/Form/Type/DashboardEmailsInTimeWidgetType.php @@ -51,7 +51,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label' => 'mautic.email.companyId.filter', 'label_attr' => ['class' => 'control-label'], 'attr' => ['class' => 'form-control'], - 'empty_data' => '', + 'empty_value' => '', 'required' => false, 'multiple' => false, 'modal_route' => null, @@ -62,12 +62,12 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'campaignId', 'campaign_list', [ - 'label' => 'mautic.email.campaignId.filter', - 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], - 'empty_data' => '', - 'required' => false, - 'multiple' => false, + 'label' => 'mautic.email.campaignId.filter', + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_value' => '', + 'required' => false, + 'multiple' => false, ] ); @@ -75,11 +75,11 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'segmentId', 'leadlist_choices', [ - 'label' => 'mautic.email.segmentId.filter', - 'label_attr' => ['class' => 'control-label'], - 'attr' => ['class' => 'form-control'], - 'empty_data' => '', - 'required' => false, + 'label' => 'mautic.email.segmentId.filter', + 'label_attr' => ['class' => 'control-label'], + 'attr' => ['class' => 'form-control'], + 'empty_value' => '', + 'required' => false, ] ); } From 5a7e2379acf4da2821f52ec183c533525cab63d7 Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Thu, 31 May 2018 17:18:28 -0500 Subject: [PATCH 551/778] Clean filter input from the request --- .../DashboardBundle/Controller/Api/WidgetApiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php index 025aca3eb8d..f63c70537af 100644 --- a/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php +++ b/app/bundles/DashboardBundle/Controller/Api/WidgetApiController.php @@ -90,7 +90,7 @@ public function getDataAction($type) 'dateFrom' => $fromDate, 'dateTo' => $toDate, 'limit' => (int) $this->request->get('limit', null), - 'filter' => $this->request->get('filter', []), + 'filter' => InputHelper::clean($this->request->get('filter', [])), 'dataset' => $dataset, ]; From aad455ee79a93502ff692201081197feb46de674 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 1 Jun 2018 13:01:16 +0200 Subject: [PATCH 552/778] Check results command - exclude visitors --- .../Command/CheckQueryBuildersCommand.php | 16 +++++++--------- app/bundles/LeadBundle/Model/ListModel.php | 3 ++- .../LeadBundle/Segment/ContactSegmentService.php | 9 +++++++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 4019997ca2f..02bab8013cd 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -155,17 +155,15 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $failed = ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); - if (!$failed) { - $result = $listModel->getSegmentTotal($l); - $expected = $result[$l->getId()]['count']; + $result = $listModel->getSegmentTotal($l); + $expected = $result[$l->getId()]['count']; - $lists = $listModel->getLeadsByList(['id'=>$l->getId()]); - $real = count($lists[$l->getId()]); + $lists = $listModel->getLeadsByList(['id'=>$l->getId()]); + $real = count($lists[$l->getId()]); - if ($expected != $real and count($l->getFilters())) { - echo "ERROR: database contains $real records but query proposes $expected results\n"; - $failed = true; - } + if ($expected != $real and count($l->getFilters())) { + echo "ERROR: database contains $real records but query proposes $expected results\n"; + $failed = true; } return !$failed; diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index 70b05e2e1ea..e137c96fbed 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -845,7 +845,8 @@ public function getSegmentTotal(LeadList $entity) $dtHelper = new DateTimeHelper(); $batchLimiters = [ - 'dateTime' => $dtHelper->toUtcString(), + 'dateTime' => $dtHelper->toUtcString(), + 'excludeVisitors' => true, ]; return $this->leadSegmentService->getTotalLeadListLeadsCount($entity, $batchLimiters); diff --git a/app/bundles/LeadBundle/Segment/ContactSegmentService.php b/app/bundles/LeadBundle/Segment/ContactSegmentService.php index 20df16bbb08..a6742f12930 100644 --- a/app/bundles/LeadBundle/Segment/ContactSegmentService.php +++ b/app/bundles/LeadBundle/Segment/ContactSegmentService.php @@ -88,13 +88,14 @@ public function getNewLeadListLeadsCount(LeadList $segment, array $batchLimiters } /** - * @param LeadList $segment + * @param LeadList $segment + * @param array|null $batchLimiters for debug purpose only * * @return array * * @throws \Exception */ - public function getTotalLeadListLeadsCount(LeadList $segment) + public function getTotalLeadListLeadsCount(LeadList $segment, array $batchLimiters = null) { $segmentFilters = $this->contactSegmentFilterFactory->getSegmentFilters($segment); @@ -110,6 +111,10 @@ public function getTotalLeadListLeadsCount(LeadList $segment) $qb = $this->getTotalSegmentContactsQuery($segment); + if (!empty($batchLimiters['excludeVisitors'])) { + $this->excludeVisitors($qb); + } + $qb = $this->contactSegmentQueryBuilder->wrapInCount($qb); $this->logger->debug('Segment QB: Create SQL: '.$qb->getDebugOutput(), ['segmentId' => $segment->getId()]); From c962dc29193aecedfff048d018dfa61d431b49ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 1 Jun 2018 13:06:18 +0200 Subject: [PATCH 553/778] Company. Campaign, Segment moved to filter --- .../EventListener/DashboardSubscriber.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index d028ea5c496..e42232f379a 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -124,16 +124,16 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $limit = $params['limit']; } $companyId = null; - if (isset($params['companyId'])) { - $companyId = $params['companyId']; + if (isset($params['filter']['companyId'])) { + $companyId = $params['filter']['companyId']; } $campaignId = null; - if (isset($params['campaignId'])) { - $campaignId = $params['campaignId']; + if (isset($params['filter']['campaignId'])) { + $campaignId = $params['filter']['campaignId']; } $segmentId = null; - if (isset($params['segmentId'])) { - $segmentId = $params['segmentId']; + if (isset($params['filter']['segmentId'])) { + $segmentId = $params['filter']['segmentId']; } $headItems = [ 'mautic.dashboard.label.contact.id', @@ -183,16 +183,16 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $limit = $params['limit']; } $companyId = null; - if (isset($params['companyId'])) { - $companyId = $params['companyId']; + if (isset($params['filter']['companyId'])) { + $companyId = $params['filter']['companyId']; } $campaignId = null; - if (isset($params['campaignId'])) { - $campaignId = $params['campaignId']; + if (isset($params['filter']['campaignId'])) { + $campaignId = $params['filter']['campaignId']; } $segmentId = null; - if (isset($params['segmentId'])) { - $segmentId = $params['segmentId']; + if (isset($params['filter']['segmentId'])) { + $segmentId = $params['filter']['segmentId']; } $event->setTemplateData([ 'headItems' => [ From a7b9601cfb41f5a6a5a0fc44eabe8496707cf3a9 Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 1 Jun 2018 13:59:37 +0200 Subject: [PATCH 554/778] fix batch filters applied incorrectly --- .../Command/CheckQueryBuildersCommand.php | 2 +- .../Query/ContactSegmentQueryBuilder.php | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 63385c61bb4..432a42c718f 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -163,7 +163,7 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $real = count($lists[$l->getId()]); if ($expected!=$real and count($l->getFilters())) { - echo "ERROR: database contains $real records but query proposes $expected results\n"; + echo "ERROR: database contains $real records but query suggests $expected records\n"; $failed = true; } } diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 480ad357e73..4be6ab4d2e9 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -180,14 +180,15 @@ public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentI $queryBuilder->leftJoin('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', $tableAlias, $tableAlias.'.lead_id = l.id'); $queryBuilder->addSelect($tableAlias.'.lead_id AS '.$tableAlias.'_lead_id'); - $expression = $queryBuilder->expr()->andX( - $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId), - $queryBuilder->expr()->orX( - $queryBuilder->expr()->isNull($tableAlias.'.date_added'), - $queryBuilder->expr()->lte($tableAlias.'.date_added', "'".$batchRestrictions['dateTime']."'") - ) - ); - + if (isset($batchRestrictions['dateTime'])) { + $expression = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId), + $queryBuilder->expr()->lte('l.date_added', "'".$batchRestrictions['dateTime']."'") + ); + } else { + $expression = $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId); + } + $queryBuilder->addJoinCondition($tableAlias, $expression); if ($setHaving) { From da0e3c8e260ee46cca3430ec09a451c49a27e15b Mon Sep 17 00:00:00 2001 From: Jan Kozak Date: Fri, 1 Jun 2018 14:04:26 +0200 Subject: [PATCH 555/778] fix merge problem --- .../Command/CheckQueryBuildersCommand.php | 49 +++++++++---------- .../Query/ContactSegmentQueryBuilder.php | 2 +- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index 66e3fc097cf..a974c5d6b30 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -39,14 +39,14 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); + $container = $this->getContainer(); $this->logger = $container->get('monolog.logger.mautic'); /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ $listModel = $container->get('mautic.lead.model.list'); - $id = $input->getOption('segment-id'); - $verbose = $input->getOption('verbose'); + $id = $input->getOption('segment-id'); + $verbose = $input->getOption('verbose'); $this->skipOld = $input->getOption('skip-old'); $failed = $ok = 0; @@ -55,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $list = $listModel->getEntity($id); if (!$list) { - $output->writeln('Segment with id "'.$id.'" not found'); + $output->writeln('Segment with id "' . $id . '" not found'); return 1; } @@ -69,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $lists = $listModel->getEntities( [ 'iterator_mode' => true, - 'orderBy' => 'l.id', + 'orderBy' => 'l.id', ] ); @@ -109,24 +109,24 @@ private function format_period($inputSeconds) private function runSegment($output, $verbose, $l, ListModel $listModel) { - $output->write('Running segment '.$l->getId().'...'); + $output->write('Running segment ' . $l->getId() . '...'); if (!$this->skipOld) { $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); - $timer1 = microtime(true); + $timer1 = microtime(true); $processed = $listModel->getVersionOld($l); - $timer1 = microtime(true) - $timer1; + $timer1 = microtime(true) - $timer1; } else { - $processed = ['count'=>-1, 'maxId'=>-1]; - $timer1 = 0; + $processed = ['count' => -1, 'maxId' => -1]; + $timer1 = 0; } $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); - $timer2 = microtime(true); + $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); - $timer2 = microtime(true) - $timer2; + $timer2 = microtime(true) - $timer2; $processed2 = array_shift($processed2); @@ -138,12 +138,12 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $output->write( sprintf('old: c: %d, m: %d, time: %s(est) <--> new: c: %d, m: %s, time: %s', - $processed['count'], - $processed['maxId'], - $this->format_period($timer1), - $processed2['count'], - $processed2['maxId'], - $this->format_period($timer2) + $processed['count'], + $processed['maxId'], + $this->format_period($timer1), + $processed2['count'], + $processed2['maxId'], + $this->format_period($timer2) ) ); @@ -155,16 +155,15 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $failed = ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); - $result = $listModel->getSegmentTotal($l); + $result = $listModel->getSegmentTotal($l); $expected = $result[$l->getId()]['count']; - $lists = $listModel->getLeadsByList(['id'=>$l->getId()]); - $real = count($lists[$l->getId()]); + $lists = $listModel->getLeadsByList(['id' => $l->getId()]); + $real = count($lists[$l->getId()]); - if ($expected!=$real and count($l->getFilters())) { - echo "ERROR: database contains $real records but query proposes $expected results\n"; - $failed = true; - } + if ($expected != $real and count($l->getFilters())) { + echo "ERROR: database contains $real records but query proposes $expected results\n"; + $failed = true; } return !$failed; diff --git a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php index 4be6ab4d2e9..733df8a9fb0 100644 --- a/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php +++ b/app/bundles/LeadBundle/Segment/Query/ContactSegmentQueryBuilder.php @@ -188,7 +188,7 @@ public function addNewContactsRestrictions(QueryBuilder $queryBuilder, $segmentI } else { $expression = $queryBuilder->expr()->eq($tableAlias.'.leadlist_id', $segmentId); } - + $queryBuilder->addJoinCondition($tableAlias, $expression); if ($setHaving) { From 16dce1893cc74a6f75986496a50a4b03d84da0cb Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 1 Jun 2018 14:09:09 +0200 Subject: [PATCH 556/778] Fix CSFixer issues --- .../Command/CheckQueryBuildersCommand.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index a974c5d6b30..b9d1b68852e 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -39,14 +39,14 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); + $container = $this->getContainer(); $this->logger = $container->get('monolog.logger.mautic'); /** @var \Mautic\LeadBundle\Model\ListModel $listModel */ $listModel = $container->get('mautic.lead.model.list'); - $id = $input->getOption('segment-id'); - $verbose = $input->getOption('verbose'); + $id = $input->getOption('segment-id'); + $verbose = $input->getOption('verbose'); $this->skipOld = $input->getOption('skip-old'); $failed = $ok = 0; @@ -55,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $list = $listModel->getEntity($id); if (!$list) { - $output->writeln('Segment with id "' . $id . '" not found'); + $output->writeln('Segment with id "'.$id.'" not found'); return 1; } @@ -69,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $lists = $listModel->getEntities( [ 'iterator_mode' => true, - 'orderBy' => 'l.id', + 'orderBy' => 'l.id', ] ); @@ -109,24 +109,24 @@ private function format_period($inputSeconds) private function runSegment($output, $verbose, $l, ListModel $listModel) { - $output->write('Running segment ' . $l->getId() . '...'); + $output->write('Running segment '.$l->getId().'...'); if (!$this->skipOld) { $this->logger->info(sprintf('Running OLD segment #%d', $l->getId())); - $timer1 = microtime(true); + $timer1 = microtime(true); $processed = $listModel->getVersionOld($l); - $timer1 = microtime(true) - $timer1; + $timer1 = microtime(true) - $timer1; } else { $processed = ['count' => -1, 'maxId' => -1]; - $timer1 = 0; + $timer1 = 0; } $this->logger->info(sprintf('Running NEW segment #%d', $l->getId())); - $timer2 = microtime(true); + $timer2 = microtime(true); $processed2 = $listModel->getVersionNew($l); - $timer2 = microtime(true) - $timer2; + $timer2 = microtime(true) - $timer2; $processed2 = array_shift($processed2); @@ -155,11 +155,11 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $failed = ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); - $result = $listModel->getSegmentTotal($l); + $result = $listModel->getSegmentTotal($l); $expected = $result[$l->getId()]['count']; $lists = $listModel->getLeadsByList(['id' => $l->getId()]); - $real = count($lists[$l->getId()]); + $real = count($lists[$l->getId()]); if ($expected != $real and count($l->getFilters())) { echo "ERROR: database contains $real records but query proposes $expected results\n"; From f7f8f8899c43f1eaec660534587d9cfe1e7ec6d2 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 1 Jun 2018 14:30:01 +0200 Subject: [PATCH 557/778] Remove check for total results in DB - Not correct according to manually added / removed contacts --- .../Command/CheckQueryBuildersCommand.php | 11 --------- app/bundles/LeadBundle/Model/ListModel.php | 23 ------------------- 2 files changed, 34 deletions(-) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index b9d1b68852e..fc49c3a1314 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -155,17 +155,6 @@ private function runSegment($output, $verbose, $l, ListModel $listModel) $failed = ((intval($processed['count']) != intval($processed2['count'])) or (intval($processed['maxId']) != intval($processed2['maxId']))); - $result = $listModel->getSegmentTotal($l); - $expected = $result[$l->getId()]['count']; - - $lists = $listModel->getLeadsByList(['id' => $l->getId()]); - $real = count($lists[$l->getId()]); - - if ($expected != $real and count($l->getFilters())) { - echo "ERROR: database contains $real records but query proposes $expected results\n"; - $failed = true; - } - return !$failed; } } diff --git a/app/bundles/LeadBundle/Model/ListModel.php b/app/bundles/LeadBundle/Model/ListModel.php index e137c96fbed..caf88ff02c9 100644 --- a/app/bundles/LeadBundle/Model/ListModel.php +++ b/app/bundles/LeadBundle/Model/ListModel.php @@ -829,29 +829,6 @@ public function getVersionNew(LeadList $entity) return $this->leadSegmentService->getNewLeadListLeadsCount($entity, $batchLimiters); } - /** - * @deprecated this method will be removed very soon, do not use it - * - * @param LeadList $entity - * - * @return array - * - * @throws \Exception - */ - public function getSegmentTotal(LeadList $entity) - { - $id = $entity->getId(); - $list = ['id' => $id, 'filters' => $entity->getFilters()]; - $dtHelper = new DateTimeHelper(); - - $batchLimiters = [ - 'dateTime' => $dtHelper->toUtcString(), - 'excludeVisitors' => true, - ]; - - return $this->leadSegmentService->getTotalLeadListLeadsCount($entity, $batchLimiters); - } - /** * @param LeadList $entity * From 02653f2c9b0c2e7c05a966fb91c9d77d2343bc5e Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 1 Jun 2018 16:31:17 +0200 Subject: [PATCH 558/778] Fix for dateTime limiter - should be used on lead table and not lead_list --- app/bundles/LeadBundle/Entity/LeadListRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/LeadBundle/Entity/LeadListRepository.php b/app/bundles/LeadBundle/Entity/LeadListRepository.php index 3b4352522f0..e57d7a003c9 100644 --- a/app/bundles/LeadBundle/Entity/LeadListRepository.php +++ b/app/bundles/LeadBundle/Entity/LeadListRepository.php @@ -427,7 +427,7 @@ public function getLeadsByList($lists, $args = [], Logger $logger = null) if (!empty($batchLimiters['dateTime'])) { // Only leads in the list at the time of count $listOnExpr->add( - $q->expr()->lte('ll.date_added', $q->expr()->literal($batchLimiters['dateTime'])) + $q->expr()->lte('l.date_added', $q->expr()->literal($batchLimiters['dateTime'])) ); } From b965dc2b9f619b971e66344554cfe60deaf7f229 Mon Sep 17 00:00:00 2001 From: Petr Fidler Date: Fri, 1 Jun 2018 17:11:26 +0200 Subject: [PATCH 559/778] Check command fix - prevent by division by 0 error --- app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php index fc49c3a1314..f076697186a 100644 --- a/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php +++ b/app/bundles/LeadBundle/Command/CheckQueryBuildersCommand.php @@ -94,6 +94,8 @@ protected function execute(InputInterface $input, OutputInterface $output) } $total = $ok + $failed; + $total = $total ?: 1; //prevent division be zero error + $output->writeln(''); $output->writeln(sprintf('Total success rate: %d%%, %d succeeded: and %s%s failed... ', round(($ok / $total) * 100), $ok, ($failed ? $failed : ''), (!$failed ? $failed : ''))); From 8f4f205819c9296f448e34c8423c916772abaca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 1 Jun 2018 18:36:32 +0200 Subject: [PATCH 560/778] Datasets fix --- app/bundles/EmailBundle/Model/EmailModel.php | 108 ++++++++----------- 1 file changed, 43 insertions(+), 65 deletions(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 16fa284ad1b..fcba037592d 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1754,15 +1754,9 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da if (!$canViewOthers) { $this->limitQueryToCreator($q); } - if ($companyId !== null) { - $this->addCompanyFilter($q, $companyId); - } - if ($campaignId !== null) { - $this->addCampaignFilter($q, $campaignId); - } - if ($segmentId !== null) { - $this->addSegmentFilter($q, $segmentId); - } + $this->addCompanyFilter($q, $companyId); + $this->addCampaignFilter($q, $campaignId); + $this->addSegmentFilter($q, $segmentId); $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.sent.emails'), $data); } @@ -1772,15 +1766,9 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da if (!$canViewOthers) { $this->limitQueryToCreator($q); } - if ($companyId !== null) { - $this->addCompanyFilter($q, $companyId); - } - if ($campaignId !== null) { - $this->addCampaignFilter($q, $campaignId); - } - if ($segmentId !== null) { - $this->addSegmentFilter($q, $segmentId); - } + $this->addCompanyFilter($q, $companyId); + $this->addCampaignFilter($q, $campaignId); + $this->addSegmentFilter($q, $segmentId); $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data); } @@ -1792,23 +1780,16 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da } $q->andWhere($q->expr()->eq('t.is_failed', ':true')) ->setParameter('true', true, 'boolean'); - if ($companyId !== null) { - $this->addCompanyFilter($q, $companyId); - } - if ($campaignId !== null) { - $this->addCampaignFilter($q, $campaignId); - } - if ($segmentId !== null) { - $this->addSegmentFilter($q, $segmentId); - } + $this->addCompanyFilter($q, $companyId); + $this->addCampaignFilter($q, $campaignId); + $this->addSegmentFilter($q, $segmentId); $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.failed.emails'), $data); } if ($flag == 'all' || $flag == 'clicked' || in_array('clicked', $datasets)) { $q = $query->prepareTimeDataQuery('page_hits', 'date_hit', []); - $q->andWhere('t.source = :source'); - $q->setParameter('source', 'email'); + $q->leftJoin('t', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 't.source_id = es.email_id AND t.source = "email"'); if (isset($filter['email_id'])) { if (is_array($filter['email_id'])) { @@ -1823,17 +1804,9 @@ public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $da if (!$canViewOthers) { $this->limitQueryToCreator($q); } - if ($companyId !== null) { - $this->addCompanyFilter($q, $companyId); - } - if ($campaignId !== null) { - $this->addCampaignFilter($q, $campaignId); - } - if ($segmentId !== null) { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') - ->andWhere('t.list_id = :segmentId') - ->setParameter('segmentId', $segmentId); - } + $this->addCompanyFilter($q, $companyId); + $this->addCampaignFilter($q, $campaignId); + $this->addSegmentFilter($q, $segmentId, 'es'); $data = $query->loadAndBuildTimeData($q); $chart->setDataset($this->translator->trans('mautic.email.clicked'), $data); @@ -1874,54 +1847,59 @@ public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reaso ->andWhere($q->expr()->eq('t.reason', ':reason')) ->setParameter('reason', $reason); + $q->leftJoin('t', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 't.channel_id = es.email_id AND t.channel = "email"'); + if (!$canViewOthers) { $this->limitQueryToCreator($q); } - if ($companyId !== null) { - $this->addCompanyFilter($q, $companyId); - } - if ($campaignId !== null) { - $this->addCampaignFilter($q, $campaignId); - } - if ($segmentId !== null) { - $this->addSegmentFilter($q, $segmentId); - } + $this->addCompanyFilter($q, $companyId); + $this->addCampaignFilter($q, $campaignId, 'es'); + $this->addSegmentFilter($q, $segmentId, 'es'); return $data = $query->loadAndBuildTimeData($q); } /** * @param QueryBuilder $q - * @param int $companyId + * @param int|null $companyId + * @param string $fromAlias */ - private function addCompanyFilter(QueryBuilder $q, $companyId) + private function addCompanyFilter(QueryBuilder $q, $companyId = null, $fromAlias = 't') { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', 't.lead_id = company_lead.lead_id') - ->andWhere('company_lead.company_id = :companyId') - ->setParameter('companyId', $companyId); + $q->leftJoin($fromAlias, MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', $fromAlias.'.lead_id = company_lead.lead_id'); + if ($companyId !== null) { + $q->andWhere('company_lead.company_id = :companyId') + ->setParameter('companyId', $companyId); + } } /** * @param QueryBuilder $q - * @param int $campaignId + * @param int|null $campaignId + * @param string $fromAlias */ - private function addCampaignFilter(QueryBuilder $q, $campaignId) + private function addCampaignFilter(QueryBuilder $q, $campaignId = null, $fromAlias = 't') { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 't.source_id = ce.id AND t.source = "campaign.event"') - ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') - ->andWhere('ce.campaign_id = :campaignId') - ->setParameter('campaignId', $campaignId); + $q->leftJoin($fromAlias, MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', $fromAlias.'.source_id = ce.id AND '.$fromAlias.'.source = "campaign.event"') + ->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id'); + if ($campaignId !== null) { + $q->andWhere('ce.campaign_id = :campaignId') + ->setParameter('campaignId', $campaignId); + } } /** * @param QueryBuilder $q - * @param int $segmentId + * @param int|null $segmentId + * @param string $fromAlias */ - private function addSegmentFilter(QueryBuilder $q, $segmentId) + private function addSegmentFilter(QueryBuilder $q, $segmentId = null, $fromAlias = 't') { - $q->innerJoin('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 't.list_id = ll.id') - ->andWhere('t.list_id = :segmentId') - ->setParameter('segmentId', $segmentId); + $q->leftJoin($fromAlias, MAUTIC_TABLE_PREFIX.'lead_lists', 'll', $fromAlias.'.list_id = ll.id'); + if ($segmentId !== null) { + $q->andWhere($fromAlias.'.list_id = :segmentId') + ->setParameter('segmentId', $segmentId); + } } /** From 44283c784def305c88b3aa384555a48f9dbaae2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hadam=C4=8D=C3=ADk?= Date: Fri, 1 Jun 2018 19:13:54 +0200 Subject: [PATCH 561/778] Change left joins to inner joins --- app/bundles/EmailBundle/Model/EmailModel.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index fcba037592d..9aadc6a67fc 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -1866,9 +1866,9 @@ public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reaso */ private function addCompanyFilter(QueryBuilder $q, $companyId = null, $fromAlias = 't') { - $q->leftJoin($fromAlias, MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', $fromAlias.'.lead_id = company_lead.lead_id'); if ($companyId !== null) { - $q->andWhere('company_lead.company_id = :companyId') + $q->innerJoin($fromAlias, MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', $fromAlias.'.lead_id = company_lead.lead_id') + ->andWhere('company_lead.company_id = :companyId') ->setParameter('companyId', $companyId); } } @@ -1880,10 +1880,10 @@ private function addCompanyFilter(QueryBuilder $q, $companyId = null, $fromAlias */ private function addCampaignFilter(QueryBuilder $q, $campaignId = null, $fromAlias = 't') { - $q->leftJoin($fromAlias, MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', $fromAlias.'.source_id = ce.id AND '.$fromAlias.'.source = "campaign.event"') - ->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id'); if ($campaignId !== null) { - $q->andWhere('ce.campaign_id = :campaignId') + $q->innerJoin($fromAlias, MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', $fromAlias.'.source_id = ce.id AND '.$fromAlias.'.source = "campaign.event"') + ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id') + ->andWhere('ce.campaign_id = :campaignId') ->setParameter('campaignId', $campaignId); } } @@ -1895,9 +1895,9 @@ private function addCampaignFilter(QueryBuilder $q, $campaignId = null, $fromAli */ private function addSegmentFilter(QueryBuilder $q, $segmentId = null, $fromAlias = 't') { - $q->leftJoin($fromAlias, MAUTIC_TABLE_PREFIX.'lead_lists', 'll', $fromAlias.'.list_id = ll.id'); if ($segmentId !== null) { - $q->andWhere($fromAlias.'.list_id = :segmentId') + $q->innerJoin($fromAlias, MAUTIC_TABLE_PREFIX.'lead_lists', 'll', $fromAlias.'.list_id = ll.id') + ->andWhere($fromAlias.'.list_id = :segmentId') ->setParameter('segmentId', $segmentId); } } From d0d36ffbb8a2888dafc169f3cdd2c0078926e5fa Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 1 Jun 2018 17:52:51 -0500 Subject: [PATCH 562/778] Fixed order of variable causing bad data --- .../EmailBundle/EventListener/DashboardSubscriber.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php index e42232f379a..8b04b4b8e15 100644 --- a/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php +++ b/app/bundles/EmailBundle/EventListener/DashboardSubscriber.php @@ -135,6 +135,7 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) if (isset($params['filter']['segmentId'])) { $segmentId = $params['filter']['segmentId']; } + $headItems = [ 'mautic.dashboard.label.contact.id', 'mautic.dashboard.label.contact.email.address', @@ -160,8 +161,8 @@ public function onWidgetDetailGenerate(WidgetDetailEvent $event) $params['dateTo'], ['groupBy' => 'sends', 'canViewOthers' => $canViewOthers], $companyId, - $segmentId, - $campaignId + $campaignId, + $segmentId ), ] ); From 4a6520ac7229546b33945228004f1bd9825dd6dd Mon Sep 17 00:00:00 2001 From: Alan Hartless Date: Fri, 1 Jun 2018 17:57:03 -0500 Subject: [PATCH 563/778] Prevent null values from being removed in the body --- app/bundles/EmailBundle/Model/EmailModel.php | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/bundles/EmailBundle/Model/EmailModel.php b/app/bundles/EmailBundle/Model/EmailModel.php index 0f0c21cbedc..a9a8df1c794 100644 --- a/app/bundles/EmailBundle/Model/EmailModel.php +++ b/app/bundles/EmailBundle/Model/EmailModel.php @@ -571,14 +571,14 @@ public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime 'open' => $stat['is_read'], 'click' => ($stat['link_hits'] !== null) ? $stat['link_hits'] : 0, 'links_clicked' => [], - 'email_id' => $stat['email_id'], - 'email_name' => $stat['email_name'], - 'segment_id' => $stat['segment_id'], - 'segment_name' => $stat['segment_name'], - 'company_id' => $stat['company_id'], - 'company_name' => $stat['company_name'], - 'campaign_id' => $stat['campaign_id'], - 'campaign_name' => $stat['campaign_name'], + 'email_id' => (string) $stat['email_id'], + 'email_name' => (string) $stat['email_name'], + 'segment_id' => (string) $stat['segment_id'], + 'segment_name' => (string) $stat['segment_name'], + 'company_id' => (string) $stat['company_id'], + 'company_name' => (string) $stat['company_name'], + 'campaign_id' => (string) $stat['campaign_id'], + 'campaign_name' => (string) $stat['campaign_name'], ]; if ($item['click'] && $item['email_id'] && $item['contact_id']) { @@ -621,11 +621,11 @@ public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $data = []; foreach ($redirects as $redirect) { $data[] = [ - 'url' => $redirect['url'], - 'unique_hits' => $redirect['unique_hits'], - 'hits' => $redirect['hits'], - 'email_id' => $redirect['email_id'], - 'email_name' => $redirect['email_name'], + 'url' => (string) $redirect['url'], + 'unique_hits' => (string) $redirect['unique_hits'], + 'hits' => (string) $redirect['hits'], + 'email_id' => (string) $redirect['email_id'], + 'email_name' => (string) $redirect['email_name'], ]; } From c7a09c63d0e715cbff3777fe9fa707bef5b35346 Mon Sep 17 00:00:00 2001 From: Zdeno Kuzmany Date: Fri, 27 Apr 2018 23:12:43 +0200 Subject: [PATCH 564/778] Inject new custom content below graph --- app/bundles/AssetBundle/Views/Asset/details.html.php | 2 ++ app/bundles/CampaignBundle/Views/Campaign/details.html.php | 2 ++ app/bundles/ChannelBundle/Views/Message/details.html.php | 2 ++ .../DynamicContentBundle/Views/DynamicContent/details.html.php | 2 ++ app/bundles/EmailBundle/Views/Email/details.html.php | 2 ++ app/bundles/FormBundle/Views/Form/details.html.php | 2 ++ app/bundles/LeadBundle/Views/List/details.html.php | 3 +++ app/bundles/PageBundle/Views/Page/details.html.php | 2 ++ 8 files changed, 17 insertions(+) diff --git a/app/bundles/AssetBundle/Views/Asset/details.html.php b/app/bundles/AssetBundle/Views/Asset/details.html.php index cec6c53d35e..255cb9e66d9 100644 --- a/app/bundles/AssetBundle/Views/Asset/details.html.php +++ b/app/bundles/AssetBundle/Views/Asset/details.html.php @@ -151,6 +151,8 @@
+ getCustomContent('asset.stats.graph', $mauticTemplateVars); ?> +
render( diff --git a/app/bundles/CampaignBundle/Views/Campaign/details.html.php b/app/bundles/CampaignBundle/Views/Campaign/details.html.php index eace407e881..35e697ffefa 100644 --- a/app/bundles/CampaignBundle/Views/Campaign/details.html.php +++ b/app/bundles/CampaignBundle/Views/Campaign/details.html.php @@ -153,6 +153,8 @@ class="caret"> trans('mautic.core.details
+ getCustomContent('campaign.stats.graph', $mauticTemplateVars); ?> +
+ getCustomContent('channel.stats.graph', $mauticTemplateVars); ?> +
+ getCustomContent('dynamiccontent.stats.graph', $mauticTemplateVars); ?> +
+ getCustomContent('form.stats.graph', $mauticTemplateVars); ?> +
+ + getCustomContent('segment.stats.graph', $mauticTemplateVars); ?> +
diff --git a/app/bundles/PageBundle/Views/Page/details.html.php b/app/bundles/PageBundle/Views/Page/details.html.php index 80e53574a81..365a452b307 100644 --- a/app/bundles/PageBundle/Views/Page/details.html.php +++ b/app/bundles/PageBundle/Views/Page/details.html.php @@ -171,6 +171,8 @@
+ getCustomContent('page.stats.graph', $mauticTemplateVars); ?> +
+ getCustomContent('sms.stats.graph', $mauticTemplateVars); ?> +
+ getCustomContent('focus.stats.graph', $mauticTemplateVars); ?> diff --git a/plugins/MauticSocialBundle/Views/Monitoring/details.html.php b/plugins/MauticSocialBundle/Views/Monitoring/details.html.php index 7af8b5ece44..d2ada0fface 100644 --- a/plugins/MauticSocialBundle/Views/Monitoring/details.html.php +++ b/plugins/MauticSocialBundle/Views/Monitoring/details.html.php @@ -86,6 +86,8 @@
+ getCustomContent('campaign.stats.graph', $mauticTemplateVars); ?> +
- getCustomContent('asset.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
diff --git a/app/bundles/CampaignBundle/Views/Campaign/details.html.php b/app/bundles/CampaignBundle/Views/Campaign/details.html.php index 35e697ffefa..85110508e5b 100644 --- a/app/bundles/CampaignBundle/Views/Campaign/details.html.php +++ b/app/bundles/CampaignBundle/Views/Campaign/details.html.php @@ -153,7 +153,7 @@ class="caret"> trans('mautic.core.details
- getCustomContent('campaign.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
- getCustomContent('channel.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
- getCustomContent('dynamiccontent.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
- getCustomContent('form.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
- getCustomContent('segment.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?> diff --git a/app/bundles/PageBundle/Views/Page/details.html.php b/app/bundles/PageBundle/Views/Page/details.html.php index 365a452b307..cf1466af948 100644 --- a/app/bundles/PageBundle/Views/Page/details.html.php +++ b/app/bundles/PageBundle/Views/Page/details.html.php @@ -171,7 +171,7 @@
- getCustomContent('page.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
- getCustomContent('sms.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
- getCustomContent('focus.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?> diff --git a/plugins/MauticSocialBundle/Views/Monitoring/details.html.php b/plugins/MauticSocialBundle/Views/Monitoring/details.html.php index d2ada0fface..f7f532a35e8 100644 --- a/plugins/MauticSocialBundle/Views/Monitoring/details.html.php +++ b/plugins/MauticSocialBundle/Views/Monitoring/details.html.php @@ -86,7 +86,7 @@
- getCustomContent('campaign.stats.graph', $mauticTemplateVars); ?> + getCustomContent('details.stats.graph.below', $mauticTemplateVars); ?>
diff --git a/app/bundles/CampaignBundle/Views/Event/jump.html.php b/app/bundles/CampaignBundle/Views/Event/jump.html.php index ea684e51631..b26caf71915 100644 --- a/app/bundles/CampaignBundle/Views/Event/jump.html.php +++ b/app/bundles/CampaignBundle/Views/Event/jump.html.php @@ -16,8 +16,11 @@
-
+
From 8a6a403bf877c5eeb3c38c2822f842e6a53e2351 Mon Sep 17 00:00:00 2001 From: Don Gilbert Date: Fri, 15 Jun 2018 15:37:53 -0400 Subject: [PATCH 696/778] Restrict the events that can be attached to the jump event. Add JS to disable the bottom anchor if nothing can be attached. Prevent campaignEventOnLoad from executing when an event modal is opened (wasted cycles) --- .../CampaignBundle/Assets/css/campaign.css | 1 + .../CampaignBundle/Assets/js/campaign.js | 50 ++++++++++++++----- .../EventListener/CampaignSubscriber.php | 18 +++++-- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/app/bundles/CampaignBundle/Assets/css/campaign.css b/app/bundles/CampaignBundle/Assets/css/campaign.css index 56dc8635e98..8ab497b1ac2 100644 --- a/app/bundles/CampaignBundle/Assets/css/campaign.css +++ b/app/bundles/CampaignBundle/Assets/css/campaign.css @@ -7,6 +7,7 @@ position: fixed; top: 15px; right: 30px; + z-index: 1040; } .campaign-builder .builder-content { diff --git a/app/bundles/CampaignBundle/Assets/js/campaign.js b/app/bundles/CampaignBundle/Assets/js/campaign.js index ebd64c85c67..73f25e0963e 100644 --- a/app/bundles/CampaignBundle/Assets/js/campaign.js +++ b/app/bundles/CampaignBundle/Assets/js/campaign.js @@ -152,7 +152,12 @@ Mautic.campaignOnUnload = function(container) { * @param response */ Mautic.campaignEventOnLoad = function (container, response) { - //new action created so append it to the form + if (!response.hasOwnProperty('eventId')) { + // There's nothing for us to do, so bail + return; + } + + // New action created so append it to the form var domEventId = 'CampaignEvent_' + response.eventId; var eventId = '#' + domEventId; @@ -163,12 +168,8 @@ Mautic.campaignEventOnLoad = function (container, response) { Mautic.campaignBuilderInstance.detach(Mautic.campaignBuilderLastConnection); } Mautic.campaignBuilderConnectionRequiresUpdate = false; - Mautic.campaignBuilderUpdateLabel(domEventId); - - if (response.hasOwnProperty("event")) { - Mautic.campaignBuilderCanvasEvents[response.event.id] = response.event; - } + Mautic.campaignBuilderCanvasEvents[response.event.id] = response.event; if (response.deleted) { Mautic.campaignBuilderInstance.remove(document.getElementById(domEventId)); @@ -192,12 +193,7 @@ Mautic.campaignEventOnLoad = function (container, response) { mQuery(eventId).css({'left': x + 'px', 'top': y + 'px'}); - if (response.eventType == 'decision' || response.eventType == 'condition') { - Mautic.campaignBuilderRegisterAnchors(['top', 'yes', 'no'], eventId); - } else { - Mautic.campaignBuilderRegisterAnchors(['top', 'bottom'], eventId); - } - + Mautic.campaignBuilderRegisterAnchors(Mautic.getAnchorsForEvent(response.event), eventId); Mautic.campaignBuilderInstance.draggable(domEventId, Mautic.campaignDragOptions); //activate new stuff @@ -240,6 +236,36 @@ Mautic.campaignEventOnLoad = function (container, response) { Mautic.campaignBuilderInstance.repaintEverything(); }; +/** + * Determine anchors to set up for the given event. + * + * This inspects the `connectionRestrictions` property + * within the event's settings that were passed when + * registering the event in your bundle's CampaignEventListener. + * + * @param event + */ +Mautic.getAnchorsForEvent = function (event) { + if (event.settings.hasOwnProperty('connectionRestrictions')) { + var restrictions = event.settings.connectionRestrictions.target; + + // If all connections are restricted, only anchor the top + if ( + restrictions.hasOwnProperty('decision') && restrictions.decision.length === 1 && restrictions.decision[0] === "none" && + restrictions.hasOwnProperty('action') && restrictions.action.length === 1 && restrictions.action[0] === "none" && + restrictions.hasOwnProperty('condition') && restrictions.condition.length === 1 && restrictions.condition[0] === "none" + ) { + return ['top']; + } + } + + if (event.eventType === 'decision' || event.eventType === 'condition') { + return ['top', 'yes', 'no']; + } + + return ['top', 'bottom']; +}; + /** * Setup the campaign source view * diff --git a/app/bundles/CampaignBundle/EventListener/CampaignSubscriber.php b/app/bundles/CampaignBundle/EventListener/CampaignSubscriber.php index 6d9cf60b99a..6a8c139af70 100644 --- a/app/bundles/CampaignBundle/EventListener/CampaignSubscriber.php +++ b/app/bundles/CampaignBundle/EventListener/CampaignSubscriber.php @@ -12,6 +12,7 @@ namespace Mautic\CampaignBundle\EventListener; use Mautic\CampaignBundle\CampaignEvents; +use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Event as Events; use Mautic\CampaignBundle\Form\Type\CampaignEventJumpToEventType; use Mautic\CoreBundle\EventListener\CommonSubscriber; @@ -111,11 +112,18 @@ public function onCampaignBuild(Events\CampaignBuilderEvent $event) { // Add action to jump to another event in the campaign flow. $event->addAction('campaign.jump_to_event', [ - 'label' => 'mautic.campaign.event.jump_to_event', - 'description' => 'mautic.campaign.event.jump_to_event_descr', - 'formType' => CampaignEventJumpToEventType::class, - 'template' => 'MauticCampaignBundle:Event:jump.html.php', - 'batchEventName' => CampaignEvents::ON_EVENT_JUMP_TO_EVENT, + 'label' => 'mautic.campaign.event.jump_to_event', + 'description' => 'mautic.campaign.event.jump_to_event_descr', + 'formType' => CampaignEventJumpToEventType::class, + 'template' => 'MauticCampaignBundle:Event:jump.html.php', + 'batchEventName' => CampaignEvents::ON_EVENT_JUMP_TO_EVENT, + 'connectionRestrictions' => [ + 'target' => [ + Event::TYPE_DECISION => ['none'], + Event::TYPE_ACTION => ['none'], + Event::TYPE_CONDITION => ['none'], + ], + ], ]); } } From b8a29d463781578d457258c170464de586ed15e3 Mon Sep 17 00:00:00 2001 From: Don Gilbert Date: Fri, 15 Jun 2018 16:52:14 -0400 Subject: [PATCH 697/778] Prevent jumping to decisions --- app/bundles/CampaignBundle/Assets/js/campaign.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/bundles/CampaignBundle/Assets/js/campaign.js b/app/bundles/CampaignBundle/Assets/js/campaign.js index 73f25e0963e..3fd43479036 100644 --- a/app/bundles/CampaignBundle/Assets/js/campaign.js +++ b/app/bundles/CampaignBundle/Assets/js/campaign.js @@ -1898,7 +1898,7 @@ Mautic.updateJumpToEventOptions = function() { for (var eventId in Mautic.campaignBuilderCanvasEvents) { var event = Mautic.campaignBuilderCanvasEvents[eventId]; - if (event.type !== 'campaign.jump_to_event') { + if (event.type !== 'campaign.jump_to_event' && event.eventType !== 'decision') { var opt = mQuery("