450 lines
15 KiB
PHP
Executable File
450 lines
15 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mautic\PointBundle\Model;
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
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\Model\GlobalSearchInterface;
|
|
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\Event\TriggerBuilderEvent;
|
|
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 implements GlobalSearchInterface
|
|
{
|
|
protected $triggers = [];
|
|
|
|
/**
|
|
* @var array<string, mixed[]>
|
|
*/
|
|
private $cachedEvents = [];
|
|
|
|
public function __construct(
|
|
protected IpLookupHelper $ipLookupHelper,
|
|
protected LeadModel $leadModel,
|
|
protected TriggerEventModel $pointTriggerEventModel,
|
|
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($this->cachedEvents)) {
|
|
$event = new TriggerBuilderEvent($this->translator);
|
|
$this->dispatcher->dispatch($event, PointEvents::TRIGGER_ON_BUILD);
|
|
$this->cachedEvents = $event->getEvents();
|
|
}
|
|
|
|
return $this->cachedEvents;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
|
|
private function invokeCallback($event, Lead $lead, array $settings): mixed
|
|
{
|
|
$args = [
|
|
'event' => $event,
|
|
'lead' => $lead,
|
|
'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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 '';
|
|
}
|
|
}
|