372 lines
13 KiB
PHP
Executable File
372 lines
13 KiB
PHP
Executable File
<?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 = [];
|
|
}
|
|
}
|