Spaces:
No application file
No application file
| namespace Mautic\PointBundle\Model; | |
| use Doctrine\ORM\EntityManagerInterface; | |
| use Mautic\CoreBundle\Factory\MauticFactory; | |
| use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
| use Mautic\CoreBundle\Helper\DateTimeHelper; | |
| use Mautic\CoreBundle\Helper\IpLookupHelper; | |
| use Mautic\CoreBundle\Helper\UserHelper; | |
| use Mautic\CoreBundle\Model\FormModel as CommonFormModel; | |
| use Mautic\CoreBundle\Security\Permissions\CorePermissions; | |
| use Mautic\CoreBundle\Translation\Translator; | |
| use Mautic\LeadBundle\Entity\Lead; | |
| use Mautic\LeadBundle\Entity\LeadRepository; | |
| use Mautic\LeadBundle\Model\LeadModel; | |
| use Mautic\LeadBundle\Tracker\ContactTracker; | |
| use Mautic\PointBundle\Entity\GroupContactScore; | |
| use Mautic\PointBundle\Entity\LeadTriggerLog; | |
| use Mautic\PointBundle\Entity\Trigger; | |
| use Mautic\PointBundle\Entity\TriggerEvent; | |
| use Mautic\PointBundle\Event as Events; | |
| use Mautic\PointBundle\Form\Type\TriggerType; | |
| use Mautic\PointBundle\PointEvents; | |
| use Psr\Log\LoggerInterface; | |
| use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
| use Symfony\Component\Form\FormFactoryInterface; | |
| use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; | |
| use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
| use Symfony\Contracts\EventDispatcher\Event; | |
| /** | |
| * @extends CommonFormModel<Trigger> | |
| */ | |
| class TriggerModel extends CommonFormModel | |
| { | |
| protected $triggers = []; | |
| /** | |
| * @var array<string,array<string,mixed>> | |
| */ | |
| private static array $events; | |
| public function __construct( | |
| protected IpLookupHelper $ipLookupHelper, | |
| protected LeadModel $leadModel, | |
| protected TriggerEventModel $pointTriggerEventModel, | |
| /** | |
| * @deprecated https://github.com/mautic/mautic/issues/8229 | |
| */ | |
| protected MauticFactory $mauticFactory, | |
| private ContactTracker $contactTracker, | |
| EntityManagerInterface $em, | |
| CorePermissions $security, | |
| EventDispatcherInterface $dispatcher, | |
| UrlGeneratorInterface $router, | |
| Translator $translator, | |
| UserHelper $userHelper, | |
| LoggerInterface $mauticLogger, | |
| CoreParametersHelper $coreParametersHelper | |
| ) { | |
| parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); | |
| } | |
| /** | |
| * @return \Mautic\PointBundle\Entity\TriggerRepository | |
| */ | |
| public function getRepository() | |
| { | |
| return $this->em->getRepository(Trigger::class); | |
| } | |
| /** | |
| * Retrieves an instance of the TriggerEventRepository. | |
| * | |
| * @return \Mautic\PointBundle\Entity\TriggerEventRepository | |
| */ | |
| public function getEventRepository() | |
| { | |
| return $this->em->getRepository(TriggerEvent::class); | |
| } | |
| public function getPermissionBase(): string | |
| { | |
| return 'point:triggers'; | |
| } | |
| /** | |
| * @throws MethodNotAllowedHttpException | |
| */ | |
| public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface | |
| { | |
| if (!$entity instanceof Trigger) { | |
| throw new MethodNotAllowedHttpException(['Trigger']); | |
| } | |
| if (!empty($action)) { | |
| $options['action'] = $action; | |
| } | |
| return $formFactory->create(TriggerType::class, $entity, $options); | |
| } | |
| /** | |
| * @param Trigger $entity | |
| * @param bool $unlock | |
| */ | |
| public function saveEntity($entity, $unlock = true): void | |
| { | |
| $isNew = ($entity->getId()) ? false : true; | |
| parent::saveEntity($entity, $unlock); | |
| // should we trigger for existing leads? | |
| if ($entity->getTriggerExistingLeads() && $entity->isPublished()) { | |
| $events = $entity->getEvents(); | |
| $repo = $this->getEventRepository(); | |
| $persist = []; | |
| $ipAddress = $this->ipLookupHelper->getIpAddress(); | |
| $pointGroup = $entity->getGroup(); | |
| /** @var LeadRepository $leadRepository */ | |
| $leadRepository = $this->em->getRepository(Lead::class); | |
| foreach ($events as $event) { | |
| $args = [ | |
| 'filter' => [ | |
| 'force' => [ | |
| [ | |
| 'column' => 'l.date_added', | |
| 'expr' => 'lte', | |
| 'value' => (new DateTimeHelper($entity->getDateAdded()))->toUtcString(), | |
| ], | |
| ], | |
| ], | |
| ]; | |
| if (!$pointGroup) { | |
| $args['filter']['force'][] = [ | |
| 'column' => 'l.points', | |
| 'expr' => 'gte', | |
| 'value' => $entity->getPoints(), | |
| ]; | |
| } else { | |
| $args['qb'] = $leadRepository->getEntitiesDbalQueryBuilder() | |
| ->leftJoin('l', MAUTIC_TABLE_PREFIX.GroupContactScore::TABLE_NAME, 'pls', 'l.id = pls.contact_id'); | |
| $args['filter']['force'][] = [ | |
| 'column' => 'pls.score', | |
| 'expr' => 'gte', | |
| 'value' => $entity->getPoints(), | |
| ]; | |
| $args['filter']['force'][] = [ | |
| 'column' => 'pls.group_id', | |
| 'expr' => 'eq', | |
| 'value' => $entity->getGroup()->getId(), | |
| ]; | |
| } | |
| if (!$isNew) { | |
| // get a list of leads that has already had this event applied | |
| $leadIds = $repo->getLeadsForEvent($event->getId()); | |
| if (!empty($leadIds)) { | |
| $args['filter']['force'][] = [ | |
| 'column' => 'l.id', | |
| 'expr' => 'notIn', | |
| 'value' => $leadIds, | |
| ]; | |
| } | |
| } | |
| // get a list of leads that are before the trigger's date_added and trigger if not already done so | |
| $leads = $this->leadModel->getEntities($args); | |
| /** @var Lead $l */ | |
| foreach ($leads as $l) { | |
| if ($this->triggerEvent($event->convertToArray(), $l, true)) { | |
| $log = new LeadTriggerLog(); | |
| $log->setIpAddress($ipAddress); | |
| $log->setEvent($event); | |
| $log->setLead($l); | |
| $log->setDateFired(new \DateTime()); | |
| $event->addLog($log); | |
| $persist[] = $event; | |
| } | |
| } | |
| } | |
| if (!empty($persist)) { | |
| $repo->saveEntities($persist); | |
| } | |
| } | |
| } | |
| public function getEntity($id = null): ?Trigger | |
| { | |
| if (null === $id) { | |
| return new Trigger(); | |
| } | |
| return parent::getEntity($id); | |
| } | |
| /** | |
| * @throws MethodNotAllowedHttpException | |
| */ | |
| protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null): ?Event | |
| { | |
| if (!$entity instanceof Trigger) { | |
| throw new MethodNotAllowedHttpException(['Trigger']); | |
| } | |
| switch ($action) { | |
| case 'pre_save': | |
| $name = PointEvents::TRIGGER_PRE_SAVE; | |
| break; | |
| case 'post_save': | |
| $name = PointEvents::TRIGGER_POST_SAVE; | |
| break; | |
| case 'pre_delete': | |
| $name = PointEvents::TRIGGER_PRE_DELETE; | |
| break; | |
| case 'post_delete': | |
| $name = PointEvents::TRIGGER_POST_DELETE; | |
| break; | |
| default: | |
| return null; | |
| } | |
| if ($this->dispatcher->hasListeners($name)) { | |
| if (empty($event)) { | |
| $event = new Events\TriggerEvent($entity, $isNew); | |
| } | |
| $this->dispatcher->dispatch($event, $name); | |
| return $event; | |
| } | |
| return null; | |
| } | |
| /** | |
| * @param array $sessionEvents | |
| */ | |
| public function setEvents(Trigger $entity, $sessionEvents): void | |
| { | |
| $order = 1; | |
| $existingActions = $entity->getEvents(); | |
| foreach ($sessionEvents as $properties) { | |
| $isNew = (!empty($properties['id']) && isset($existingActions[$properties['id']])) ? false : true; | |
| $event = !$isNew ? $existingActions[$properties['id']] : new TriggerEvent(); | |
| foreach ($properties as $f => $v) { | |
| if (in_array($f, ['id', 'order'])) { | |
| continue; | |
| } | |
| $func = 'set'.ucfirst($f); | |
| if (method_exists($event, $func)) { | |
| $event->$func($v); | |
| } | |
| } | |
| $event->setTrigger($entity); | |
| $event->setOrder($order); | |
| ++$order; | |
| $entity->addTriggerEvent($properties['id'], $event); | |
| } | |
| // Persist if editing the trigger | |
| if ($entity->getId()) { | |
| $this->pointTriggerEventModel->saveEntities($entity->getEvents()); | |
| } | |
| } | |
| /** | |
| * Gets array of custom events from bundles subscribed PointEvents::TRIGGER_ON_BUILD. | |
| * | |
| * @return mixed[] | |
| */ | |
| public function getEvents() | |
| { | |
| if (empty(self::$events)) { | |
| // build them | |
| self::$events = []; | |
| $event = new Events\TriggerBuilderEvent($this->translator); | |
| $this->dispatcher->dispatch($event, PointEvents::TRIGGER_ON_BUILD); | |
| self::$events = $event->getEvents(); | |
| } | |
| return self::$events; | |
| } | |
| /** | |
| * Gets array of custom events from bundles inside groups. | |
| * | |
| * @return mixed[] | |
| */ | |
| public function getEventGroups(): array | |
| { | |
| $events = $this->getEvents(); | |
| $groups = []; | |
| foreach ($events as $key => $event) { | |
| $groups[$event['group']][$key] = $event; | |
| } | |
| return $groups; | |
| } | |
| /** | |
| * Triggers a specific event. | |
| * | |
| * @param array $event triggerEvent converted to array | |
| * @param bool $force | |
| * | |
| * @return bool Was event triggered | |
| */ | |
| public function triggerEvent($event, Lead $lead = null, $force = false) | |
| { | |
| // only trigger events for anonymous users | |
| if (!$force && !$this->security->isAnonymous()) { | |
| return false; | |
| } | |
| if (null === $lead) { | |
| $lead = $this->contactTracker->getContact(); | |
| } | |
| if (!$force) { | |
| // get a list of events that has already been performed on this lead | |
| $appliedEvents = $this->getEventRepository()->getLeadTriggeredEvents($lead->getId()); | |
| // if it's already been done, then skip it | |
| if (isset($appliedEvents[$event['id']])) { | |
| return false; | |
| } | |
| } | |
| $availableEvents = $this->getEvents(); | |
| $eventType = $event['type']; | |
| // make sure the event still exists | |
| if (!isset($availableEvents[$eventType])) { | |
| return false; | |
| } | |
| $settings = $availableEvents[$eventType]; | |
| if (isset($settings['callback']) && is_callable($settings['callback'])) { | |
| return $this->invokeCallback($event, $lead, $settings); | |
| } else { | |
| /** @var TriggerEvent $triggerEvent */ | |
| $triggerEvent = $this->getEventRepository()->find($event['id']); | |
| $triggerExecutedEvent = new Events\TriggerExecutedEvent($triggerEvent, $lead); | |
| $this->dispatcher->dispatch($triggerExecutedEvent, $settings['eventName']); | |
| return $triggerExecutedEvent->getResult(); | |
| } | |
| } | |
| /** | |
| * @return bool | |
| */ | |
| private function invokeCallback($event, Lead $lead, array $settings) | |
| { | |
| $args = [ | |
| 'event' => $event, | |
| 'lead' => $lead, | |
| 'factory' => $this->mauticFactory, | |
| 'config' => $event['properties'], | |
| ]; | |
| if (is_array($settings['callback'])) { | |
| $reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]); | |
| } elseif (str_contains($settings['callback'], '::')) { | |
| $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); | |
| } | |
| /** | |
| * Trigger events for the current lead. | |
| */ | |
| public function triggerEvents(Lead $lead): void | |
| { | |
| $points = $lead->getPoints(); | |
| // find all published triggers that is applicable to this points | |
| /** @var \Mautic\PointBundle\Entity\TriggerEventRepository $repo */ | |
| $repo = $this->getEventRepository(); | |
| $events = $repo->getPublishedByPointTotal($points); | |
| $groupEvents = $repo->getPublishedByGroupScore($lead->getGroupScores()); | |
| $events = array_merge($events, $groupEvents); | |
| if (!empty($events)) { | |
| // get a list of actions that has already been applied to this lead | |
| $appliedEvents = $repo->getLeadTriggeredEvents($lead->getId()); | |
| $ipAddress = $this->ipLookupHelper->getIpAddress(); | |
| $persist = []; | |
| foreach ($events as $event) { | |
| if (isset($appliedEvents[$event['id']])) { | |
| // don't apply the event to the lead if it's already been done | |
| continue; | |
| } | |
| if ($this->triggerEvent($event, $lead, true)) { | |
| $log = new LeadTriggerLog(); | |
| $log->setIpAddress($ipAddress); | |
| $log->setEvent($triggerEvent = $this->getEventRepository()->find($event['id'])); | |
| $log->setLead($lead); | |
| $log->setDateFired(new \DateTime()); | |
| $persist[] = $log; | |
| } | |
| } | |
| if (!empty($persist)) { | |
| $this->getEventRepository()->saveEntities($persist); | |
| $this->getEventRepository()->detachEntities($persist); | |
| if (isset($triggerEvent)) { | |
| $this->getEventRepository()->deleteEntity($triggerEvent); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Returns configured color based on passed in $points. | |
| * | |
| * @return string | |
| */ | |
| public function getColorForLeadPoints($points) | |
| { | |
| if (!$this->triggers) { | |
| $this->triggers = $this->getRepository()->getTriggerColors(); | |
| } | |
| foreach ($this->triggers as $trigger) { | |
| if ($points >= $trigger['points']) { | |
| return $trigger['color']; | |
| } | |
| } | |
| return ''; | |
| } | |
| } | |