Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Mautic\PointBundle\Entity\GroupRepository;
|
||||
use Mautic\PointBundle\Event as Events;
|
||||
use Mautic\PointBundle\Form\Type\GroupType;
|
||||
use Mautic\PointBundle\PointGroupEvents;
|
||||
use Symfony\Component\Form\FormFactory;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<Group>
|
||||
*/
|
||||
class PointGroupModel extends CommonFormModel implements GlobalSearchInterface
|
||||
{
|
||||
public function getRepository(): GroupRepository
|
||||
{
|
||||
return $this->em->getRepository(Group::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:groups';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param FormFactory $formFactory
|
||||
* @param string|null $action
|
||||
* @param array<string,string> $options
|
||||
*
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, $formFactory, $action = null, $options = []): FormInterface
|
||||
{
|
||||
if (!$entity instanceof Group) {
|
||||
throw new MethodNotAllowedHttpException(['Group']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(GroupType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function getEntity($id = null): ?Group
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Group();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Group) {
|
||||
throw new MethodNotAllowedHttpException(['Group']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = PointGroupEvents::GROUP_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = PointGroupEvents::GROUP_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = PointGroupEvents::GROUP_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = PointGroupEvents::GROUP_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new Events\GroupEvent($entity);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function adjustPoints(Lead $contact, Group $group, int $points, string $operator = Lead::POINTS_ADD): Lead
|
||||
{
|
||||
$contactScore = $contact->getGroupScore($group);
|
||||
|
||||
if (empty($contactScore)) {
|
||||
$contactScore = new GroupContactScore();
|
||||
$contactScore->setContact($contact);
|
||||
$contactScore->setGroup($group);
|
||||
$contactScore->setScore(0);
|
||||
$contact->addGroupScore($contactScore);
|
||||
}
|
||||
$oldScore = $contactScore->getScore();
|
||||
$newScore = $oldScore;
|
||||
|
||||
match ($operator) {
|
||||
Lead::POINTS_ADD => $newScore += $points,
|
||||
Lead::POINTS_SUBTRACT => $newScore -= $points,
|
||||
Lead::POINTS_MULTIPLY => $newScore *= $points,
|
||||
Lead::POINTS_DIVIDE => $newScore /= $points,
|
||||
Lead::POINTS_SET => $newScore = $points,
|
||||
default => throw new \UnexpectedValueException('Invalid operator'),
|
||||
};
|
||||
$contactScore->setScore($newScore);
|
||||
$this->em->persist($contactScore);
|
||||
$this->em->flush();
|
||||
|
||||
$scoreChangeEvent = new Events\GroupScoreChangeEvent($contactScore, $oldScore, $newScore);
|
||||
$this->dispatcher->dispatch($scoreChangeEvent, PointGroupEvents::SCORE_CHANGE);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
public static function isAllowedPointOperation(string $operator): bool
|
||||
{
|
||||
return in_array($operator, [Lead::POINTS_ADD, Lead::POINTS_SUBTRACT, Lead::POINTS_MULTIPLY, Lead::POINTS_DIVIDE, Lead::POINTS_SET]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
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\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PointBundle\Entity\LeadPointLog;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\PointBundle\Entity\PointRepository;
|
||||
use Mautic\PointBundle\Event\PointActionEvent;
|
||||
use Mautic\PointBundle\Event\PointBuilderEvent;
|
||||
use Mautic\PointBundle\Event\PointEvent;
|
||||
use Mautic\PointBundle\Form\Type\PointType;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<Point>
|
||||
*/
|
||||
class PointModel extends CommonFormModel implements GlobalSearchInterface, ResetInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $actions = [];
|
||||
|
||||
public function __construct(
|
||||
protected RequestStack $requestStack,
|
||||
protected IpLookupHelper $ipLookupHelper,
|
||||
protected LeadModel $leadModel,
|
||||
private ContactTracker $contactTracker,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
private PointGroupModel $pointGroupModel,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PointRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Point::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:points';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Point) {
|
||||
throw new MethodNotAllowedHttpException(['Point']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
if (empty($options['pointActions'])) {
|
||||
$options['pointActions'] = $this->getPointActions();
|
||||
}
|
||||
|
||||
return $formFactory->create(PointType::class, $entity, $options);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Point
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Point();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Point) {
|
||||
throw new MethodNotAllowedHttpException(['Point']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = PointEvents::POINT_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = PointEvents::POINT_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = PointEvents::POINT_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = PointEvents::POINT_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new PointEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets array of custom actions from bundles subscribed PointEvents::POINT_ON_BUILD.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPointActions()
|
||||
{
|
||||
if ([] === $this->actions) {
|
||||
// build them
|
||||
$this->actions = [];
|
||||
$event = new PointBuilderEvent($this->translator);
|
||||
$this->dispatcher->dispatch($event, PointEvents::POINT_ON_BUILD);
|
||||
$this->actions['actions'] = $event->getActions();
|
||||
$this->actions['list'] = $event->getActionList();
|
||||
$this->actions['choices'] = $event->getActionChoices();
|
||||
}
|
||||
|
||||
return $this->actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a specific point change.
|
||||
*
|
||||
* @param mixed $eventDetails passthrough from function triggering action to the callback function
|
||||
* @param mixed $typeId Something unique to the triggering event to prevent unnecessary duplicate calls
|
||||
* @param bool $allowUserRequest
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function triggerAction($type, $eventDetails = null, $typeId = null, ?Lead $lead = null, $allowUserRequest = false): void
|
||||
{
|
||||
// only trigger actions for not logged Mautic users
|
||||
if (!$this->security->isAnonymous() && !$allowUserRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $typeId && MAUTIC_ENV === 'prod' && null !== $this->requestStack->getMainRequest()) {
|
||||
// let's prevent some unnecessary DB calls
|
||||
$session = $this->requestStack->getMainRequest()->getSession();
|
||||
$triggeredEvents = $session->get('mautic.triggered.point.actions', []);
|
||||
if (in_array($typeId, $triggeredEvents)) {
|
||||
return;
|
||||
}
|
||||
$triggeredEvents[] = $typeId;
|
||||
$session->set('mautic.triggered.point.actions', $triggeredEvents);
|
||||
}
|
||||
|
||||
// find all the actions for published points
|
||||
/** @var PointRepository $repo */
|
||||
$repo = $this->getRepository();
|
||||
$availablePoints = $repo->getPublishedByType($type);
|
||||
if (empty($availablePoints)) {
|
||||
return;
|
||||
}
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress();
|
||||
|
||||
$hasLeadPointChanges = false;
|
||||
if (null === $lead) {
|
||||
$lead = $this->contactTracker->getContact();
|
||||
|
||||
if (null === $lead || !$lead->getId()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get available actions
|
||||
$availableActions = $this->getPointActions();
|
||||
|
||||
// get a list of actions that has already been performed on this lead
|
||||
$completedActions = $repo->getCompletedLeadActions($type, $lead->getId());
|
||||
|
||||
$persist = [];
|
||||
/** @var Point $action */
|
||||
foreach ($availablePoints as $action) {
|
||||
// if it's already been done or not repeatable, then skip it
|
||||
if (!$action->getRepeatable() && isset($completedActions[$action->getId()])) {
|
||||
continue;
|
||||
}
|
||||
// make sure the action still exists
|
||||
if (!isset($availableActions['actions'][$action->getType()])) {
|
||||
continue;
|
||||
}
|
||||
$settings = $availableActions['actions'][$action->getType()];
|
||||
|
||||
$args = [
|
||||
'action' => [
|
||||
'id' => $action->getId(),
|
||||
'type' => $action->getType(),
|
||||
'name' => $action->getName(),
|
||||
'properties' => $action->getProperties(),
|
||||
'points' => $action->getDelta(),
|
||||
],
|
||||
'lead' => $lead,
|
||||
'eventDetails' => $eventDetails,
|
||||
];
|
||||
|
||||
$callback = $settings['callback'] ?? [\Mautic\PointBundle\Helper\EventHelper::class, 'engagePointAction'];
|
||||
|
||||
if (is_callable($callback)) {
|
||||
$object = null;
|
||||
if (is_array($callback)) {
|
||||
$reflection = new \ReflectionMethod($callback[0], $callback[1]);
|
||||
if (is_object($callback[0])) {
|
||||
$object = $callback[0];
|
||||
}
|
||||
} elseif (str_contains($callback, '::')) {
|
||||
$parts = explode('::', $callback);
|
||||
$reflection = new \ReflectionMethod($parts[0], $parts[1]);
|
||||
} else {
|
||||
$reflection = new \ReflectionMethod(null, $callback);
|
||||
}
|
||||
|
||||
$pass = [];
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
if (isset($args[$param->getName()])) {
|
||||
$pass[] = $args[$param->getName()];
|
||||
} else {
|
||||
$pass[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$pointsChange = $reflection->invokeArgs($object, $pass);
|
||||
|
||||
if ($pointsChange) {
|
||||
$delta = $action->getDelta();
|
||||
|
||||
$pointsChangeLogEntryName = $action->getId().': '.$action->getName();
|
||||
$pointGroup = $action->getGroup();
|
||||
if (!empty($pointGroup)) {
|
||||
$this->pointGroupModel->adjustPoints($lead, $pointGroup, $delta);
|
||||
} else {
|
||||
$lead->adjustPoints($delta);
|
||||
}
|
||||
|
||||
$hasLeadPointChanges = true;
|
||||
$parsed = explode('.', $action->getType());
|
||||
$lead->addPointsChangeLogEntry(
|
||||
$parsed[0],
|
||||
$pointsChangeLogEntryName,
|
||||
$parsed[1],
|
||||
$delta,
|
||||
$ipAddress,
|
||||
$pointGroup
|
||||
);
|
||||
|
||||
$event = new PointActionEvent($action, $lead);
|
||||
$this->dispatcher->dispatch($event, PointEvents::POINT_ON_ACTION);
|
||||
|
||||
if (!$action->getRepeatable()) {
|
||||
$log = new LeadPointLog();
|
||||
$log->setIpAddress($ipAddress);
|
||||
$log->setPoint($action);
|
||||
$log->setLead($lead);
|
||||
$log->setDateFired(new \DateTime());
|
||||
$persist[] = $log;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($persist)) {
|
||||
$this->getRepository()->saveEntities($persist);
|
||||
$this->getRepository()->detachEntities($persist);
|
||||
}
|
||||
|
||||
if ($hasLeadPointChanges) {
|
||||
$this->leadModel->saveEntity($lead);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of points.
|
||||
*
|
||||
* @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getPointLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$q = $query->prepareTimeDataQuery('lead_points_change_log', 'date_added', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id')
|
||||
->andWhere('l.owner_id = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.point.changes'), $data);
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getPointActionIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'p.type', 'expr' => 'in', 'value' => ['email.send', 'email.open']],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$pointActionIds = [];
|
||||
foreach ($entities as $entity) {
|
||||
$properties = $entity->getProperties();
|
||||
if (in_array($emailId, $properties['emails'] ?? [])) {
|
||||
$pointActionIds[] = $entity->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($pointActionIds);
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->actions = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
use Mautic\PointBundle\Entity\TriggerEventRepository;
|
||||
use Mautic\PointBundle\Form\Type\TriggerEventType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<TriggerEvent>
|
||||
*/
|
||||
class TriggerEventModel extends CommonFormModel
|
||||
{
|
||||
/**
|
||||
* @return TriggerEventRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(TriggerEvent::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:triggers';
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?TriggerEvent
|
||||
{
|
||||
if (null === $id) {
|
||||
return new TriggerEvent();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof TriggerEvent) {
|
||||
throw new MethodNotAllowedHttpException(['Trigger']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(TriggerEventType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get segments which are dependent on given segment.
|
||||
*
|
||||
* @param int $segmentId
|
||||
*/
|
||||
public function getReportIdsWithDependenciesOnSegment($segmentId): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'e.type', 'expr' => 'eq', 'value'=>'lead.changelists'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$dependents = [];
|
||||
foreach ($entities as $entity) {
|
||||
$retrFilters = $entity->getProperties();
|
||||
foreach ($retrFilters as $eachFilter) {
|
||||
if (in_array($segmentId, $eachFilter)) {
|
||||
$dependents[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dependents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getPointTriggerIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'e.type', 'expr' => 'in', 'value' => ['email.send', 'email.send_to_user']],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$triggerIds = [];
|
||||
foreach ($entities as $entity) {
|
||||
$properties = $entity->getProperties();
|
||||
if (isset($properties['email']) && (int) $properties['email'] === $emailId) {
|
||||
$triggerIds[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
if (isset($properties['useremail']['email']) && (int) $properties['useremail']['email'] === $emailId) {
|
||||
$triggerIds[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($triggerIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getPointTriggerIdsWithDependenciesOnTag(string $tagName): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'e.type', 'expr' => 'eq', 'value' => 'lead.changetags'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$triggerIds = [];
|
||||
foreach ($entities as $entity) {
|
||||
$properties = $entity->getProperties();
|
||||
foreach ($properties as $property) {
|
||||
if (in_array($tagName, $property)) {
|
||||
$triggerIds[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($triggerIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
<?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 '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user