/** * Create/update lead from form submit. * * @param $form * @param array $leadFieldMatches * * @return Lead */ protected function createLeadFromSubmit($form, array $leadFieldMatches, $leadFields) { //set the mapped data $inKioskMode = $form->isInKioskMode(); if (!$inKioskMode) { // Default to currently tracked lead $lead = $this->leadModel->getCurrentLead(); $leadId = $lead->getId(); $currentFields = $this->leadModel->flattenFields($lead->getFields()); $this->logger->debug('FORM: Not in kiosk mode so using current contact ID #' . $lead->getId()); } else { // Default to a new lead in kiosk mode $lead = new Lead(); $lead->setNewlyCreated(true); $currentFields = $leadFieldMatches; $leadId = null; $this->logger->debug('FORM: In kiosk mode so assuming a new contact'); } $uniqueLeadFields = $this->leadFieldModel->getUniqueIdentiferFields(); // Closure to get data and unique fields $getData = function ($currentFields, $uniqueOnly = false) use($leadFields, $uniqueLeadFields) { $uniqueFieldsWithData = $data = []; foreach ($leadFields as $alias => $properties) { $data[$alias] = ''; if (isset($currentFields[$alias])) { $value = $currentFields[$alias]; $data[$alias] = $value; // make sure the value is actually there and the field is one of our uniques if (!empty($value) && array_key_exists($alias, $uniqueLeadFields)) { $uniqueFieldsWithData[$alias] = $value; } } } return $uniqueOnly ? $uniqueFieldsWithData : [$data, $uniqueFieldsWithData]; }; // Closure to help search for a conflict $checkForIdentifierConflict = function ($fieldSet1, $fieldSet2) { // Find fields in both sets $potentialConflicts = array_keys(array_intersect_key($fieldSet1, $fieldSet2)); $this->logger->debug('FORM: Potential conflicts ' . implode(', ', array_keys($potentialConflicts)) . ' = ' . implode(', ', $potentialConflicts)); $conflicts = []; foreach ($potentialConflicts as $field) { if (!empty($fieldSet1[$field]) && !empty($fieldSet2[$field])) { if (strtolower($fieldSet1[$field]) !== strtolower($fieldSet2[$field])) { $conflicts[] = $field; } } } return [count($conflicts), $conflicts]; }; // Get data for the form submission list($data, $uniqueFieldsWithData) = $getData($leadFieldMatches); $this->logger->debug('FORM: Unique fields submitted include ' . implode(', ', $uniqueFieldsWithData)); // Check for duplicate lead /** @var \Mautic\LeadBundle\Entity\Lead[] $leads */ $leads = !empty($uniqueFieldsWithData) ? $this->em->getRepository('MauticLeadBundle:Lead')->getLeadsByUniqueFields($uniqueFieldsWithData, $leadId) : []; $uniqueFieldsCurrent = $getData($currentFields, true); if (count($leads)) { $this->logger->debug(count($leads) . ' found based on unique identifiers'); /** @var \Mautic\LeadBundle\Entity\Lead $foundLead */ $foundLead = $leads[0]; $this->logger->debug('FORM: Testing contact ID# ' . $foundLead->getId() . ' for conflicts'); // Check for a conflict with the currently tracked lead $foundLeadFields = $this->leadModel->flattenFields($foundLead->getFields()); // Get unique identifier fields for the found lead then compare with the lead currently tracked $uniqueFieldsFound = $getData($foundLeadFields, true); list($hasConflict, $conflicts) = $checkForIdentifierConflict($uniqueFieldsFound, $uniqueFieldsCurrent); if ($inKioskMode || $hasConflict) { // Use the found lead without merging because there is some sort of conflict with unique identifiers or in kiosk mode and thus should not merge $lead = $foundLead; if ($hasConflict) { $this->logger->debug('FORM: Conflicts found in ' . implode(', ', $conflicts) . ' so not merging'); } else { $this->logger->debug('FORM: In kiosk mode so not merging'); } } else { $this->logger->debug('FORM: Merging contacts ' . $lead->getId() . ' and ' . $foundLead->getId()); // Merge the found lead with currently tracked lead $lead = $this->leadModel->mergeLeads($lead, $foundLead); } // Update unique fields data for comparison with submitted data $currentFields = $this->leadModel->flattenFields($lead->getFields()); $uniqueFieldsCurrent = $getData($currentFields, true); } if (!$inKioskMode) { // Check for conflicts with the submitted data and the currently tracked lead list($hasConflict, $conflicts) = $checkForIdentifierConflict($uniqueFieldsWithData, $uniqueFieldsCurrent); $this->logger->debug('FORM: Current unique contact fields ' . implode(', ', array_keys($uniqueFieldsCurrent)) . ' = ' . implode(', ', $uniqueFieldsCurrent)); $this->logger->debug('FORM: Submitted unique contact fields ' . implode(', ', array_keys($uniqueFieldsWithData)) . ' = ' . implode(', ', $uniqueFieldsWithData)); if ($hasConflict) { // There's a conflict so create a new lead $lead = new Lead(); $lead->setNewlyCreated(true); $this->logger->debug('FORM: Conflicts found in ' . implode(', ', $conflicts) . ' between current tracked contact and submitted data so assuming a new contact'); } } //check for existing IP address $ipAddress = $this->ipLookupHelper->getIpAddress(); //no lead was found by a mapped email field so create a new one if ($lead->isNewlyCreated()) { if (!$inKioskMode) { $lead->addIpAddress($ipAddress); $this->logger->debug('FORM: Associating ' . $ipAddress->getIpAddress() . ' to contact'); } } elseif (!$inKioskMode) { $leadIpAddresses = $lead->getIpAddresses(); if (!$leadIpAddresses->contains($ipAddress)) { $lead->addIpAddress($ipAddress); $this->logger->debug('FORM: Associating ' . $ipAddress->getIpAddress() . ' to contact'); } } //set the mapped fields $this->leadModel->setFieldValues($lead, $data, false); if (!empty($event)) { $event->setIpAddress($ipAddress); $lead->addPointsChangeLog($event); } // last active time $lead->setLastActive(new \DateTime()); //create a new lead $this->leadModel->saveEntity($lead, false); if (!$inKioskMode) { // Set the current lead which will generate tracking cookies $this->leadModel->setCurrentLead($lead); } else { // Set system current lead which will still allow execution of events without generating tracking cookies $this->leadModel->setSystemCurrentLead($lead); } return $lead; }
/** * 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(); $campaignName = $campaign->getName(); $repo = $this->getRepository(); $campaignRepo = $this->getCampaignRepository(); // 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']); 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) { $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' => $campaignId, 'name' => $campaignName]; // 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); $repo->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 && $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 && $currentCount < $maxCount) { $progress->setProgress($currentCount); } // Free some memory gc_collect_cycles(); ++$batchDebugCounter; } if ($output) { $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; }